splent-cli 1.37.0__tar.gz → 1.38.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.37.0/src/splent_cli.egg-info → splent_cli-1.38.0}/PKG-INFO +1 -1
- {splent_cli-1.37.0 → splent_cli-1.38.0}/pyproject.toml +1 -1
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_release.py +13 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_test.py +226 -12
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_build.py +7 -4
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_create.py +33 -19
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_deploy.py +97 -16
- splent_cli-1.38.0/src/splent_cli/services/neutral_app.py +567 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/release.py +61 -11
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/release_dry_run.py +8 -3
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_.env.prod.example.j2 +12 -7
- {splent_cli-1.37.0 → splent_cli-1.38.0/src/splent_cli.egg-info}/PKG-INFO +1 -1
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli.egg-info/SOURCES.txt +1 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/COPYING +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/COPYING.LESSER +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/README.md +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/setup.cfg +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/__init__.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/__main__.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/cli.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/__init__.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/__init__.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/__init__.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_contracts.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_deps.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_docker.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_env.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_features.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_github.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_infra.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_product.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_releases.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_vendor.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/clear/__init__.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/command/__init__.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/command/command_create.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/coverage.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_console.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_dump.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_reset.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_restore.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_seed.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_status.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/doctor.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/env/env_list.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/env/env_set.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/env/env_show.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/export/__init__.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/export/export_puml.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_attach.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_compat.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_contract.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_create.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_detach.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_info.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_install.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_refine.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_remove.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_unlock.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/lint.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/locust.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/marketplace/login.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/marketplace/logout.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/marketplace/marketplace_index.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/marketplace/whoami.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/__init__.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_auto_require.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_clean.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_commands.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_config.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_configure.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_console.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_containers.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_derive.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_down.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_drift.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_env.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_list.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_logs.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_missing.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_port.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_release.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_restart.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_routes.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_run.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_select.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_shell.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_signals.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_test.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_up.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_validate.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/release/__init__.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/release/release_core.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/release/release_resume.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/selenium.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/__init__.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_compare.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_outdated.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_pin.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_publish.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_remove_feature.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/tokens_setup.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/version.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/__init__.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/compose.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/context.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/credentials.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/deploy_bundle.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/image_registry.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/index_refresh.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/marketplace.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/marketplace_api.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/marketplace_url.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/preflight.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/pypi_quota.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/registry.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/release_gate.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/spl_store.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/web_container.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_.gitignore.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_MANIFEST.in.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_README.md.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_commands.py.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_config.py.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_docker-compose.dev.yml.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_docker-compose.prod.yml.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_docker.env.example.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_forms.py.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_hooks.py.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_init.py.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_init_config.py.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_init_light.py.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_init_service.py.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_migrations_alembic.ini.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_migrations_env.py.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_migrations_script.py.mako.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_models.py.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_pyproject.toml.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_pyproject_minimal.toml.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_repositories.py.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_routes.py.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_scripts.js.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_seeders.py.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_services.py.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_signals.py.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_templates_index.html.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_tests_conftest.py.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_tests_e2e.py.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_tests_functional.py.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_tests_integration.py.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_tests_locustfile.py.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_tests_unit.py.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_webpack.config.js.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/licenses/COPYING +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/licenses/COPYING.LESSER +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_.env.dev.example.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_.gitignore.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_00_core_requirements_dev.sh.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_00_install_features.sh.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_01_compile_assets.sh.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_02_0_db_wait_connection.sh.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_02_1_db_create_db_test.sh.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_02_2_db_create_splent_migrations.sh.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_03_initialize_migrations.sh.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_04_handle_migrations.sh.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_05_0_start_app_dev.sh.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_05_1_start_app_prod.sh.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_400.html +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_401.html +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_404.html +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_500.html +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_Dockerfile.dev.dockerignore.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_Dockerfile.dev.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_Dockerfile.prod.dockerignore.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_Dockerfile.prod.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_README.md.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_app.css +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_app.js +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_base_template.html +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_ci_upgrade.yml.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_ci_validate.yml.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_config.py.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_docker-compose.dev.yml.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_docker-compose.prod.yml.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_dropzone.css +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_entrypoint.dev.sh.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_entrypoint.prod.sh.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_errors.py.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_favicon.svg +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_init.py.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_logging.py.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_own.css +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_package.json.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_pyproject.toml.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_uvl.j2 +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/__init__.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/archetype.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/cache_utils.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/ci_services.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/ci_workflow.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/command_loader.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/contract_freshness.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/db_client.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/db_utils.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/decorators.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/feature_installer.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/feature_naming.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/feature_requirements.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/feature_resolution.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/feature_utils.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/forge.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/git_url.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/integrity.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/io_utils.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/lifecycle.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/manifest.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/menu_keys.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/migration_lock.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/path_utils.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/proc.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/redact.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/refinement_facts.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/running_version.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/template_drift.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/toml_edit.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/upgrade_plan.py +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli.egg-info/requires.txt +0 -0
- {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli.egg-info/top_level.txt +0 -0
|
@@ -2008,6 +2008,14 @@ def create_versioned_snapshot(namespace, feature_name, version, workspace, repo=
|
|
|
2008
2008
|
is_flag=True,
|
|
2009
2009
|
help="If PyPI's new-project limit is spent, wait for it however long it takes.",
|
|
2010
2010
|
)
|
|
2011
|
+
@click.option(
|
|
2012
|
+
"--neutral",
|
|
2013
|
+
is_flag=True,
|
|
2014
|
+
help="Gate on the tests in a neutral app (the framework, the feature, the "
|
|
2015
|
+
"releases of what it requires, the default configuration), and run them in "
|
|
2016
|
+
"the active product as a check that does not block. Automatic when the "
|
|
2017
|
+
"product does not declare the feature.",
|
|
2018
|
+
)
|
|
2011
2019
|
def feature_release(
|
|
2012
2020
|
feature_ref,
|
|
2013
2021
|
version,
|
|
@@ -2020,6 +2028,7 @@ def feature_release(
|
|
|
2020
2028
|
no_github,
|
|
2021
2029
|
allow_unfinished,
|
|
2022
2030
|
wait,
|
|
2031
|
+
neutral,
|
|
2023
2032
|
):
|
|
2024
2033
|
no_forge = release.no_forge_requested(no_forge, no_github)
|
|
2025
2034
|
workspace = str(context.workspace())
|
|
@@ -2056,6 +2065,7 @@ def feature_release(
|
|
|
2056
2065
|
skip_checks=skip_checks,
|
|
2057
2066
|
attach=attach,
|
|
2058
2067
|
workspace=workspace,
|
|
2068
|
+
neutral=neutral,
|
|
2059
2069
|
)
|
|
2060
2070
|
return
|
|
2061
2071
|
|
|
@@ -2154,6 +2164,7 @@ def feature_release(
|
|
|
2154
2164
|
pre_commit_hook=_pre_commit,
|
|
2155
2165
|
post_pypi_hook=_post_pypi,
|
|
2156
2166
|
wait=wait,
|
|
2167
|
+
neutral=neutral,
|
|
2157
2168
|
)
|
|
2158
2169
|
|
|
2159
2170
|
attach_cmd = f"splent feature:attach {feature_ref} {tag}"
|
|
@@ -2282,6 +2293,7 @@ def _dry_run(
|
|
|
2282
2293
|
skip_checks,
|
|
2283
2294
|
attach,
|
|
2284
2295
|
workspace,
|
|
2296
|
+
neutral=False,
|
|
2285
2297
|
):
|
|
2286
2298
|
"""feature:release --dry-run. Changes nothing, exits 1 if a real run would stop."""
|
|
2287
2299
|
from splent_cli.services import release_dry_run
|
|
@@ -2350,6 +2362,7 @@ def _dry_run(
|
|
|
2350
2362
|
workspace=workspace,
|
|
2351
2363
|
product=product,
|
|
2352
2364
|
attach=attach,
|
|
2365
|
+
neutral=neutral,
|
|
2353
2366
|
)
|
|
2354
2367
|
if rehearsal.blockers:
|
|
2355
2368
|
raise SystemExit(1)
|
|
@@ -2,6 +2,7 @@ import os
|
|
|
2
2
|
import re
|
|
3
3
|
import shlex
|
|
4
4
|
import subprocess
|
|
5
|
+
import tempfile
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
import click
|
|
7
8
|
from dotenv import dotenv_values
|
|
@@ -14,7 +15,7 @@ from splent_cli.utils.feature_utils import (
|
|
|
14
15
|
get_normalize_feature_name_in_splent_format,
|
|
15
16
|
parse_feature_entry,
|
|
16
17
|
)
|
|
17
|
-
from splent_cli.services import context, web_container
|
|
18
|
+
from splent_cli.services import context, neutral_app, web_container
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
# Test levels in execution order (default = first three)
|
|
@@ -229,7 +230,7 @@ def _database_name(db_url) -> str:
|
|
|
229
230
|
return ""
|
|
230
231
|
|
|
231
232
|
|
|
232
|
-
def
|
|
233
|
+
def _refuse_production() -> None:
|
|
233
234
|
if _is_prod_active():
|
|
234
235
|
click.secho(
|
|
235
236
|
"❌ Cannot run tests in production.\n"
|
|
@@ -241,6 +242,10 @@ def _validate_testing_environment():
|
|
|
241
242
|
)
|
|
242
243
|
raise SystemExit(1)
|
|
243
244
|
|
|
245
|
+
|
|
246
|
+
def _validate_testing_environment():
|
|
247
|
+
_refuse_production()
|
|
248
|
+
|
|
244
249
|
try:
|
|
245
250
|
env = get_current_app_config_value("TESTING")
|
|
246
251
|
db_url = get_current_app_config_value("SQLALCHEMY_DATABASE_URI")
|
|
@@ -561,6 +566,161 @@ def with_import_path(dirs: list[str], env: dict) -> str:
|
|
|
561
566
|
return os.pathsep.join([*dirs, *(p for p in existing if p and p not in dirs)])
|
|
562
567
|
|
|
563
568
|
|
|
569
|
+
# ── The neutral app ───────────────────────────────────────────────────
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def declaring_product(feature_ref: str) -> str | None:
|
|
573
|
+
"""The active product when it declares the feature FEATURE_REF names.
|
|
574
|
+
|
|
575
|
+
None when it does not: its tests then run in the neutral app, since the
|
|
576
|
+
product's app does not load the feature.
|
|
577
|
+
"""
|
|
578
|
+
product = context.require_app()
|
|
579
|
+
name = get_normalize_feature_name_in_splent_format(_pkg_name(feature_ref))
|
|
580
|
+
declared = any(_pkg_name(ref) == name for ref in get_features_from_pyproject())
|
|
581
|
+
return product if declared else None
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _neutral_refusal(text: str) -> None:
|
|
585
|
+
click.secho(f"❌ {text}", fg="red")
|
|
586
|
+
raise SystemExit(1)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _feature_under_test(feature_ref: str, workspace: Path) -> neutral_app.Member:
|
|
590
|
+
"""The copy of FEATURE_REF whose tests the neutral app runs.
|
|
591
|
+
|
|
592
|
+
Its working copy at the workspace root, or the release FEATURE_REF names
|
|
593
|
+
(``recaptcha@v0.2.2``), from the cache.
|
|
594
|
+
"""
|
|
595
|
+
from splent_cli.utils.feature_resolution import resolve_feature_ref
|
|
596
|
+
|
|
597
|
+
found = resolve_feature_ref(feature_ref, workspace)
|
|
598
|
+
name = get_normalize_feature_name_in_splent_format(_pkg_name(feature_ref))
|
|
599
|
+
if found is None:
|
|
600
|
+
_neutral_refusal(
|
|
601
|
+
f"There is no copy of {name} in the workspace to test: no working\n"
|
|
602
|
+
f" copy at its root and no release in .splent_cache.\n"
|
|
603
|
+
f" Clone one first, for example with\n"
|
|
604
|
+
f" splent feature:clone splent-io/{name}"
|
|
605
|
+
)
|
|
606
|
+
root, namespace, name = found
|
|
607
|
+
version = root.name.partition("@")[2] or None
|
|
608
|
+
return neutral_app.Member(namespace, name, version, root, "under test")
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _tests_dir(member: neutral_app.Member) -> Path | None:
|
|
612
|
+
found = _collect_test_paths([(member.name, member.root)])
|
|
613
|
+
return found[0][1] if found else None
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _neutral_working_copies(with_refs, workspace: Path) -> list[tuple[str, Path]]:
|
|
617
|
+
"""The working copies --with names, for the neutral app.
|
|
618
|
+
|
|
619
|
+
Unlike in the product's app, a feature need not be declared anywhere:
|
|
620
|
+
the neutral app loads it from its working copy, in place of its release
|
|
621
|
+
when the feature under test requires it, and in addition otherwise.
|
|
622
|
+
"""
|
|
623
|
+
copies: list[tuple[str, Path]] = []
|
|
624
|
+
for ref in with_refs:
|
|
625
|
+
name = get_normalize_feature_name_in_splent_format(_pkg_name(ref))
|
|
626
|
+
path = editable_feature_dir(workspace, name)
|
|
627
|
+
if path is None:
|
|
628
|
+
short = name.removeprefix("splent_feature_")
|
|
629
|
+
_neutral_refusal(
|
|
630
|
+
f"--with {ref}, but {name} has no working copy at the workspace root.\n"
|
|
631
|
+
f" Run 'splent feature:unlock {short}' to make one."
|
|
632
|
+
)
|
|
633
|
+
if all(existing != name for existing, _ in copies):
|
|
634
|
+
copies.append((name, Path(path)))
|
|
635
|
+
return copies
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def neutral_plan(feature_ref: str, with_refs=()) -> list[neutral_app.Member]:
|
|
639
|
+
"""The features of the neutral app for FEATURE_REF's tests, printed.
|
|
640
|
+
|
|
641
|
+
Every release is fetched into the cache here, so the run in the
|
|
642
|
+
product's container finds them all.
|
|
643
|
+
"""
|
|
644
|
+
workspace = context.workspace()
|
|
645
|
+
product = context.require_app()
|
|
646
|
+
feature = _feature_under_test(feature_ref, workspace)
|
|
647
|
+
if declaring_product(feature_ref) is None:
|
|
648
|
+
click.secho(
|
|
649
|
+
f" {feature.name} is not declared in {product}, so its tests run "
|
|
650
|
+
"in a neutral app.",
|
|
651
|
+
fg="yellow",
|
|
652
|
+
)
|
|
653
|
+
copies = _neutral_working_copies(with_refs, workspace)
|
|
654
|
+
try:
|
|
655
|
+
members = neutral_app.plan(
|
|
656
|
+
workspace, feature, _tests_dir(feature), copies, say=click.echo
|
|
657
|
+
)
|
|
658
|
+
except neutral_app.NeutralAppError as e:
|
|
659
|
+
_neutral_refusal(f"The neutral app cannot be put together: {e}")
|
|
660
|
+
click.echo(
|
|
661
|
+
" Neutral app: the framework, these features and their default "
|
|
662
|
+
f"configuration.\n Nothing of {product} reaches it but its test database."
|
|
663
|
+
)
|
|
664
|
+
for line in neutral_app.describe(members):
|
|
665
|
+
click.echo(line)
|
|
666
|
+
return members
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def _run_neutral(
|
|
670
|
+
feature_ref, member_entries, with_refs, levels, keyword, verbose
|
|
671
|
+
) -> None:
|
|
672
|
+
"""Run FEATURE_REF's tests in the neutral app, in this container."""
|
|
673
|
+
if "e2e" in levels:
|
|
674
|
+
_neutral_refusal(
|
|
675
|
+
"End-to-end tests drive the product's running server, and the\n"
|
|
676
|
+
" neutral app is never served. Run them in a product that declares\n"
|
|
677
|
+
" the feature, without --neutral."
|
|
678
|
+
)
|
|
679
|
+
workspace = context.workspace()
|
|
680
|
+
try:
|
|
681
|
+
members = (
|
|
682
|
+
neutral_app.parse_members(workspace, member_entries)
|
|
683
|
+
if member_entries
|
|
684
|
+
else neutral_plan(feature_ref, with_refs)
|
|
685
|
+
)
|
|
686
|
+
except neutral_app.NeutralAppError as e:
|
|
687
|
+
_neutral_refusal(str(e))
|
|
688
|
+
name = get_normalize_feature_name_in_splent_format(_pkg_name(feature_ref))
|
|
689
|
+
feature = next((m for m in members if m.name == name), None)
|
|
690
|
+
if feature is None:
|
|
691
|
+
_neutral_refusal(f"The neutral app given does not hold {name}.")
|
|
692
|
+
|
|
693
|
+
_refuse_production()
|
|
694
|
+
problem = neutral_app.database_problem(dict(os.environ))
|
|
695
|
+
if problem:
|
|
696
|
+
_neutral_refusal(problem)
|
|
697
|
+
missing = neutral_app.missing_distributions(members)
|
|
698
|
+
if missing:
|
|
699
|
+
product = context.require_app()
|
|
700
|
+
_neutral_refusal(
|
|
701
|
+
f"The neutral app needs packages that {product}'s image does not\n"
|
|
702
|
+
" have at the version asked for:\n"
|
|
703
|
+
+ "\n".join(f" {line}" for line in missing)
|
|
704
|
+
+ "\n Install them in the container, or run the tests in a product\n"
|
|
705
|
+
" that declares the feature."
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
test_paths = _collect_test_paths([(feature.name, feature.root)])
|
|
709
|
+
if not test_paths:
|
|
710
|
+
click.secho("⚠️ No test directories found.", fg="yellow")
|
|
711
|
+
return
|
|
712
|
+
|
|
713
|
+
click.secho(
|
|
714
|
+
f"\n🧪 Running tests for {feature_ref} in a neutral app "
|
|
715
|
+
f"[{', '.join(levels)}]...",
|
|
716
|
+
fg="cyan",
|
|
717
|
+
)
|
|
718
|
+
with tempfile.TemporaryDirectory(prefix="splent-neutral-") as tmp:
|
|
719
|
+
neutral_app.write_app(Path(tmp), members)
|
|
720
|
+
env = neutral_app.environment(Path(tmp), members)
|
|
721
|
+
_run_pytest(test_paths, levels, keyword, verbose, environment=env)
|
|
722
|
+
|
|
723
|
+
|
|
564
724
|
def _resolve_levels(unit, integration, functional, e2e, load) -> tuple[str, ...]:
|
|
565
725
|
"""Determine which test levels to run based on CLI flags."""
|
|
566
726
|
explicit = []
|
|
@@ -583,7 +743,14 @@ def _run_pytest(
|
|
|
583
743
|
keyword: str | None,
|
|
584
744
|
verbose: bool,
|
|
585
745
|
extra_sources: list[tuple[str, Path]] = (),
|
|
746
|
+
environment: dict | None = None,
|
|
586
747
|
):
|
|
748
|
+
"""Run each feature's tests, level by level, and exit 1 if any failed.
|
|
749
|
+
|
|
750
|
+
ENVIRONMENT, when given, is the whole environment of every run (the
|
|
751
|
+
neutral app's). Otherwise each run gets this process's, with the import
|
|
752
|
+
path and the feature sources of the active product.
|
|
753
|
+
"""
|
|
587
754
|
passed = failed = skipped = 0
|
|
588
755
|
for pkg, test_dir, src_dir in test_paths:
|
|
589
756
|
click.secho(f"\n ▶ {pkg}", fg="cyan", bold=True)
|
|
@@ -596,7 +763,7 @@ def _run_pytest(
|
|
|
596
763
|
sources = [(pkg, Path(src_dir).parent)] + [
|
|
597
764
|
(name, Path(root)) for name, root in extra_sources if name != pkg
|
|
598
765
|
]
|
|
599
|
-
import_path = feature_src_dirs(sources)
|
|
766
|
+
import_path = feature_src_dirs(sources) if environment is None else []
|
|
600
767
|
|
|
601
768
|
for level in levels:
|
|
602
769
|
level_dir = test_dir / level
|
|
@@ -613,11 +780,14 @@ def _run_pytest(
|
|
|
613
780
|
click.secho(note, fg="yellow")
|
|
614
781
|
continue
|
|
615
782
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
783
|
+
if environment is not None:
|
|
784
|
+
env = dict(environment)
|
|
785
|
+
else:
|
|
786
|
+
env = os.environ.copy()
|
|
787
|
+
env["PYTHONPATH"] = with_import_path(import_path, env)
|
|
788
|
+
env["SPLENT_FEATURE_SOURCES"] = ",".join(
|
|
789
|
+
f"{name}={root}" for name, root in sources
|
|
790
|
+
)
|
|
621
791
|
|
|
622
792
|
cmd = [
|
|
623
793
|
"pytest",
|
|
@@ -710,6 +880,17 @@ def _run_pytest(
|
|
|
710
880
|
help="Load FEATURE from its working copy at the workspace root too, instead "
|
|
711
881
|
"of the version the product pins. Repeatable.",
|
|
712
882
|
)
|
|
883
|
+
@click.option(
|
|
884
|
+
"--neutral",
|
|
885
|
+
is_flag=True,
|
|
886
|
+
help="Run the tests in a neutral app instead of the product's: the "
|
|
887
|
+
"framework, the feature, the releases of what it requires and the "
|
|
888
|
+
"default configuration. Automatic for a feature the product does not "
|
|
889
|
+
"declare.",
|
|
890
|
+
)
|
|
891
|
+
# The neutral app as the CLI container resolved it, for the run in the
|
|
892
|
+
# product's container, which then fetches nothing.
|
|
893
|
+
@click.option("--neutral-member", "neutral_members", multiple=True, hidden=True)
|
|
713
894
|
def feature_test(
|
|
714
895
|
feature_ref,
|
|
715
896
|
keyword,
|
|
@@ -720,6 +901,8 @@ def feature_test(
|
|
|
720
901
|
e2e,
|
|
721
902
|
load,
|
|
722
903
|
with_features,
|
|
904
|
+
neutral,
|
|
905
|
+
neutral_members,
|
|
723
906
|
):
|
|
724
907
|
"""
|
|
725
908
|
Run the test suite for features declared in the active product.
|
|
@@ -736,12 +919,22 @@ def feature_test(
|
|
|
736
919
|
splent feature:test auth --functional # functional tests for auth
|
|
737
920
|
splent feature:test -k test_login -v # keyword filter, verbose
|
|
738
921
|
splent feature:test profile --with auth # profile's tests, auth's working copy
|
|
922
|
+
splent feature:test recaptcha # not in the product: a neutral app
|
|
923
|
+
splent feature:test theme --neutral # in the product, tested without it
|
|
739
924
|
|
|
740
925
|
\b
|
|
741
926
|
The app under test loads the feature whose tests run from the copy those
|
|
742
927
|
tests come from. --with does the same for other features the tests
|
|
743
928
|
cross into, so changes made to two features together are tested
|
|
744
929
|
together.
|
|
930
|
+
|
|
931
|
+
A feature the product does not declare is tested in a neutral app, and
|
|
932
|
+
--neutral chooses it for one the product declares. The neutral app is a
|
|
933
|
+
new product holding the framework, the feature, the newest releases of
|
|
934
|
+
what it requires and of what its tests import, and their default
|
|
935
|
+
configuration. Nothing of the product's templates, config.py or .env
|
|
936
|
+
reaches it, only its test database. --with puts a working copy in place
|
|
937
|
+
of a release there.
|
|
745
938
|
"""
|
|
746
939
|
levels_flags = (
|
|
747
940
|
("unit", unit),
|
|
@@ -750,12 +943,33 @@ def feature_test(
|
|
|
750
943
|
("e2e", e2e),
|
|
751
944
|
("load", load),
|
|
752
945
|
)
|
|
946
|
+
neutral = neutral or bool(neutral_members)
|
|
947
|
+
if neutral and not feature_ref:
|
|
948
|
+
raise click.UsageError(
|
|
949
|
+
"--neutral builds the app for one feature. Name it: "
|
|
950
|
+
"splent feature:test <feature> --neutral"
|
|
951
|
+
)
|
|
753
952
|
if not _is_product_container():
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
feature_ref, keyword, verbose, levels_flags, with_features
|
|
757
|
-
)
|
|
953
|
+
argv = _passthrough_args(
|
|
954
|
+
feature_ref, keyword, verbose, levels_flags, with_features
|
|
758
955
|
)
|
|
956
|
+
if feature_ref and (neutral or declaring_product(feature_ref) is None):
|
|
957
|
+
members = neutral_plan(feature_ref, with_features)
|
|
958
|
+
argv.append("--neutral")
|
|
959
|
+
for member in members:
|
|
960
|
+
argv += ["--neutral-member", member.entry]
|
|
961
|
+
_delegate_to_web_container(argv)
|
|
962
|
+
|
|
963
|
+
if feature_ref and (neutral or declaring_product(feature_ref) is None):
|
|
964
|
+
_run_neutral(
|
|
965
|
+
feature_ref,
|
|
966
|
+
neutral_members,
|
|
967
|
+
with_features,
|
|
968
|
+
_resolve_levels(unit, integration, functional, e2e, load),
|
|
969
|
+
keyword,
|
|
970
|
+
verbose,
|
|
971
|
+
)
|
|
972
|
+
return
|
|
759
973
|
|
|
760
974
|
# Look the feature up first: an undeclared one is refused before the
|
|
761
975
|
# guard builds an app that could not load it anyway.
|
|
@@ -98,10 +98,13 @@ OPTIONAL_DEPLOY_SETTINGS: tuple[tuple[str, str, tuple[str, ...]], ...] = (
|
|
|
98
98
|
"SPLENT_TRUSTED_PROXIES",
|
|
99
99
|
"1",
|
|
100
100
|
(
|
|
101
|
-
"Reverse proxies
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
"
|
|
101
|
+
"Reverse proxies whose X-Forwarded headers reach the app. 1 behind",
|
|
102
|
+
"the usual TLS front, and 1 with the nginx feature too, at the edge",
|
|
103
|
+
"or behind that front, since nginx is the only hop the app talks to",
|
|
104
|
+
"and forwards one value of each header. One more only for a proxy",
|
|
105
|
+
"that adds its own value to them. 0 when nothing is in front. Unset",
|
|
106
|
+
"means 0, so behind a proxy every caller shares the proxy's address",
|
|
107
|
+
"and X-Forwarded-Proto is ignored.",
|
|
105
108
|
),
|
|
106
109
|
),
|
|
107
110
|
(
|
|
@@ -309,6 +309,37 @@ def make_product(name, spl_name, spl_doi, features_file, force):
|
|
|
309
309
|
)
|
|
310
310
|
|
|
311
311
|
|
|
312
|
+
def app_package_files(name: str) -> tuple[dict[str, str], dict[str, str]]:
|
|
313
|
+
"""The product's own package, src/<name>/, as ``(rendered, copied)``.
|
|
314
|
+
|
|
315
|
+
Each maps a path inside the product to its template. product:create
|
|
316
|
+
writes them, and so does the neutral app feature:test runs a feature
|
|
317
|
+
in, which is a product created with that feature and nothing else
|
|
318
|
+
(services/neutral_app.py).
|
|
319
|
+
"""
|
|
320
|
+
rendered = {
|
|
321
|
+
f"src/{name}/__init__.py": "product/product_init.py.j2",
|
|
322
|
+
f"src/{name}/config.py": "product/product_config.py.j2",
|
|
323
|
+
f"src/{name}/errors.py": "product/product_errors.py.j2",
|
|
324
|
+
f"src/{name}/logging.py": "product/product_logging.py.j2",
|
|
325
|
+
}
|
|
326
|
+
copied = {
|
|
327
|
+
f"src/{name}/static/css/app.css": "product/product_app.css",
|
|
328
|
+
f"src/{name}/static/css/dropzone.css": "product/product_dropzone.css",
|
|
329
|
+
f"src/{name}/static/css/own.css": "product/product_own.css",
|
|
330
|
+
f"src/{name}/static/favicon.svg": "product/product_favicon.svg",
|
|
331
|
+
f"src/{name}/static/js/app.js": "product/product_app.js",
|
|
332
|
+
# 400/401 are the framework fallback's lookups; 404/500 come from
|
|
333
|
+
# the theme (errors/404.html, errors/500.html) so they wear the skin.
|
|
334
|
+
# There is no 403.html. Without one the framework draws the 403
|
|
335
|
+
# inside base_template.html, so the account keeps its sidebar.
|
|
336
|
+
f"src/{name}/templates/400.html": "product/product_400.html",
|
|
337
|
+
f"src/{name}/templates/401.html": "product/product_401.html",
|
|
338
|
+
f"src/{name}/templates/base_template.html": "product/product_base_template.html",
|
|
339
|
+
}
|
|
340
|
+
return rendered, copied
|
|
341
|
+
|
|
342
|
+
|
|
312
343
|
def _write_product(env, name, base_path, template_ctx):
|
|
313
344
|
"""Render the product tree into base_path, which does not exist yet."""
|
|
314
345
|
for subdir in [
|
|
@@ -353,26 +384,9 @@ def _write_product(env, name, base_path, template_ctx):
|
|
|
353
384
|
"package.json": "product/product_package.json.j2",
|
|
354
385
|
"pyproject.toml": "product/product_pyproject.toml.j2",
|
|
355
386
|
"README.md": "product/product_README.md.j2",
|
|
356
|
-
f"src/{name}/__init__.py": "product/product_init.py.j2",
|
|
357
|
-
f"src/{name}/config.py": "product/product_config.py.j2",
|
|
358
|
-
f"src/{name}/errors.py": "product/product_errors.py.j2",
|
|
359
|
-
f"src/{name}/logging.py": "product/product_logging.py.j2",
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
raw_files = {
|
|
363
|
-
f"src/{name}/static/css/app.css": "product/product_app.css",
|
|
364
|
-
f"src/{name}/static/css/dropzone.css": "product/product_dropzone.css",
|
|
365
|
-
f"src/{name}/static/css/own.css": "product/product_own.css",
|
|
366
|
-
f"src/{name}/static/favicon.svg": "product/product_favicon.svg",
|
|
367
|
-
f"src/{name}/static/js/app.js": "product/product_app.js",
|
|
368
|
-
# 400/401 are the framework fallback's lookups; 404/500 come from
|
|
369
|
-
# the theme (errors/404.html, errors/500.html) so they wear the skin.
|
|
370
|
-
# There is no 403.html. Without one the framework draws the 403
|
|
371
|
-
# inside base_template.html, so the account keeps its sidebar.
|
|
372
|
-
f"src/{name}/templates/400.html": "product/product_400.html",
|
|
373
|
-
f"src/{name}/templates/401.html": "product/product_401.html",
|
|
374
|
-
f"src/{name}/templates/base_template.html": "product/product_base_template.html",
|
|
375
387
|
}
|
|
388
|
+
package_templates, raw_files = app_package_files(name)
|
|
389
|
+
jinja_templates.update(package_templates)
|
|
376
390
|
|
|
377
391
|
for rel_path, tpl in jinja_templates.items():
|
|
378
392
|
abs_path = os.path.join(base_path, rel_path)
|
|
@@ -417,6 +417,95 @@ def compose_environment(
|
|
|
417
417
|
return env
|
|
418
418
|
|
|
419
419
|
|
|
420
|
+
# ── SPLENT_TRUSTED_PROXIES ──────────────────────────────────────────────
|
|
421
|
+
|
|
422
|
+
NGINX_FEATURE = "splent_feature_nginx"
|
|
423
|
+
#: The first nginx release that forwards one value of each X-Forwarded
|
|
424
|
+
#: header, after deciding with realip whether to believe a TLS front.
|
|
425
|
+
NGINX_ONE_VALUE = (1, 4, 0)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def nginx_version(product_path: str) -> str | None:
|
|
429
|
+
"""The version of the nginx feature the product deploys.
|
|
430
|
+
|
|
431
|
+
``""`` when it declares nginx without a version, None when it does not
|
|
432
|
+
declare it, for production, at all.
|
|
433
|
+
"""
|
|
434
|
+
from splent_cli.utils.feature_utils import (
|
|
435
|
+
load_product_features,
|
|
436
|
+
parse_feature_entry,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
entries = load_product_features(product_path, "prod")
|
|
441
|
+
except Exception: # noqa: BLE001 - only the advice depends on it
|
|
442
|
+
return None
|
|
443
|
+
for entry in entries:
|
|
444
|
+
_, name, version = parse_feature_entry(entry)
|
|
445
|
+
if name == NGINX_FEATURE:
|
|
446
|
+
return version or ""
|
|
447
|
+
return None
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _release(version: str) -> tuple[int, ...] | None:
|
|
451
|
+
m = re.match(r"v?(\d+)\.(\d+)\.(\d+)", version or "")
|
|
452
|
+
return tuple(int(g) for g in m.groups()) if m else None
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def trusted_proxies_advice(env_vars: dict, nginx: str | None) -> list[str]:
|
|
456
|
+
"""What to tell the operator about SPLENT_TRUSTED_PROXIES, line by line.
|
|
457
|
+
|
|
458
|
+
Empty when the value looks right. The number counts the proxies whose
|
|
459
|
+
X-Forwarded values reach the app, one value per proxy, and the app
|
|
460
|
+
ignores the headers when it finds fewer values than that.
|
|
461
|
+
|
|
462
|
+
NGINX is the version of the nginx feature the product deploys (see
|
|
463
|
+
:func:`nginx_version`). That feature is the only hop the app talks to,
|
|
464
|
+
whether it is the edge or sits behind a TLS front: it decides with
|
|
465
|
+
NGINX_TRUSTED_PROXY_CIDRS whether to believe the front and forwards one
|
|
466
|
+
value of each header, so the right number with it is 1. 2, which the
|
|
467
|
+
CLI used to suggest for nginx behind a TLS front, made the app ignore
|
|
468
|
+
the headers: http:// links, cookies without Secure, no HSTS.
|
|
469
|
+
"""
|
|
470
|
+
raw = (env_vars.get("SPLENT_TRUSTED_PROXIES") or "").strip()
|
|
471
|
+
if not raw:
|
|
472
|
+
if nginx is not None:
|
|
473
|
+
return [
|
|
474
|
+
"SPLENT_TRUSTED_PROXIES is not set in deploy/.env, so the app "
|
|
475
|
+
"believes no X-Forwarded header.",
|
|
476
|
+
"The product runs the nginx feature, the only hop the app talks "
|
|
477
|
+
"to, so set it to 1, whether nginx is the edge or sits behind a "
|
|
478
|
+
"TLS front.",
|
|
479
|
+
]
|
|
480
|
+
return [
|
|
481
|
+
"SPLENT_TRUSTED_PROXIES is not set in deploy/.env, so the app "
|
|
482
|
+
"believes no X-Forwarded header.",
|
|
483
|
+
"Behind a reverse proxy set it to the number of proxies in front "
|
|
484
|
+
"(1 behind the usual TLS front). Set it to 0 when nothing is in "
|
|
485
|
+
"front.",
|
|
486
|
+
]
|
|
487
|
+
try:
|
|
488
|
+
hops = int(raw)
|
|
489
|
+
except ValueError:
|
|
490
|
+
return []
|
|
491
|
+
if nginx is None or hops <= 1:
|
|
492
|
+
return []
|
|
493
|
+
lines = [
|
|
494
|
+
f"SPLENT_TRUSTED_PROXIES is {hops} in deploy/.env, and the app talks to "
|
|
495
|
+
"nginx alone, which forwards one value of each X-Forwarded header.",
|
|
496
|
+
f"The app looks for {hops}, finds one and ignores them all: http:// "
|
|
497
|
+
"links, cookies without Secure, no HSTS. Set it to 1.",
|
|
498
|
+
]
|
|
499
|
+
release = _release(nginx)
|
|
500
|
+
if release is not None and release < NGINX_ONE_VALUE:
|
|
501
|
+
lines.append(
|
|
502
|
+
f"splent_feature_nginx {nginx} adds the TLS front to X-Forwarded-For "
|
|
503
|
+
"as well, so behind one the app takes the front for the client. "
|
|
504
|
+
"Upgrade it to v1.4.0 or later."
|
|
505
|
+
)
|
|
506
|
+
return lines
|
|
507
|
+
|
|
508
|
+
|
|
420
509
|
def _compose_cmd(product: str, product_path: str, env_path: str, *args) -> list:
|
|
421
510
|
"""``docker compose`` aimed at the deploy bundle, as a host would run it."""
|
|
422
511
|
return [
|
|
@@ -645,16 +734,11 @@ def product_deploy(down, ci, image):
|
|
|
645
734
|
# this setting the app takes the proxy for the caller: one address for
|
|
646
735
|
# every client and plain http in every link it builds. A deployment that
|
|
647
736
|
# predates the template line (or deleted it) is told once per deploy;
|
|
648
|
-
# an explicit 0 says nothing is in front and silences it.
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
"believes no X-Forwarded header.\n"
|
|
654
|
-
" Behind a reverse proxy set it to the number of proxies (1 behind "
|
|
655
|
-
"the usual TLS front). Set it to 0 when nothing is in front.",
|
|
656
|
-
fg="yellow",
|
|
657
|
-
)
|
|
737
|
+
# an explicit 0 says nothing is in front and silences it. With the nginx
|
|
738
|
+
# feature a number above 1 is as bad, and said too.
|
|
739
|
+
proxy_advice = trusted_proxies_advice(env_vars, nginx_version(product_path))
|
|
740
|
+
if proxy_advice:
|
|
741
|
+
click.secho("\n".join(f" {line}" for line in proxy_advice), fg="yellow")
|
|
658
742
|
|
|
659
743
|
# ---------------------------------------------------------
|
|
660
744
|
# Warn about write directories no volume preserves
|
|
@@ -897,14 +981,11 @@ def product_deploy(down, ci, image):
|
|
|
897
981
|
# compose output. Never set automatically, because a deployment that
|
|
898
982
|
# publishes the app port beyond the host would then let any client forge
|
|
899
983
|
# X-Forwarded-For.
|
|
900
|
-
if
|
|
984
|
+
if proxy_advice:
|
|
901
985
|
click.echo()
|
|
902
986
|
click.secho(
|
|
903
|
-
"
|
|
904
|
-
"
|
|
905
|
-
" Uncomment '# SPLENT_TRUSTED_PROXIES=1' there (2 with the nginx "
|
|
906
|
-
"feature behind the TLS front, 0 when nothing is in front) and run "
|
|
907
|
-
"product:deploy again.",
|
|
987
|
+
"\n".join(f" {line}" for line in proxy_advice)
|
|
988
|
+
+ "\n Change it in deploy/.env and run product:deploy again.",
|
|
908
989
|
fg="yellow",
|
|
909
990
|
)
|
|
910
991
|
|