splent-cli 1.40.0__tar.gz → 1.42.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.42.0}/PKG-INFO +1 -1
- {splent_cli-1.40.0 → splent_cli-1.42.0}/pyproject.toml +1 -1
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/cli.py +93 -34
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/database/db_status.py +18 -3
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_create.py +2 -1
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_test.py +122 -22
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_auto_require.py +24 -1
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_build.py +35 -10
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_commands.py +71 -37
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_configure.py +118 -6
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_deploy.py +8 -6
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_missing.py +26 -5
- splent_cli-1.42.0/src/splent_cli/commands/product/product_params.py +231 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_test.py +74 -4
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_validate.py +3 -11
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/uvl/uvl_utils.py +27 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/neutral_app.py +107 -9
- splent_cli-1.42.0/src/splent_cli/services/own_commands.py +76 -0
- splent_cli-1.42.0/src/splent_cli/templates/feature/feature_services_service.py.j2 +8 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_.env.prod.example.j2 +7 -0
- splent_cli-1.42.0/src/splent_cli/templates/product/product_03_initialize_migrations.sh.j2 +78 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_docker-compose.prod.yml.j2 +7 -3
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/feature_params.py +89 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/uvl_params.py +31 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0/src/splent_cli.egg-info}/PKG-INFO +1 -1
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli.egg-info/SOURCES.txt +3 -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.42.0}/COPYING +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/COPYING.LESSER +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/README.md +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/setup.cfg +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/__init__.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/__main__.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/__init__.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/cache/__init__.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/check/__init__.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/check/check_contracts.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/check/check_deps.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/check/check_docker.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/check/check_env.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/check/check_features.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/check/check_github.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/check/check_infra.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/check/check_product.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/check/check_releases.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/check/check_vendor.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/clear/__init__.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/command/__init__.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/command/command_create.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/coverage.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/database/db_console.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/database/db_dump.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/database/db_reset.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/database/db_restore.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/database/db_seed.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/doctor.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/env/env_list.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/env/env_set.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/env/env_show.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/export/__init__.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/export/export_puml.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_attach.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_compat.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_contract.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_detach.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_info.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_install.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_refine.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_release.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_remove.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_unlock.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/lint.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/locust.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/marketplace/login.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/marketplace/logout.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/marketplace/marketplace_index.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/marketplace/whoami.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/__init__.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_clean.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_config.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_console.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_containers.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_create.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_derive.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_down.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_drift.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_env.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_list.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_logs.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_port.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_release.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_restart.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_routes.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_run.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_select.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_shell.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_signals.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_up.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/release/__init__.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/release/release_core.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/release/release_resume.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/selenium.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/__init__.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_compare.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_outdated.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_pin.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_publish.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_remove_feature.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/tokens_setup.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/version.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/__init__.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/compose.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/context.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/credentials.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/deploy_bundle.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/image_registry.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/index_refresh.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/marketplace.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/marketplace_api.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/marketplace_url.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/preflight.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/pypi_quota.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/registry.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/release.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/release_dry_run.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/release_gate.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/spl_store.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/web_container.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_.gitignore.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_MANIFEST.in.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_README.md.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_commands.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_config.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_docker-compose.dev.yml.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_docker-compose.prod.yml.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_docker.env.example.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_forms.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_hooks.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_init.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_init_config.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_init_light.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_init_service.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_migrations_alembic.ini.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_migrations_env.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_migrations_script.py.mako.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_models.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_pyproject.toml.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_pyproject_minimal.toml.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_repositories.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_routes.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_scripts.js.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_seeders.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_services.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_signals.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_templates_index.html.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_tests_conftest.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_tests_e2e.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_tests_functional.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_tests_integration.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_tests_locustfile.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_tests_unit.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_webpack.config.js.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/licenses/COPYING +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/licenses/COPYING.LESSER +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_.env.dev.example.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_.gitignore.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_00_core_requirements_dev.sh.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_00_install_features.sh.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_01_compile_assets.sh.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_02_0_db_wait_connection.sh.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.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.42.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.42.0}/src/splent_cli/templates/product/product_04_handle_migrations.sh.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_05_0_start_app_dev.sh.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_05_1_start_app_prod.sh.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_400.html +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_401.html +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_404.html +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_500.html +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_Dockerfile.dev.dockerignore.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_Dockerfile.dev.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_Dockerfile.prod.dockerignore.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_Dockerfile.prod.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_README.md.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_app.css +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_app.js +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_base_template.html +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_ci_upgrade.yml.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_ci_validate.yml.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_config.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_docker-compose.dev.yml.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_dropzone.css +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_entrypoint.dev.sh.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_entrypoint.prod.sh.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_errors.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_favicon.svg +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_init.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_logging.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_own.css +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_package.json.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_pyproject.toml.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_uvl.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/__init__.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/archetype.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/cache_utils.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/ci_services.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/ci_workflow.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/command_loader.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/contract_freshness.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/db_client.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/db_utils.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/decorators.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/feature_installer.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/feature_naming.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/feature_requirements.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/feature_resolution.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/feature_utils.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/forge.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/git_url.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/integrity.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/io_utils.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/lifecycle.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/manifest.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/menu_keys.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/migration_lock.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/path_utils.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/proc.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/redact.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/refinement_facts.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/running_version.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/template_drift.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/toml_edit.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/upgrade_plan.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli.egg-info/requires.txt +0 -0
- {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli.egg-info/top_level.txt +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import contextlib
|
|
1
2
|
import logging
|
|
2
3
|
import os
|
|
3
4
|
|
|
@@ -20,6 +21,27 @@ def _missing_module(exc: BaseException) -> str | None:
|
|
|
20
21
|
return None
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
@contextlib.contextmanager
|
|
25
|
+
def _quiet_unless(debug: bool):
|
|
26
|
+
"""The framework's logging muted while the app is built for discovery.
|
|
27
|
+
|
|
28
|
+
Discovery runs before every command, including the ones a product that
|
|
29
|
+
is not built yet (selected, not configured or derived) runs while
|
|
30
|
+
getting there. Building its app then fails, and the framework logged
|
|
31
|
+
that failure with a full traceback on every single command, although
|
|
32
|
+
nothing was wrong. The failure is expected there and handled, so the
|
|
33
|
+
framework's logging is muted for the attempt.
|
|
34
|
+
"""
|
|
35
|
+
if debug:
|
|
36
|
+
yield
|
|
37
|
+
return
|
|
38
|
+
logging.disable(logging.CRITICAL)
|
|
39
|
+
try:
|
|
40
|
+
yield
|
|
41
|
+
finally:
|
|
42
|
+
logging.disable(logging.NOTSET)
|
|
43
|
+
|
|
44
|
+
|
|
23
45
|
class SPLENTCLI(click.Group):
|
|
24
46
|
"""
|
|
25
47
|
Main SPLENT CLI class.
|
|
@@ -27,6 +49,7 @@ class SPLENTCLI(click.Group):
|
|
|
27
49
|
- Automatically injects the Flask app context for commands marked with `requires_app = True`.
|
|
28
50
|
- Checks DB connectivity for commands marked with `requires_db = True`.
|
|
29
51
|
- Discovers CLI commands contributed by features via ``app.extensions["splent_feature_commands"]``.
|
|
52
|
+
- Discovers the product's own commands, ``product:<name>`` (services/own_commands.py).
|
|
30
53
|
- Displays commands grouped by category for a cleaner, more readable help output.
|
|
31
54
|
"""
|
|
32
55
|
|
|
@@ -49,44 +72,65 @@ class SPLENTCLI(click.Group):
|
|
|
49
72
|
return self._feature_cmds_cache
|
|
50
73
|
|
|
51
74
|
self._feature_cmds_cache: dict[str, click.BaseCommand] = {}
|
|
52
|
-
# Discovery runs before every command, including the ones a product
|
|
53
|
-
# that is not built yet (selected, not configured or derived) runs
|
|
54
|
-
# while getting there. Building its app then fails, and the framework
|
|
55
|
-
# logged that failure with a full traceback on every single command,
|
|
56
|
-
# although nothing was wrong. The failure is expected here and
|
|
57
|
-
# handled below, so the framework's logging is muted for the attempt.
|
|
58
75
|
debug = bool(os.getenv("SPLENT_DEBUG"))
|
|
59
|
-
if not debug:
|
|
60
|
-
logging.disable(logging.CRITICAL)
|
|
61
76
|
try:
|
|
62
|
-
|
|
77
|
+
with _quiet_unless(debug):
|
|
78
|
+
from splent_cli.utils.dynamic_imports import get_app
|
|
63
79
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
app = get_app()
|
|
81
|
+
with app.app_context():
|
|
82
|
+
registry = app.extensions.get("splent_feature_commands", {})
|
|
83
|
+
for feature_short, commands in registry.items():
|
|
84
|
+
cmd_names = ", ".join(c.name for c in commands if c.name)
|
|
85
|
+
group = click.Group(
|
|
86
|
+
name=f"feature:{feature_short}",
|
|
87
|
+
help=f"Commands contributed by splent_feature_{feature_short}.",
|
|
88
|
+
short_help=f"Subcommands: {cmd_names}"
|
|
89
|
+
if cmd_names
|
|
90
|
+
else f"Commands from splent_feature_{feature_short}.",
|
|
91
|
+
)
|
|
92
|
+
group.requires_app = True # type: ignore[attr-defined]
|
|
93
|
+
for cmd in commands:
|
|
94
|
+
group.add_command(cmd)
|
|
95
|
+
self._feature_cmds_cache[group.name] = group
|
|
80
96
|
except Exception as e:
|
|
81
97
|
if debug:
|
|
82
98
|
click.secho(
|
|
83
99
|
f" ⚠ Feature commands not loaded: {e}", fg="yellow", err=True
|
|
84
100
|
)
|
|
85
|
-
finally:
|
|
86
|
-
if not debug:
|
|
87
|
-
logging.disable(logging.NOTSET)
|
|
88
101
|
return self._feature_cmds_cache
|
|
89
102
|
|
|
103
|
+
def load_own_commands(self):
|
|
104
|
+
"""The product's own commands, ``product:<name>``, as an
|
|
105
|
+
:class:`~splent_cli.services.own_commands.OwnCommands`.
|
|
106
|
+
|
|
107
|
+
Found like a feature's: once the product's app is built, which
|
|
108
|
+
fails quietly here for a product that cannot be built yet. The
|
|
109
|
+
CLI's own product:* commands win over a product's.
|
|
110
|
+
"""
|
|
111
|
+
if hasattr(self, "_own_cmds_cache"):
|
|
112
|
+
return self._own_cmds_cache
|
|
113
|
+
|
|
114
|
+
from splent_cli.services import own_commands
|
|
115
|
+
|
|
116
|
+
self._own_cmds_cache = own_commands.NONE
|
|
117
|
+
debug = bool(os.getenv("SPLENT_DEBUG"))
|
|
118
|
+
try:
|
|
119
|
+
with _quiet_unless(debug):
|
|
120
|
+
from splent_cli.utils.dynamic_imports import get_app
|
|
121
|
+
|
|
122
|
+
app = get_app()
|
|
123
|
+
with app.app_context():
|
|
124
|
+
self._own_cmds_cache = own_commands.collect(
|
|
125
|
+
os.getenv("SPLENT_APP"), set(self.commands)
|
|
126
|
+
)
|
|
127
|
+
except Exception as e:
|
|
128
|
+
if debug:
|
|
129
|
+
click.secho(
|
|
130
|
+
f" ⚠ Product commands not loaded: {e}", fg="yellow", err=True
|
|
131
|
+
)
|
|
132
|
+
return self._own_cmds_cache
|
|
133
|
+
|
|
90
134
|
def get_command(self, ctx, cmd_name):
|
|
91
135
|
# "feature:<short>" belongs to the feature called <short> whenever the
|
|
92
136
|
# product installs one. Built-ins win everywhere else.
|
|
@@ -100,13 +144,19 @@ class SPLENTCLI(click.Group):
|
|
|
100
144
|
feat_cmds = self._load_feature_commands()
|
|
101
145
|
if cmd_name in feat_cmds:
|
|
102
146
|
return feat_cmds[cmd_name]
|
|
103
|
-
|
|
147
|
+
command = super().get_command(ctx, cmd_name)
|
|
148
|
+
# "product:<name>" is the product's only where the CLI has no
|
|
149
|
+
# command of that name, so asking for a built-in never imports it.
|
|
150
|
+
if command is None and cmd_name and cmd_name.startswith("product:"):
|
|
151
|
+
command = self.load_own_commands().commands.get(cmd_name)
|
|
152
|
+
return command
|
|
104
153
|
|
|
105
154
|
def list_commands(self, ctx):
|
|
106
155
|
builtin = super().list_commands(ctx)
|
|
107
156
|
feat = sorted(self._load_feature_commands().keys())
|
|
157
|
+
own = sorted(self.load_own_commands().commands)
|
|
108
158
|
# A name a feature claimed is listed once, as the feature's.
|
|
109
|
-
return [name for name in builtin if name not in feat] + feat
|
|
159
|
+
return [name for name in builtin if name not in feat] + feat + own
|
|
110
160
|
|
|
111
161
|
# ── App context injection ─────────────────────────────────────
|
|
112
162
|
|
|
@@ -132,9 +182,14 @@ class SPLENTCLI(click.Group):
|
|
|
132
182
|
cmd_name = ctx.protected_args[0] if ctx.protected_args else None
|
|
133
183
|
command = self.get_command(ctx, cmd_name)
|
|
134
184
|
|
|
135
|
-
# A feature's own
|
|
136
|
-
# here they may be missing although the
|
|
137
|
-
|
|
185
|
+
# A feature's commands, and the product's own, are only found by
|
|
186
|
+
# building the app, so here they may be missing although the
|
|
187
|
+
# product has them.
|
|
188
|
+
if (
|
|
189
|
+
command is None
|
|
190
|
+
and cmd_name
|
|
191
|
+
and cmd_name.startswith(("feature:", "product:"))
|
|
192
|
+
):
|
|
138
193
|
self._delegate_to_the_product(ctx)
|
|
139
194
|
|
|
140
195
|
if command and getattr(command, "requires_app", False):
|
|
@@ -169,6 +224,7 @@ class SPLENTCLI(click.Group):
|
|
|
169
224
|
"""Group SPLENT commands by category in the CLI help output."""
|
|
170
225
|
all_cmds = self.list_commands(ctx)
|
|
171
226
|
feat_cmds = self._load_feature_commands()
|
|
227
|
+
own_cmds = self.load_own_commands().commands
|
|
172
228
|
groups = {
|
|
173
229
|
"🌿 Feature Management": [
|
|
174
230
|
cmd
|
|
@@ -176,7 +232,9 @@ class SPLENTCLI(click.Group):
|
|
|
176
232
|
if cmd.startswith("feature:") and cmd not in feat_cmds
|
|
177
233
|
],
|
|
178
234
|
"🏗️ Product Management": [
|
|
179
|
-
cmd
|
|
235
|
+
cmd
|
|
236
|
+
for cmd in all_cmds
|
|
237
|
+
if cmd.startswith("product:") and cmd not in own_cmds
|
|
180
238
|
],
|
|
181
239
|
"🧬 SPL & Variability": [
|
|
182
240
|
cmd for cmd in all_cmds if cmd.startswith(("spl:", "uvl:"))
|
|
@@ -207,6 +265,7 @@ class SPLENTCLI(click.Group):
|
|
|
207
265
|
if cmd in ("lint", "coverage", "selenium", "locust", "locust:stop")
|
|
208
266
|
],
|
|
209
267
|
"🔌 Feature Commands": [cmd for cmd in all_cmds if cmd in feat_cmds],
|
|
268
|
+
"🔌 Product Commands": [cmd for cmd in all_cmds if cmd in own_cmds],
|
|
210
269
|
}
|
|
211
270
|
total = 0
|
|
212
271
|
for title, cmds in groups.items():
|
|
@@ -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,79 @@ def _delegate_to_web_container(argv: list[str]) -> None:
|
|
|
84
87
|
)
|
|
85
88
|
|
|
86
89
|
|
|
87
|
-
def
|
|
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
|
+
|
|
153
|
+
def run_tests_in_web_container(product: str, argv: list[str], env=()) -> 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. ``env`` names more variables of this process's
|
|
162
|
+
environment for the run (product:test's [tool.splent.test] env).
|
|
94
163
|
"""
|
|
95
164
|
name = web_container.name(product)
|
|
96
165
|
if not _container_running(name):
|
|
@@ -112,6 +181,11 @@ def run_tests_in_web_container(product: str, argv: list[str]) -> int:
|
|
|
112
181
|
value = os.environ.get(var)
|
|
113
182
|
if value:
|
|
114
183
|
passthrough += ["-e", f"{var}={value}"]
|
|
184
|
+
# By name alone: docker reads each value from this process's
|
|
185
|
+
# environment, so a password never shows in the exec's arguments.
|
|
186
|
+
for var in env:
|
|
187
|
+
if var in os.environ:
|
|
188
|
+
passthrough += ["-e", var]
|
|
115
189
|
# The exec reads a pipe only this process writes to, so it sees the end
|
|
116
190
|
# of its input exactly when this process ends, however it ends.
|
|
117
191
|
read_end, write_end = os.pipe()
|
|
@@ -121,6 +195,7 @@ def run_tests_in_web_container(product: str, argv: list[str]) -> int:
|
|
|
121
195
|
"docker",
|
|
122
196
|
"exec",
|
|
123
197
|
"-i",
|
|
198
|
+
*workspace_user(name),
|
|
124
199
|
"-e",
|
|
125
200
|
f"SPLENT_APP={product}",
|
|
126
201
|
*passthrough,
|
|
@@ -694,15 +769,19 @@ def _run_neutral(
|
|
|
694
769
|
problem = neutral_app.database_problem(dict(os.environ))
|
|
695
770
|
if problem:
|
|
696
771
|
_neutral_refusal(problem)
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
772
|
+
product = context.require_app()
|
|
773
|
+
unmet = neutral_app.unmet_requirements(members)
|
|
774
|
+
from_workspace = [str(u) for u in unmet if not u.installable]
|
|
775
|
+
if from_workspace:
|
|
776
|
+
# The framework, the CLI and the features are the workspace's
|
|
777
|
+
# copies, which a run never replaces with a release.
|
|
700
778
|
_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
|
|
779
|
+
f"The neutral app needs SPLENT packages that {product}'s container\n"
|
|
780
|
+
" does not have at the version asked for:\n"
|
|
781
|
+
+ "\n".join(f" {line}" for line in from_workspace)
|
|
782
|
+
+ "\n The container has the workspace's copies of them. Update the\n"
|
|
783
|
+
" working copy, or run the tests in a product that declares the\n"
|
|
784
|
+
" feature."
|
|
706
785
|
)
|
|
707
786
|
|
|
708
787
|
test_paths = _collect_test_paths([(feature.name, feature.root)])
|
|
@@ -710,15 +789,33 @@ def _run_neutral(
|
|
|
710
789
|
click.secho("⚠️ No test directories found.", fg="yellow")
|
|
711
790
|
return
|
|
712
791
|
|
|
713
|
-
click.secho(
|
|
714
|
-
f"\n🧪 Running tests for {feature_ref} in a neutral app "
|
|
715
|
-
f"[{', '.join(levels)}]...",
|
|
716
|
-
fg="cyan",
|
|
717
|
-
)
|
|
718
792
|
with tempfile.TemporaryDirectory(prefix="splent-neutral-") as tmp:
|
|
793
|
+
venv_dir = None
|
|
794
|
+
if unmet:
|
|
795
|
+
click.echo(
|
|
796
|
+
f" {product}'s image lacks what these need, so the run installs "
|
|
797
|
+
"it in a\n virtual environment of its own, removed after it:"
|
|
798
|
+
)
|
|
799
|
+
for line in unmet:
|
|
800
|
+
click.echo(f" {line}")
|
|
801
|
+
try:
|
|
802
|
+
venv_dir = neutral_app.install_requirements(Path(tmp), unmet)
|
|
803
|
+
except neutral_app.NeutralAppError as e:
|
|
804
|
+
_neutral_refusal(
|
|
805
|
+
f"{e}\n Install them in the container, or run the tests "
|
|
806
|
+
"in a product\n that declares the feature."
|
|
807
|
+
)
|
|
808
|
+
click.secho(
|
|
809
|
+
f"\n🧪 Running tests for {feature_ref} in a neutral app "
|
|
810
|
+
f"[{', '.join(levels)}]...",
|
|
811
|
+
fg="cyan",
|
|
812
|
+
)
|
|
719
813
|
neutral_app.write_app(Path(tmp), members)
|
|
720
|
-
env = neutral_app.environment(Path(tmp), members)
|
|
721
|
-
|
|
814
|
+
env = neutral_app.environment(Path(tmp), members, venv_dir=venv_dir)
|
|
815
|
+
python = str(venv_dir / "bin" / "python") if venv_dir else None
|
|
816
|
+
_run_pytest(
|
|
817
|
+
test_paths, levels, keyword, verbose, environment=env, python=python
|
|
818
|
+
)
|
|
722
819
|
|
|
723
820
|
|
|
724
821
|
def _resolve_levels(unit, integration, functional, e2e, load) -> tuple[str, ...]:
|
|
@@ -744,12 +841,15 @@ def _run_pytest(
|
|
|
744
841
|
verbose: bool,
|
|
745
842
|
extra_sources: list[tuple[str, Path]] = (),
|
|
746
843
|
environment: dict | None = None,
|
|
844
|
+
python: str | None = None,
|
|
747
845
|
):
|
|
748
846
|
"""Run each feature's tests, level by level, and exit 1 if any failed.
|
|
749
847
|
|
|
750
848
|
ENVIRONMENT, when given, is the whole environment of every run (the
|
|
751
849
|
neutral app's). Otherwise each run gets this process's, with the import
|
|
752
|
-
path and the feature sources of the active product.
|
|
850
|
+
path and the feature sources of the active product. PYTHON, when given,
|
|
851
|
+
is the interpreter that runs pytest (the neutral app's virtual
|
|
852
|
+
environment).
|
|
753
853
|
"""
|
|
754
854
|
passed = failed = skipped = 0
|
|
755
855
|
for pkg, test_dir, src_dir in test_paths:
|
|
@@ -790,7 +890,7 @@ def _run_pytest(
|
|
|
790
890
|
)
|
|
791
891
|
|
|
792
892
|
cmd = [
|
|
793
|
-
"pytest",
|
|
893
|
+
*([python, "-m", "pytest"] if python else ["pytest"]),
|
|
794
894
|
str(level_dir),
|
|
795
895
|
"--rootdir=.",
|
|
796
896
|
"-W",
|
{splent_cli-1.40.0 → splent_cli-1.42.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.
|