splent-cli 1.34.1__tar.gz → 1.35.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.34.1/src/splent_cli.egg-info → splent_cli-1.35.0}/PKG-INFO +1 -1
- {splent_cli-1.34.1 → splent_cli-1.35.0}/pyproject.toml +1 -1
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/cli.py +48 -1
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_compat.py +135 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_create.py +7 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_detach.py +4 -3
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_env.py +4 -1
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_refine.py +7 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_remove.py +5 -4
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_test.py +5 -20
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_resolve.py +8 -1
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_sync_template.py +23 -1
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_validate.py +9 -1
- splent_cli-1.35.0/src/splent_cli/services/web_container.py +66 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_ci_validate.yml.j2 +15 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/cache_utils.py +34 -0
- splent_cli-1.35.0/src/splent_cli/utils/feature_naming.py +58 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/integrity.py +2 -2
- {splent_cli-1.34.1 → splent_cli-1.35.0/src/splent_cli.egg-info}/PKG-INFO +1 -1
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli.egg-info/SOURCES.txt +2 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/COPYING +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/COPYING.LESSER +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/README.md +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/setup.cfg +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/__init__.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/__main__.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/__init__.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/cache/__init__.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/check/__init__.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_contracts.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_deps.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_docker.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_env.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_features.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_github.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_infra.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_product.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_releases.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/clear/__init__.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/command/__init__.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/command/command_create.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/coverage.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_console.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_dump.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_reset.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_restore.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_seed.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_status.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/doctor.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/env/env_list.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/env/env_set.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/env/env_show.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/export/__init__.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/export/export_puml.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_attach.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_contract.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_info.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_install.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_release.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_unlock.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/lint.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/locust.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/marketplace/login.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/marketplace/logout.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/marketplace/marketplace_index.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/marketplace/whoami.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/__init__.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_auto_require.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_build.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_clean.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_commands.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_config.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_configure.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_console.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_containers.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_create.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_deploy.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_derive.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_down.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_drift.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_env.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_list.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_logs.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_missing.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_port.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_release.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_restart.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_routes.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_run.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_select.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_shell.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_signals.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_test.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_up.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/release/__init__.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/release/release_core.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/release/release_resume.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/selenium.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/__init__.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_compare.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_outdated.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_pin.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_publish.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_remove_feature.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/tokens_setup.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/version.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/__init__.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/compose.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/context.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/credentials.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/deploy_bundle.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/image_registry.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/index_refresh.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/marketplace.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/marketplace_api.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/marketplace_url.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/preflight.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/pypi_quota.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/registry.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/release.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/release_dry_run.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/release_gate.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/spl_store.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_.gitignore.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_MANIFEST.in.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_README.md.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_commands.py.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_config.py.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_docker-compose.dev.yml.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_docker-compose.prod.yml.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_docker.env.example.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_forms.py.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_hooks.py.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_init.py.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_init_config.py.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_init_light.py.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_init_service.py.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_migrations_alembic.ini.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_migrations_env.py.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_migrations_script.py.mako.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_models.py.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_pyproject.toml.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_pyproject_minimal.toml.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_repositories.py.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_routes.py.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_scripts.js.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_seeders.py.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_services.py.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_signals.py.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_templates_index.html.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_tests_conftest.py.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_tests_e2e.py.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_tests_functional.py.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_tests_integration.py.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_tests_locustfile.py.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_tests_unit.py.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_webpack.config.js.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/licenses/COPYING +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/licenses/COPYING.LESSER +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_.env.dev.example.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_.env.prod.example.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_.gitignore.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_00_core_requirements_dev.sh.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_00_install_features.sh.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_01_compile_assets.sh.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_02_0_db_wait_connection.sh.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_02_1_db_create_db_test.sh.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_02_2_db_create_splent_migrations.sh.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_03_initialize_migrations.sh.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_04_handle_migrations.sh.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_05_0_start_app_dev.sh.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_05_1_start_app_prod.sh.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_400.html +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_401.html +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_404.html +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_500.html +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_Dockerfile.dev.dockerignore.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_Dockerfile.dev.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_Dockerfile.prod.dockerignore.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_Dockerfile.prod.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_README.md.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_app.css +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_app.js +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_base_template.html +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_ci_upgrade.yml.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_config.py.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_docker-compose.dev.yml.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_docker-compose.prod.yml.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_dropzone.css +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_entrypoint.dev.sh.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_entrypoint.prod.sh.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_errors.py.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_favicon.svg +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_init.py.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_logging.py.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_own.css +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_package.json.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_pyproject.toml.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_uvl.j2 +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/__init__.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/archetype.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/command_loader.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/contract_freshness.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/db_client.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/db_utils.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/decorators.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/feature_installer.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/feature_utils.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/forge.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/git_url.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/io_utils.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/lifecycle.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/manifest.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/path_utils.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/proc.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/redact.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/refinement_facts.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/running_version.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/template_drift.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/toml_edit.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/upgrade_plan.py +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli.egg-info/requires.txt +0 -0
- {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli.egg-info/top_level.txt +0 -0
|
@@ -11,6 +11,15 @@ from splent_cli.utils.command_loader import load_commands
|
|
|
11
11
|
load_dotenv()
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def _missing_module(exc: BaseException) -> str | None:
|
|
15
|
+
"""The module whose absence made building the app fail, if that is why."""
|
|
16
|
+
while exc is not None:
|
|
17
|
+
if isinstance(exc, ModuleNotFoundError) and exc.name:
|
|
18
|
+
return exc.name
|
|
19
|
+
exc = exc.__cause__ or exc.__context__
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
|
|
14
23
|
class SPLENTCLI(click.Group):
|
|
15
24
|
"""
|
|
16
25
|
Main SPLENT CLI class.
|
|
@@ -101,15 +110,53 @@ class SPLENTCLI(click.Group):
|
|
|
101
110
|
|
|
102
111
|
# ── App context injection ─────────────────────────────────────
|
|
103
112
|
|
|
113
|
+
def _delegate_to_the_product(self, ctx) -> None:
|
|
114
|
+
"""Run this command in the product's web container, and exit with its
|
|
115
|
+
code; return when that container is not running.
|
|
116
|
+
|
|
117
|
+
Called only when the app cannot be built here: building it imports
|
|
118
|
+
what the features declare, which the product's image installs and the
|
|
119
|
+
CLI's may not (see services/web_container.py).
|
|
120
|
+
"""
|
|
121
|
+
from splent_cli.services import web_container
|
|
122
|
+
|
|
123
|
+
product = os.getenv("SPLENT_APP")
|
|
124
|
+
if not product or web_container.is_product_container():
|
|
125
|
+
return
|
|
126
|
+
if not web_container.running(web_container.name(product)):
|
|
127
|
+
return
|
|
128
|
+
argv = [*ctx.protected_args, *ctx.args]
|
|
129
|
+
raise SystemExit(web_container.delegate(product, argv))
|
|
130
|
+
|
|
104
131
|
def invoke(self, ctx):
|
|
105
132
|
cmd_name = ctx.protected_args[0] if ctx.protected_args else None
|
|
106
133
|
command = self.get_command(ctx, cmd_name)
|
|
107
134
|
|
|
135
|
+
# A feature's own commands are only found by building the app, so
|
|
136
|
+
# here they may be missing although the product has them.
|
|
137
|
+
if command is None and cmd_name and cmd_name.startswith("feature:"):
|
|
138
|
+
self._delegate_to_the_product(ctx)
|
|
139
|
+
|
|
108
140
|
if command and getattr(command, "requires_app", False):
|
|
109
141
|
from splent_cli.utils.dynamic_imports import get_app
|
|
110
142
|
from splent_cli.utils.db_utils import check_db_connection
|
|
111
143
|
|
|
112
|
-
|
|
144
|
+
try:
|
|
145
|
+
app = get_app()
|
|
146
|
+
except Exception as exc:
|
|
147
|
+
missing = _missing_module(exc)
|
|
148
|
+
if missing is None:
|
|
149
|
+
raise
|
|
150
|
+
# Only then: where the app builds here, the command runs here
|
|
151
|
+
# with the tools the CLI image has (ruff formats migrations).
|
|
152
|
+
self._delegate_to_the_product(ctx)
|
|
153
|
+
product = os.getenv("SPLENT_APP", "The product")
|
|
154
|
+
raise click.ClickException(
|
|
155
|
+
f"{product}'s features need {missing}, which the product's "
|
|
156
|
+
"image installs and the CLI's does not.\n"
|
|
157
|
+
" Start the product (splent product:up --dev) and run the "
|
|
158
|
+
"command again: it then runs in the product's web container."
|
|
159
|
+
) from exc
|
|
113
160
|
if getattr(command, "requires_db", False):
|
|
114
161
|
if not check_db_connection(app):
|
|
115
162
|
raise SystemExit(1)
|
|
@@ -411,6 +411,141 @@ def _analyse_all(labeled_contracts: list[tuple[str, dict]]) -> list[dict]:
|
|
|
411
411
|
return sorted(findings, key=lambda f: SEVERITY_ORDER[f["severity"]])
|
|
412
412
|
|
|
413
413
|
|
|
414
|
+
def _contract_env(path: Path) -> str | None:
|
|
415
|
+
"""The environment a feature's contract confines it to, if any."""
|
|
416
|
+
pyproject = path / "pyproject.toml"
|
|
417
|
+
if not pyproject.exists():
|
|
418
|
+
return None
|
|
419
|
+
data = load_toml(pyproject, what=f"pyproject.toml for {path.name}")
|
|
420
|
+
return data.get("tool", {}).get("splent", {}).get("contract", {}).get("env")
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def env_scope_findings(workspace: str, product_dir: str) -> list[dict]:
|
|
424
|
+
"""Features confined to one environment, in a list that installs them in
|
|
425
|
+
the other.
|
|
426
|
+
|
|
427
|
+
mailhog and phpmyadmin declare env = "dev" in their contract. In the base
|
|
428
|
+
list the production image installed them too, phpMyAdmin with autologin
|
|
429
|
+
to the database, and nothing said so.
|
|
430
|
+
"""
|
|
431
|
+
from splent_framework.utils.pyproject_reader import PyprojectReader
|
|
432
|
+
|
|
433
|
+
reader = PyprojectReader.for_product(product_dir)
|
|
434
|
+
lists = {
|
|
435
|
+
"features": reader.features,
|
|
436
|
+
"features_dev": reader.features_dev,
|
|
437
|
+
"features_prod": reader.features_prod,
|
|
438
|
+
}
|
|
439
|
+
findings = []
|
|
440
|
+
for key, entries in lists.items():
|
|
441
|
+
for entry in entries:
|
|
442
|
+
ns, name, version = _parse_pyproject_entry(entry)
|
|
443
|
+
path = product_feature_dir(workspace, product_dir, ns, name, version)
|
|
444
|
+
if path is None:
|
|
445
|
+
continue
|
|
446
|
+
scope = _contract_env(path)
|
|
447
|
+
short = name.removeprefix("splent_feature_")
|
|
448
|
+
if scope == "dev" and key != "features_dev":
|
|
449
|
+
message = (
|
|
450
|
+
f"{short} is a development tool (its contract says env = "
|
|
451
|
+
f'"dev"), but [tool.splent] {key} puts it in the production '
|
|
452
|
+
"image too. Move it to features_dev."
|
|
453
|
+
)
|
|
454
|
+
elif scope == "prod" and key == "features_dev":
|
|
455
|
+
message = (
|
|
456
|
+
f"{short} only runs in production (its contract says env = "
|
|
457
|
+
'"prod"), but it is in [tool.splent] features_dev. Move it '
|
|
458
|
+
"to features_prod."
|
|
459
|
+
)
|
|
460
|
+
else:
|
|
461
|
+
continue
|
|
462
|
+
findings.append(
|
|
463
|
+
{
|
|
464
|
+
"severity": "error",
|
|
465
|
+
"field": "env",
|
|
466
|
+
"message": message,
|
|
467
|
+
"features": [short],
|
|
468
|
+
}
|
|
469
|
+
)
|
|
470
|
+
return findings
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _joins_the_shared_network(compose_file: Path) -> bool:
|
|
474
|
+
"""Whether a compose file puts its services on the shared splent_network
|
|
475
|
+
instead of the product's own (``name: ${SPLENT_NETWORK:-splent_network}``)."""
|
|
476
|
+
import yaml
|
|
477
|
+
|
|
478
|
+
try:
|
|
479
|
+
data = yaml.safe_load(compose_file.read_text()) or {}
|
|
480
|
+
except (OSError, yaml.YAMLError):
|
|
481
|
+
return False
|
|
482
|
+
networks = data.get("networks") or {}
|
|
483
|
+
spec = networks.get("splent_network")
|
|
484
|
+
if not isinstance(spec, dict) or not spec.get("external"):
|
|
485
|
+
return False
|
|
486
|
+
return "SPLENT_NETWORK" not in str(spec.get("name", ""))
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def network_findings(workspace: str, product_dir: str) -> list[dict]:
|
|
490
|
+
"""Feature services the product cannot reach.
|
|
491
|
+
|
|
492
|
+
Each product has a network of its own, and a feature's services join it
|
|
493
|
+
through SPLENT_NETWORK. mailhog and phpmyadmin named the shared network
|
|
494
|
+
instead, so the app could not send mail to Mailhog and phpMyAdmin could
|
|
495
|
+
not reach the database, and nothing said why.
|
|
496
|
+
"""
|
|
497
|
+
from splent_framework.utils.pyproject_reader import PyprojectReader
|
|
498
|
+
|
|
499
|
+
reader = PyprojectReader.for_product(product_dir)
|
|
500
|
+
findings = []
|
|
501
|
+
for entry in reader.features + reader.features_dev + reader.features_prod:
|
|
502
|
+
ns, name, version = _parse_pyproject_entry(entry)
|
|
503
|
+
path = product_feature_dir(workspace, product_dir, ns, name, version)
|
|
504
|
+
if path is None:
|
|
505
|
+
continue
|
|
506
|
+
files = sorted((path / "docker").glob("docker-compose*.yml"))
|
|
507
|
+
if any(_joins_the_shared_network(f) for f in files):
|
|
508
|
+
short = name.removeprefix("splent_feature_")
|
|
509
|
+
findings.append(
|
|
510
|
+
{
|
|
511
|
+
"severity": "error",
|
|
512
|
+
"field": "docker",
|
|
513
|
+
"message": (
|
|
514
|
+
f"{short}'s services join the shared splent_network, "
|
|
515
|
+
"where the product's app cannot reach them. Its compose "
|
|
516
|
+
"file needs name: ${SPLENT_NETWORK:-splent_network} "
|
|
517
|
+
"under networks.splent_network."
|
|
518
|
+
),
|
|
519
|
+
"features": [short],
|
|
520
|
+
}
|
|
521
|
+
)
|
|
522
|
+
return findings
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def naming_findings(workspace: str, product_dir: str) -> list[dict]:
|
|
526
|
+
"""Features named after a product (auth_uvlhub). A warning, so the debt
|
|
527
|
+
shows in every validation until the product drops them."""
|
|
528
|
+
from splent_framework.utils.pyproject_reader import PyprojectReader
|
|
529
|
+
|
|
530
|
+
from splent_cli.utils.feature_naming import product_suffix, product_suffix_message
|
|
531
|
+
|
|
532
|
+
reader = PyprojectReader.for_product(product_dir)
|
|
533
|
+
findings = []
|
|
534
|
+
for entry in reader.features + reader.features_dev + reader.features_prod:
|
|
535
|
+
_, name, _ = _parse_pyproject_entry(entry)
|
|
536
|
+
product = product_suffix(name, workspace)
|
|
537
|
+
if product:
|
|
538
|
+
findings.append(
|
|
539
|
+
{
|
|
540
|
+
"severity": "warning",
|
|
541
|
+
"field": "name",
|
|
542
|
+
"message": product_suffix_message(name, product),
|
|
543
|
+
"features": [name.removeprefix("splent_feature_")],
|
|
544
|
+
}
|
|
545
|
+
)
|
|
546
|
+
return findings
|
|
547
|
+
|
|
548
|
+
|
|
414
549
|
def run_all_product_check(workspace: str, product_dir: str) -> list[dict]:
|
|
415
550
|
"""Run the full product compatibility check. Returns findings list."""
|
|
416
551
|
try:
|
|
@@ -9,6 +9,7 @@ from splent_cli.utils.feature_utils import (
|
|
|
9
9
|
require_valid_feature_ref,
|
|
10
10
|
)
|
|
11
11
|
from splent_cli.services import context
|
|
12
|
+
from splent_cli.utils.feature_naming import product_suffix, product_suffix_message
|
|
12
13
|
|
|
13
14
|
from splent_cli.utils.running_version import cli_version as _running_cli_version
|
|
14
15
|
|
|
@@ -223,6 +224,12 @@ def make_feature(full_name, feature_type, docker):
|
|
|
223
224
|
|
|
224
225
|
# --- Target directory (workspace root for editable features) ---
|
|
225
226
|
workspace = str(context.workspace())
|
|
227
|
+
|
|
228
|
+
product = product_suffix(feature_name, workspace)
|
|
229
|
+
if product:
|
|
230
|
+
click.secho(f"❌ {product_suffix_message(feature_name, product)}", fg="red")
|
|
231
|
+
raise SystemExit(1)
|
|
232
|
+
|
|
226
233
|
feature_dir = os.path.join(workspace, feature_name)
|
|
227
234
|
src_path = os.path.join(feature_dir, "src", org_safe, feature_name)
|
|
228
235
|
|
|
@@ -25,7 +25,7 @@ from splent_cli.utils.manifest import (
|
|
|
25
25
|
|
|
26
26
|
@click.command(
|
|
27
27
|
"feature:detach",
|
|
28
|
-
short_help="
|
|
28
|
+
short_help="Unpin a released version: the entry goes back to editable (cache is kept).",
|
|
29
29
|
)
|
|
30
30
|
@click.argument("feature_identifier", required=True)
|
|
31
31
|
@click.argument("version", required=True)
|
|
@@ -36,9 +36,10 @@ from splent_cli.utils.manifest import (
|
|
|
36
36
|
)
|
|
37
37
|
def feature_detach(feature_identifier, version, force):
|
|
38
38
|
"""
|
|
39
|
-
|
|
39
|
+
Unpin a released version, the reverse of feature:attach.
|
|
40
40
|
|
|
41
|
-
-
|
|
41
|
+
- Drops @version from the entry in pyproject.toml, which stays as the
|
|
42
|
+
bare, editable namespace/feature (feature:remove takes it out)
|
|
42
43
|
- Removes symlink in features/<namespace>/
|
|
43
44
|
- Leaves .splent_cache intact
|
|
44
45
|
"""
|
|
@@ -119,7 +119,10 @@ def feature_env(feature_name, generate, env_name):
|
|
|
119
119
|
|
|
120
120
|
if selected:
|
|
121
121
|
try:
|
|
122
|
-
|
|
122
|
+
# copyfile, not copy: a pinned feature's cache is read-only, and
|
|
123
|
+
# copying the mode made its .env read-only too, so product:env
|
|
124
|
+
# --merge failed writing __FEATURE_HOST_DIR__ into it.
|
|
125
|
+
shutil.copyfile(selected, env_file)
|
|
123
126
|
except OSError as exc:
|
|
124
127
|
click.echo(f"❌ Failed to create {env_file} from {selected}: {exc}")
|
|
125
128
|
raise SystemExit(1)
|
|
@@ -14,6 +14,7 @@ from pathlib import Path
|
|
|
14
14
|
import click
|
|
15
15
|
|
|
16
16
|
from splent_cli.services import context
|
|
17
|
+
from splent_cli.utils.feature_naming import product_suffix, product_suffix_message
|
|
17
18
|
from splent_cli.utils.feature_utils import normalize_namespace, read_features_from_data
|
|
18
19
|
from splent_cli.utils.refinement_facts import (
|
|
19
20
|
FeatureSource,
|
|
@@ -1009,6 +1010,12 @@ def feature_refinement(refiner_name):
|
|
|
1009
1010
|
# A feature other features refine cannot refine anything (a chain)
|
|
1010
1011
|
_refuse_if_refined(_get_product_features(workspace, product), refiner_name)
|
|
1011
1012
|
|
|
1013
|
+
# A refiner named after a product is a product patch, not variability.
|
|
1014
|
+
named_after = product_suffix(refiner_name, workspace)
|
|
1015
|
+
if named_after:
|
|
1016
|
+
click.secho(f" {product_suffix_message(refiner_name, named_after)}", fg="red")
|
|
1017
|
+
raise SystemExit(1)
|
|
1018
|
+
|
|
1012
1019
|
# Check if refinement already configured
|
|
1013
1020
|
refiner_data = load_toml(refiner_pyproject, what=f"{refiner_name} pyproject.toml")
|
|
1014
1021
|
existing_refinement = (
|
|
@@ -24,7 +24,7 @@ from splent_cli.utils.manifest import (
|
|
|
24
24
|
|
|
25
25
|
@click.command(
|
|
26
26
|
"feature:remove",
|
|
27
|
-
short_help="
|
|
27
|
+
short_help="Remove a feature from the active product, pinned or editable (keeps files).",
|
|
28
28
|
)
|
|
29
29
|
@click.argument("feature_name", required=True)
|
|
30
30
|
@click.option("--namespace", "-n", help="Namespace (defaults to 'splent-io').")
|
|
@@ -47,9 +47,10 @@ from splent_cli.utils.manifest import (
|
|
|
47
47
|
)
|
|
48
48
|
def feature_remove(feature_name, namespace, env_scope, force):
|
|
49
49
|
"""
|
|
50
|
-
Removes a
|
|
51
|
-
- Removes entry from
|
|
52
|
-
- Removes symlink under /workspace/<product>/features/<namespace
|
|
50
|
+
Removes a feature from the current SPLENT product, pinned or editable:
|
|
51
|
+
- Removes its entry from the feature lists in pyproject.toml
|
|
52
|
+
- Removes its symlink under /workspace/<product>/features/<namespace>/
|
|
53
|
+
The checkout and the cached release stay on disk.
|
|
53
54
|
|
|
54
55
|
\b
|
|
55
56
|
Without flags every list is searched (features, features_dev, features_prod).
|
|
@@ -13,7 +13,7 @@ from splent_cli.utils.feature_utils import (
|
|
|
13
13
|
get_normalize_feature_name_in_splent_format,
|
|
14
14
|
parse_feature_entry,
|
|
15
15
|
)
|
|
16
|
-
from splent_cli.services import context
|
|
16
|
+
from splent_cli.services import context, web_container
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
# Test levels in execution order (default = first three)
|
|
@@ -21,28 +21,13 @@ TEST_LEVELS = ("unit", "integration", "functional", "e2e", "load")
|
|
|
21
21
|
DEFAULT_LEVELS = ("unit", "integration", "functional")
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
SPLENT_CONTAINER is the explicit answer when set (the CLI container says
|
|
28
|
-
"cli", product images say "product"); /.dockerenv covers containers older
|
|
29
|
-
than the variable.
|
|
30
|
-
"""
|
|
31
|
-
container = os.getenv("SPLENT_CONTAINER")
|
|
32
|
-
if container:
|
|
33
|
-
return container != "cli"
|
|
34
|
-
return os.path.exists("/.dockerenv")
|
|
24
|
+
# Shared with every command that needs the product's app (see cli.py).
|
|
25
|
+
_is_product_container = web_container.is_product_container
|
|
26
|
+
_container_running = web_container.running
|
|
35
27
|
|
|
36
28
|
|
|
37
29
|
def _web_container() -> str:
|
|
38
|
-
return
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def _container_running(name: str) -> bool:
|
|
42
|
-
result = run(
|
|
43
|
-
["docker", "ps", "-q", "-f", f"name=^{name}$"], check=False, capture=True
|
|
44
|
-
)
|
|
45
|
-
return result.returncode == 0 and bool(result.stdout.strip())
|
|
30
|
+
return web_container.name(context.require_app())
|
|
46
31
|
|
|
47
32
|
|
|
48
33
|
_PASSTHROUGH_ENV = ("PYTEST_ADDOPTS", "PYTHONDONTWRITEBYTECODE")
|
|
@@ -9,7 +9,7 @@ from splent_cli.utils.feature_utils import (
|
|
|
9
9
|
parse_feature_entry,
|
|
10
10
|
read_features_from_data,
|
|
11
11
|
)
|
|
12
|
-
from splent_cli.utils.cache_utils import make_feature_writable
|
|
12
|
+
from splent_cli.utils.cache_utils import make_feature_writable, restore_snapshot
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def _splent_list(data: dict, key: str) -> list[str]:
|
|
@@ -194,11 +194,18 @@ def product_sync(ctx, force, yes, mode=None, strict=False):
|
|
|
194
194
|
failed += 1
|
|
195
195
|
continue
|
|
196
196
|
|
|
197
|
+
restored = [] if cloned else restore_snapshot(cache_dir)
|
|
197
198
|
_create_symlink(cache_dir, product_features_dir, link_path)
|
|
198
199
|
marker = click.style(" ✓", fg="green")
|
|
199
200
|
label = f" {short:<{max_w_r}}"
|
|
200
201
|
ver = click.style(f" {version}", dim=True)
|
|
201
202
|
src = click.style(" cached" if not cloned else " cloned", dim=True)
|
|
203
|
+
if restored:
|
|
204
|
+
src += click.style(
|
|
205
|
+
f" restored {len(restored)} file(s) to the release: "
|
|
206
|
+
+ ", ".join(restored),
|
|
207
|
+
fg="yellow",
|
|
208
|
+
)
|
|
202
209
|
tag = _feature_env_tag(entry, dev_set, prod_set)
|
|
203
210
|
click.echo(f" {marker}{label}{ver}{src}{tag}")
|
|
204
211
|
|
{splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_sync_template.py
RENAMED
|
@@ -2,8 +2,9 @@ import click
|
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
|
|
4
4
|
from splent_cli.services import context
|
|
5
|
-
from splent_cli.utils.io_utils import atomic_write
|
|
5
|
+
from splent_cli.utils.io_utils import atomic_write, load_toml
|
|
6
6
|
from splent_cli.utils.template_drift import (
|
|
7
|
+
CLI_VERSION,
|
|
7
8
|
PRODUCT_GROUPS,
|
|
8
9
|
count_changed_lines,
|
|
9
10
|
file_diff,
|
|
@@ -12,6 +13,23 @@ from splent_cli.utils.template_drift import (
|
|
|
12
13
|
resolve_product_dir,
|
|
13
14
|
resolve_product_rel,
|
|
14
15
|
)
|
|
16
|
+
from splent_cli.utils.toml_edit import write_toml
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _record_cli_version(product_path: Path) -> None:
|
|
20
|
+
"""Store the CLI whose templates the product now matches.
|
|
21
|
+
|
|
22
|
+
product:drift reports it as "generated with CLI vX". Only product:create
|
|
23
|
+
wrote it, so a product brought fully up to date kept naming the CLI it was
|
|
24
|
+
created with. A partial sync (--scripts, --docker...) leaves it alone,
|
|
25
|
+
because the other groups may still be older.
|
|
26
|
+
"""
|
|
27
|
+
pyproject = product_path / "pyproject.toml"
|
|
28
|
+
data = load_toml(pyproject, what="pyproject.toml")
|
|
29
|
+
splent = data.setdefault("tool", {}).setdefault("splent", {})
|
|
30
|
+
if splent.get("cli_version") != CLI_VERSION:
|
|
31
|
+
splent["cli_version"] = CLI_VERSION
|
|
32
|
+
write_toml(pyproject, data)
|
|
15
33
|
|
|
16
34
|
|
|
17
35
|
@click.command(
|
|
@@ -112,6 +130,8 @@ def product_sync_template(
|
|
|
112
130
|
changes[rel_path] = (abs_path, expected, n)
|
|
113
131
|
|
|
114
132
|
if not changes:
|
|
133
|
+
if not filter_active and not dry_run:
|
|
134
|
+
_record_cli_version(product_path)
|
|
115
135
|
click.echo()
|
|
116
136
|
click.secho(" ✅ All SPLENT-owned files are already up to date.", fg="green")
|
|
117
137
|
click.echo()
|
|
@@ -138,6 +158,8 @@ def product_sync_template(
|
|
|
138
158
|
# Atomic: an interrupted run never leaves a half-written script.
|
|
139
159
|
atomic_write(abs_path, expected)
|
|
140
160
|
click.secho(f" ✔ {rel_path}", fg="green")
|
|
161
|
+
if not filter_active:
|
|
162
|
+
_record_cli_version(product_path)
|
|
141
163
|
|
|
142
164
|
click.echo()
|
|
143
165
|
click.secho(" Done.", fg="cyan")
|
|
@@ -110,9 +110,17 @@ def _run_sat_check(workspace, app_name, data, feature_list, print_config):
|
|
|
110
110
|
|
|
111
111
|
def _run_compat_check(workspace, product_dir):
|
|
112
112
|
"""Run contract compatibility check. Returns (findings, errors, warnings)."""
|
|
113
|
-
from splent_cli.commands.feature.feature_compat import
|
|
113
|
+
from splent_cli.commands.feature.feature_compat import (
|
|
114
|
+
env_scope_findings,
|
|
115
|
+
naming_findings,
|
|
116
|
+
network_findings,
|
|
117
|
+
run_all_product_check,
|
|
118
|
+
)
|
|
114
119
|
|
|
115
120
|
findings = run_all_product_check(workspace, product_dir)
|
|
121
|
+
findings += env_scope_findings(workspace, product_dir)
|
|
122
|
+
findings += network_findings(workspace, product_dir)
|
|
123
|
+
findings += naming_findings(workspace, product_dir)
|
|
116
124
|
errors = [f for f in findings if f["severity"] == "error"]
|
|
117
125
|
warnings = [f for f in findings if f["severity"] == "warning"]
|
|
118
126
|
return findings, errors, warnings
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Running a command inside the product's web container.
|
|
2
|
+
|
|
3
|
+
The product's app imports every feature, and therefore every third-party
|
|
4
|
+
package the features declare. Those are installed in the product's image,
|
|
5
|
+
never in the CLI's, which every product on the machine shares. A command
|
|
6
|
+
that builds the app in the CLI container died on the first such import
|
|
7
|
+
(identities needs authlib), so db:upgrade, db:seed and the features' own
|
|
8
|
+
commands only worked for products whose features needed nothing beyond the
|
|
9
|
+
framework. The web container is where the app already runs, with all of it.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import shlex
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from splent_cli.utils.proc import run
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_product_container() -> bool:
|
|
20
|
+
"""Whether this process already runs inside the product's web container.
|
|
21
|
+
|
|
22
|
+
SPLENT_CONTAINER is the explicit answer when set (the CLI container says
|
|
23
|
+
"cli", product images say "product"); /.dockerenv covers containers older
|
|
24
|
+
than the variable.
|
|
25
|
+
"""
|
|
26
|
+
container = os.getenv("SPLENT_CONTAINER")
|
|
27
|
+
if container:
|
|
28
|
+
return container != "cli"
|
|
29
|
+
return os.path.exists("/.dockerenv")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def name(product: str) -> str:
|
|
33
|
+
return f"{product}_web"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def running(container: str) -> bool:
|
|
37
|
+
result = run(
|
|
38
|
+
["docker", "ps", "-q", "-f", f"name=^{container}$"], check=False, capture=True
|
|
39
|
+
)
|
|
40
|
+
return result.returncode == 0 and bool(result.stdout.strip())
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def delegate(product: str, argv: list[str]) -> int:
|
|
44
|
+
"""Run ``splent <argv>`` in the product's web container; its exit code.
|
|
45
|
+
|
|
46
|
+
With a terminal the exec gets one too, so prompts, colours and Ctrl-C
|
|
47
|
+
behave as they do here.
|
|
48
|
+
"""
|
|
49
|
+
command = " ".join(shlex.quote(a) for a in ["splent", *argv])
|
|
50
|
+
tty = ["-t"] if sys.stdin.isatty() and sys.stdout.isatty() else []
|
|
51
|
+
result = run(
|
|
52
|
+
[
|
|
53
|
+
"docker",
|
|
54
|
+
"exec",
|
|
55
|
+
"-i",
|
|
56
|
+
*tty,
|
|
57
|
+
"-e",
|
|
58
|
+
f"SPLENT_APP={product}",
|
|
59
|
+
name(product),
|
|
60
|
+
"bash",
|
|
61
|
+
"-lc",
|
|
62
|
+
command,
|
|
63
|
+
],
|
|
64
|
+
check=False,
|
|
65
|
+
)
|
|
66
|
+
return result.returncode
|
{splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_ci_validate.yml.j2
RENAMED
|
@@ -48,3 +48,18 @@ jobs:
|
|
|
48
48
|
|
|
49
49
|
- name: Generate the deploy artifacts
|
|
50
50
|
run: splent product:build --no-image
|
|
51
|
+
|
|
52
|
+
# What the build writes is versioned, so a host deploys from a clone.
|
|
53
|
+
# A change to the features or the configuration that skipped
|
|
54
|
+
# product:build left them behind, and the CI said nothing.
|
|
55
|
+
- name: The versioned deploy artifacts are the ones the product generates
|
|
56
|
+
working-directory: {{ product_name }}
|
|
57
|
+
run: |
|
|
58
|
+
paths="deploy docker/requirements.lock splent.contracts.json docker/spl"
|
|
59
|
+
if [ -n "$(git status --porcelain -- $paths)" ]; then
|
|
60
|
+
git status --short -- $paths
|
|
61
|
+
git --no-pager diff --stat -- $paths
|
|
62
|
+
echo "These differ from what splent product:build generates."
|
|
63
|
+
echo "Run splent product:build and commit them."
|
|
64
|
+
exit 1
|
|
65
|
+
fi
|
|
@@ -58,6 +58,40 @@ def make_feature_readonly(path: str) -> None:
|
|
|
58
58
|
_chmod(os.path.join(root, name), lambda m: m & ~_WRITE_BITS)
|
|
59
59
|
|
|
60
60
|
|
|
61
|
+
def restore_snapshot(path: str) -> list[str]:
|
|
62
|
+
"""Put back what in a cached feature differs from its tag; the paths restored.
|
|
63
|
+
|
|
64
|
+
A snapshot is its tag, read-only, but files can still drift: a CLI that
|
|
65
|
+
copied snapshots without their modes left nginx 1.2.0's entrypoint script
|
|
66
|
+
without its exec bit, and nginx restarted in a loop while every check
|
|
67
|
+
said the release was in place. Untracked files (the docker/.env a product
|
|
68
|
+
generates) are left alone. Only the restored files are made read-only
|
|
69
|
+
again, so that .env stays writable for product:env.
|
|
70
|
+
"""
|
|
71
|
+
import subprocess
|
|
72
|
+
|
|
73
|
+
if not os.path.isdir(os.path.join(path, ".git")):
|
|
74
|
+
return []
|
|
75
|
+
status = subprocess.run(
|
|
76
|
+
["git", "-C", path, "status", "--porcelain", "--untracked-files=no"],
|
|
77
|
+
capture_output=True,
|
|
78
|
+
text=True,
|
|
79
|
+
)
|
|
80
|
+
if status.returncode != 0:
|
|
81
|
+
return []
|
|
82
|
+
changed = [line[3:] for line in status.stdout.splitlines() if line.strip()]
|
|
83
|
+
if not changed:
|
|
84
|
+
return []
|
|
85
|
+
subprocess.run(
|
|
86
|
+
["git", "-C", path, "checkout", "--", *changed],
|
|
87
|
+
capture_output=True,
|
|
88
|
+
text=True,
|
|
89
|
+
)
|
|
90
|
+
for rel in changed:
|
|
91
|
+
_chmod(os.path.join(path, rel), lambda m: m & ~_WRITE_BITS)
|
|
92
|
+
return changed
|
|
93
|
+
|
|
94
|
+
|
|
61
95
|
def make_feature_writable(path: str) -> None:
|
|
62
96
|
"""Restore write permissions across a cached feature so it can be edited
|
|
63
97
|
or deleted.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""A feature is named for what it does, never for the product that uses it.
|
|
2
|
+
|
|
3
|
+
auth_uvlhub, signup_uvlhub, profile_planner... each holds what one product
|
|
4
|
+
wanted from a base feature: a slot, a setting, its own markup, its texts.
|
|
5
|
+
No other product can choose it, and while it exists the base never grows the
|
|
6
|
+
extension point every product needed. What a product alone wants belongs in
|
|
7
|
+
the product (its templates, seeders and configuration); what several want
|
|
8
|
+
belongs in the base, as a slot, a registry or a setting.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from splent_cli.utils.template_drift import not_a_product_reason
|
|
14
|
+
|
|
15
|
+
# Words a product's name carries that say nothing about which product it is.
|
|
16
|
+
GENERIC_WORDS = {"app", "splent", "product", "site", "web"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def product_brands(workspace: str | Path) -> dict[str, str]:
|
|
20
|
+
"""``{brand: product}`` for the products at the workspace root.
|
|
21
|
+
|
|
22
|
+
The brand is the product's name without its generic words: uvlhub for
|
|
23
|
+
uvlhub_splent, innosoft for innosoft_app, egc for egc_wiki.
|
|
24
|
+
"""
|
|
25
|
+
brands: dict[str, str] = {}
|
|
26
|
+
root = Path(workspace)
|
|
27
|
+
if not root.is_dir():
|
|
28
|
+
return brands
|
|
29
|
+
for entry in root.iterdir():
|
|
30
|
+
if not entry.is_dir() or entry.name.startswith((".", "splent_")):
|
|
31
|
+
continue
|
|
32
|
+
if not_a_product_reason(entry) is not None:
|
|
33
|
+
continue
|
|
34
|
+
words = [w for w in entry.name.split("_") if w and w not in GENERIC_WORDS]
|
|
35
|
+
if words:
|
|
36
|
+
brands[words[0]] = entry.name
|
|
37
|
+
return brands
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def product_suffix(feature_name: str, workspace: str | Path) -> str | None:
|
|
41
|
+
"""The product a feature's name ends with (``auth_uvlhub``: uvlhub_splent),
|
|
42
|
+
or None. A name that is the brand alone (``marketplace``) is a domain."""
|
|
43
|
+
short = feature_name.removeprefix("splent_feature_")
|
|
44
|
+
head, sep, last = short.rpartition("_")
|
|
45
|
+
if not sep or not head:
|
|
46
|
+
return None
|
|
47
|
+
return product_brands(workspace).get(last)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def product_suffix_message(feature_name: str, product: str) -> str:
|
|
51
|
+
short = feature_name.removeprefix("splent_feature_")
|
|
52
|
+
return (
|
|
53
|
+
f"{short} is named after the product {product}. A feature is named for "
|
|
54
|
+
"what it does, so another product can choose it. What only this "
|
|
55
|
+
"product wants goes in the product (templates, seeders, "
|
|
56
|
+
"[tool.splent.config]); what several want goes in the base feature, "
|
|
57
|
+
"as a slot, a registry or a setting."
|
|
58
|
+
)
|