nextmv 1.7.0.dev0__tar.gz → 1.7.2__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.
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/PKG-INFO +1 -1
- nextmv-1.7.2/nextmv/__about__.py +1 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/main.py +57 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/local/executor.py +22 -14
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/local/runner.py +14 -3
- nextmv-1.7.2/nextmv/local/uv_handler.py +58 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/output.py +10 -41
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_class-assign/requirements.txt +1 -1
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_demand-alloc/allocation/requirements.txt +1 -1
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_demand-alloc/workflow/requirements.txt +1 -1
- {nextmv-1.7.0.dev0/nextmv/templates/python_multi-file_hello-world → nextmv-1.7.2/nextmv/templates/python_json_hello-world}/requirements.txt +1 -1
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_class-assign/requirements.txt +1 -1
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_demand-alloc/allocation/requirements.txt +1 -1
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_demand-alloc/workflow/requirements.txt +1 -1
- {nextmv-1.7.0.dev0/nextmv/templates/python_json_hello-world → nextmv-1.7.2/nextmv/templates/python_multi-file_hello-world}/requirements.txt +1 -1
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/pyproject.toml +0 -1
- nextmv-1.7.2/tests/cli/test_main.py +176 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/local/test_runner.py +34 -2
- nextmv-1.7.2/tests/local/test_uv_handler.py +164 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/test_output.py +16 -57
- nextmv-1.7.0.dev0/nextmv/__about__.py +0 -1
- nextmv-1.7.0.dev0/tests/cli/test_main.py +0 -68
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/.gitignore +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/.python-version +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/CONTRIBUTING.md +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/LICENSE +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/README.md +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/__entrypoint__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/_serialization.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/account.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/base_model.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/acceptance/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/acceptance/create.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/acceptance/delete.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/acceptance/get.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/acceptance/list.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/acceptance/update.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/account/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/account/create.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/account/delete.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/account/get.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/account/update.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/app/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/app/create.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/app/delete.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/app/exists.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/app/get.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/app/list.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/app/push.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/app/update.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/batch/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/batch/create.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/batch/delete.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/batch/get.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/batch/list.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/batch/metadata.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/batch/update.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/data/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/data/upload.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/ensemble/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/ensemble/create.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/ensemble/delete.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/ensemble/get.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/ensemble/list.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/ensemble/update.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/input_set/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/input_set/create.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/input_set/delete.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/input_set/get.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/input_set/list.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/input_set/update.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/instance/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/instance/create.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/instance/delete.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/instance/exists.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/instance/get.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/instance/list.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/instance/update.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/managed_input/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/managed_input/create.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/managed_input/delete.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/managed_input/get.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/managed_input/list.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/managed_input/update.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/marketplace/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/marketplace/app/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/marketplace/app/create.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/marketplace/app/get.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/marketplace/app/list.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/marketplace/app/update.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/marketplace/subscription/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/marketplace/subscription/create.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/marketplace/subscription/delete.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/marketplace/subscription/get.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/marketplace/subscription/list.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/marketplace/version/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/marketplace/version/create.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/marketplace/version/get.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/marketplace/version/list.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/marketplace/version/update.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/run/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/run/cancel.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/run/create.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/run/delete.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/run/get.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/run/input.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/run/list.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/run/logs.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/run/metadata.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/run/track.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/scenario/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/scenario/create.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/scenario/delete.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/scenario/get.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/scenario/list.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/scenario/metadata.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/scenario/update.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/secrets/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/secrets/create.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/secrets/delete.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/secrets/get.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/secrets/list.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/secrets/update.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/shadow/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/shadow/create.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/shadow/delete.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/shadow/get.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/shadow/list.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/shadow/metadata.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/shadow/start.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/shadow/stop.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/shadow/update.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/sso/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/sso/create.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/sso/delete.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/sso/disable.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/sso/domain/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/sso/domain/delete.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/sso/enable.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/sso/get.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/sso/update.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/switchback/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/switchback/create.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/switchback/delete.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/switchback/get.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/switchback/list.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/switchback/metadata.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/switchback/start.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/switchback/stop.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/switchback/update.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/upload/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/upload/create.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/version/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/version/create.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/version/delete.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/version/exists.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/version/get.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/version/list.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/cloud/version/update.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/community/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/community/clone.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/community/list.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/configuration/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/configuration/config.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/configuration/create.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/configuration/delete.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/configuration/list.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/init.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/local/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/local/app/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/local/app/delete.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/local/app/get.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/local/app/list.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/local/app/register.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/local/app/registered.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/local/app/sync.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/local/app/update.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/local/run/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/local/run/create.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/local/run/get.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/local/run/input.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/local/run/list.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/local/run/logs.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/local/run/metadata.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/local/run/visuals.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/manifest/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/manifest/init.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/manifest/validate.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/mcp/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/mcp/serve.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/mcp/server.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/mcp/tools/WORKFLOW.md +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/mcp/tools/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/mcp/tools/_helpers.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/mcp/tools/acceptance.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/mcp/tools/account.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/mcp/tools/app.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/mcp/tools/batch.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/mcp/tools/community.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/mcp/tools/ensemble.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/mcp/tools/guide.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/mcp/tools/input_set.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/mcp/tools/instance.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/mcp/tools/local.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/mcp/tools/managed_input.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/mcp/tools/profile.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/mcp/tools/run.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/mcp/tools/scenario.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/mcp/tools/secrets.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/mcp/tools/shadow.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/mcp/tools/sso.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/mcp/tools/switchback.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/mcp/tools/version.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/message.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/options.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cli/version.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/acceptance_test.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/account.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/application/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/application/_acceptance.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/application/_batch_scenario.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/application/_ensemble.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/application/_input_set.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/application/_instance.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/application/_managed_input.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/application/_run.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/application/_secrets.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/application/_shadow.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/application/_switchback.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/application/_utils.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/application/_version.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/assets.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/batch_experiment.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/client.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/community.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/ensemble.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/input_set.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/instance.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/integration.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/marketplace.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/package.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/scenario.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/secrets.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/shadow.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/sso.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/switchback.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/url.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/cloud/version.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/content_format.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/deprecated.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/input.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/local/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/local/application.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/local/geojson_handler.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/local/local.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/local/plotly_handler.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/local/registry.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/logger.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/manifest.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/model.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/options.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/polling.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/run.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/safe.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/status.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/binary_json_app.yaml +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/binary_multi-file_app.yaml +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/go_json_app.yaml +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/go_json_hello-world/.gitignore +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/go_json_hello-world/README.md +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/go_json_hello-world/app.yaml +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/go_json_hello-world/go.mod +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/go_json_hello-world/input.json +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/go_json_hello-world/main.go +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/go_multi-file_app.yaml +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/go_multi-file_hello-world/.gitignore +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/go_multi-file_hello-world/README.md +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/go_multi-file_hello-world/app.yaml +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/go_multi-file_hello-world/go.mod +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/go_multi-file_hello-world/inputs/input.json +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/go_multi-file_hello-world/main.go +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/java_json_app.yaml +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/java_json_hello-world/.gitignore +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/java_json_hello-world/Main.java +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/java_json_hello-world/README.md +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/java_json_hello-world/app.yaml +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/java_json_hello-world/input.json +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/java_json_hello-world/pom.xml +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/java_multi-file_app.yaml +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/java_multi-file_hello-world/.gitignore +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/java_multi-file_hello-world/Main.java +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/java_multi-file_hello-world/README.md +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/java_multi-file_hello-world/app.yaml +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/java_multi-file_hello-world/inputs/input.json +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/java_multi-file_hello-world/pom.xml +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_app.yaml +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_class-assign/.gitignore +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_class-assign/README.md +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_class-assign/app.yaml +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_class-assign/input.json +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_class-assign/main.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_class-assign/visualizations.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_demand-alloc/.gitignore +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_demand-alloc/README.md +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_demand-alloc/allocation/README.md +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_demand-alloc/allocation/app.yaml +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_demand-alloc/allocation/input.json +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_demand-alloc/allocation/main.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_demand-alloc/allocation/visuals.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_demand-alloc/workflow/README.md +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_demand-alloc/workflow/app.yaml +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_demand-alloc/workflow/input.json +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_demand-alloc/workflow/main.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_demand-alloc/workflow/visuals.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_hello-world/.gitignore +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_hello-world/README.md +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_hello-world/app.yaml +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_hello-world/input.json +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_json_hello-world/main.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_app.yaml +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_class-assign/.gitignore +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_class-assign/README.md +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_class-assign/app.yaml +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_class-assign/inputs/input.json +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_class-assign/main.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_class-assign/visualizations.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_demand-alloc/README.md +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_demand-alloc/allocation/README.md +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_demand-alloc/allocation/app.yaml +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_demand-alloc/allocation/input.json +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_demand-alloc/allocation/main.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_demand-alloc/allocation/visuals.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_demand-alloc/workflow/.gitignore +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_demand-alloc/workflow/README.md +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_demand-alloc/workflow/app.yaml +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_demand-alloc/workflow/inputs/input.json +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_demand-alloc/workflow/main.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_demand-alloc/workflow/visuals.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_hello-world/.gitignore +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_hello-world/README.md +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_hello-world/app.yaml +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_hello-world/inputs/input.json +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/nextmv/templates/python_multi-file_hello-world/main.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/cli/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/cli/test_configuration.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/cli/test_mcp.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/cli/test_version.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/cloud/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/cloud/app.yaml +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/cloud/test_client.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/cloud/test_instance.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/cloud/test_package.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/cloud/test_scenario.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/integration/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/integration/cloud/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/integration/cloud/test_integration_cloud.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/integration/cloud/test_integration_marketplace.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/local/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/local/test_application.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/local/test_executor.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/local/test_registry.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/scripts/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/scripts/options1.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/scripts/options2.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/scripts/options3.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/scripts/options4.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/scripts/options5.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/scripts/options6.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/scripts/options7.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/test_base_model.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/test_entrypoint/__init__.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/test_entrypoint/test_entrypoint.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/test_input.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/test_inputs/test_data.csv +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/test_inputs/test_data.json +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/test_inputs/test_data.txt +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/test_logger.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/test_manifest.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/test_model.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/test_options.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/test_polling.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/test_run.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/test_safe.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/test_serialization.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/tests/test_version.py +0 -0
- {nextmv-1.7.0.dev0 → nextmv-1.7.2}/uv.lock +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "v1.7.2"
|
|
@@ -13,7 +13,9 @@ about the features used here. An example of Rich markup can be found in the
|
|
|
13
13
|
epilog of the Typer application defined below.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
+
import io
|
|
16
17
|
import os
|
|
18
|
+
import runpy
|
|
17
19
|
import sys
|
|
18
20
|
from typing import Annotated
|
|
19
21
|
|
|
@@ -32,6 +34,7 @@ from nextmv.cli.message import confirmation, error, info, success, warning
|
|
|
32
34
|
from nextmv.cli.version import app as version_app
|
|
33
35
|
from nextmv.cli.version import version_callback
|
|
34
36
|
from nextmv.cloud.client import retrieve_endpoint_from_config, retrieve_key_from_config
|
|
37
|
+
from nextmv.local.uv_handler import _find_uv_binary
|
|
35
38
|
|
|
36
39
|
# Disable dim text for the extended help of commands.
|
|
37
40
|
rich_utils.STYLE_HELPTEXT = ""
|
|
@@ -261,6 +264,38 @@ def _remove_go_cli() -> None:
|
|
|
261
264
|
success(f"Deleted [italic red]deprecated[/italic red] [magenta]{GO_CLI_PATH}[/magenta].")
|
|
262
265
|
|
|
263
266
|
|
|
267
|
+
def setup_encoding() -> None:
|
|
268
|
+
"""
|
|
269
|
+
Configure UTF-8 encoding for Windows platforms to make sure emojis and rich text do
|
|
270
|
+
not cause encoding errors.
|
|
271
|
+
"""
|
|
272
|
+
# Only force UTF-8 encoding on Windows where the default encoding is not already UTF-8
|
|
273
|
+
# and where sys.stdout has a buffer attribute (indicating it's a real stream and not a
|
|
274
|
+
# mock).
|
|
275
|
+
if (
|
|
276
|
+
sys.platform == "win32"
|
|
277
|
+
and getattr(sys.stdout, "encoding", "").lower() != "utf-8"
|
|
278
|
+
and hasattr(sys.stdout, "buffer")
|
|
279
|
+
):
|
|
280
|
+
try:
|
|
281
|
+
sys.stdout = io.TextIOWrapper(
|
|
282
|
+
sys.stdout.buffer,
|
|
283
|
+
encoding="utf-8",
|
|
284
|
+
errors="replace", # Don't crash on bad chars
|
|
285
|
+
line_buffering=True,
|
|
286
|
+
)
|
|
287
|
+
sys.stderr = io.TextIOWrapper(
|
|
288
|
+
sys.stderr.buffer,
|
|
289
|
+
encoding="utf-8",
|
|
290
|
+
errors="replace",
|
|
291
|
+
line_buffering=True,
|
|
292
|
+
)
|
|
293
|
+
except Exception:
|
|
294
|
+
# If wrapping fails (e.g. in some CI environments),
|
|
295
|
+
# fall back to the original stream rather than crashing.
|
|
296
|
+
pass
|
|
297
|
+
|
|
298
|
+
|
|
264
299
|
def main() -> None:
|
|
265
300
|
"""
|
|
266
301
|
Entry point for the CLI with global exception handling.
|
|
@@ -269,6 +304,28 @@ def main() -> None:
|
|
|
269
304
|
own exit codes) and displays a clean error message instead of a traceback.
|
|
270
305
|
"""
|
|
271
306
|
|
|
307
|
+
# Improve compatibility with Windows terminals.
|
|
308
|
+
setup_encoding()
|
|
309
|
+
|
|
310
|
+
# Handle --run-script and --run-uv for running scripts with the bundled Python
|
|
311
|
+
# interpreter or via uv. These are used internally in case of frozen PyInstaller
|
|
312
|
+
# distributions.
|
|
313
|
+
if len(sys.argv) > 1 and sys.argv[1] == "--run-script":
|
|
314
|
+
if len(sys.argv) < 3:
|
|
315
|
+
rich.print("[red]Error:[/red] --run-script requires a script path.", file=sys.stderr)
|
|
316
|
+
sys.exit(1)
|
|
317
|
+
script_path = sys.argv[2]
|
|
318
|
+
sys.argv = sys.argv[2:] # script becomes argv[0]; its own args follow
|
|
319
|
+
runpy.run_path(script_path, run_name="__main__")
|
|
320
|
+
sys.exit(0)
|
|
321
|
+
elif len(sys.argv) > 1 and sys.argv[1] == "--run-uv":
|
|
322
|
+
if len(sys.argv) < 3:
|
|
323
|
+
rich.print("[red]Error:[/red] --run-uv requires arguments for 'uv run'.", file=sys.stderr)
|
|
324
|
+
sys.exit(1)
|
|
325
|
+
uv_bin = _find_uv_binary()
|
|
326
|
+
uv_args = [uv_bin, "run"] + sys.argv[2:]
|
|
327
|
+
os.execv(uv_bin, uv_args)
|
|
328
|
+
|
|
272
329
|
try:
|
|
273
330
|
app()
|
|
274
331
|
except (typer.Exit, typer.Abort, SystemExit):
|
|
@@ -418,11 +418,6 @@ def process_run_output(
|
|
|
418
418
|
temp_run_outputs_dir = os.path.join(temp_src, OUTPUTS_KEY)
|
|
419
419
|
|
|
420
420
|
output_format = manifest.configuration.content.format
|
|
421
|
-
_process_run_information(
|
|
422
|
-
run_id=run_id,
|
|
423
|
-
run_dir=run_dir,
|
|
424
|
-
result=result,
|
|
425
|
-
)
|
|
426
421
|
_process_run_metrics(
|
|
427
422
|
temp_run_outputs_dir=temp_run_outputs_dir,
|
|
428
423
|
outputs_dir=outputs_dir,
|
|
@@ -459,6 +454,16 @@ def process_run_output(
|
|
|
459
454
|
run_dir=run_dir,
|
|
460
455
|
outputs_dir=outputs_dir,
|
|
461
456
|
)
|
|
457
|
+
# NOTE: _process_run_information must be called last. It sets status_v2 to "succeeded"
|
|
458
|
+
# or "failed", which signals to pollers that the run is complete. format.output
|
|
459
|
+
# (written by _process_run_solutions) must already be on disk before that status
|
|
460
|
+
# transition is visible, otherwise a poller can observe status=succeeded with
|
|
461
|
+
# format_output=None and crash.
|
|
462
|
+
_process_run_information(
|
|
463
|
+
run_id=run_id,
|
|
464
|
+
run_dir=run_dir,
|
|
465
|
+
result=result,
|
|
466
|
+
)
|
|
462
467
|
|
|
463
468
|
|
|
464
469
|
def _process_run_information(run_id: str, run_dir: str, result: subprocess.CompletedProcess[str]) -> None:
|
|
@@ -1073,6 +1078,12 @@ def __determine_command(manifest: Manifest) -> list[str]:
|
|
|
1073
1078
|
"""
|
|
1074
1079
|
Returns the command to execute based on the application type.
|
|
1075
1080
|
|
|
1081
|
+
When running Python apps as a frozen PyInstaller single binary, ``sys.executable``
|
|
1082
|
+
points to the binary itself rather than a Python interpreter, so the normal
|
|
1083
|
+
``[sys.executable, "-m", "uv", "run", ...]`` invocation cannot work. Instead, we use a
|
|
1084
|
+
special ``[sys.executable, "--run-uv", ...]`` invocation that signals to our own
|
|
1085
|
+
binary to re-execute and pass to the bundled uv binary.
|
|
1086
|
+
|
|
1076
1087
|
Parameters
|
|
1077
1088
|
----------
|
|
1078
1089
|
manifest : Manifest
|
|
@@ -1084,26 +1095,23 @@ def __determine_command(manifest: Manifest) -> list[str]:
|
|
|
1084
1095
|
The command prefix to use for execution. Empty list for binary executables.
|
|
1085
1096
|
"""
|
|
1086
1097
|
if manifest.type == ManifestType.PYTHON:
|
|
1098
|
+
is_frozen = getattr(sys, "frozen", False)
|
|
1099
|
+
entry_point = [sys.executable, "-m", "uv", "run"] if not is_frozen else [sys.executable, "--run-uv"]
|
|
1100
|
+
|
|
1087
1101
|
if manifest.python and manifest.python.pip_requirements:
|
|
1088
1102
|
if isinstance(manifest.python.pip_requirements, list):
|
|
1089
1103
|
return [
|
|
1090
|
-
|
|
1091
|
-
"-m",
|
|
1092
|
-
"uv",
|
|
1093
|
-
"run",
|
|
1104
|
+
*entry_point,
|
|
1094
1105
|
"--with",
|
|
1095
1106
|
",".join(manifest.python.pip_requirements),
|
|
1096
1107
|
]
|
|
1097
1108
|
elif isinstance(manifest.python.pip_requirements, str):
|
|
1098
1109
|
return [
|
|
1099
|
-
|
|
1100
|
-
"-m",
|
|
1101
|
-
"uv",
|
|
1102
|
-
"run",
|
|
1110
|
+
*entry_point,
|
|
1103
1111
|
"--with-requirements",
|
|
1104
1112
|
manifest.python.pip_requirements,
|
|
1105
1113
|
]
|
|
1106
|
-
return [
|
|
1114
|
+
return [*entry_point]
|
|
1107
1115
|
elif manifest.type == ManifestType.GO:
|
|
1108
1116
|
return []
|
|
1109
1117
|
elif manifest.type == ManifestType.BINARY:
|
|
@@ -122,7 +122,18 @@ def run(
|
|
|
122
122
|
"options": options,
|
|
123
123
|
}
|
|
124
124
|
)
|
|
125
|
-
|
|
125
|
+
# When frozen as a single binary (PyInstaller), sys.executable points to
|
|
126
|
+
# the binary itself rather than a Python interpreter, so we use the
|
|
127
|
+
# --run-script mechanism to run executor.py with the bundled interpreter.
|
|
128
|
+
# In a normal Python package install, sys.executable is a real interpreter
|
|
129
|
+
# and executor.py can be launched directly.
|
|
130
|
+
# We always use the absolute path to executor.py so it can be resolved
|
|
131
|
+
# regardless of the working directory the binary was invoked from.
|
|
132
|
+
executor_path = os.path.join(os.path.dirname(__file__), "executor.py")
|
|
133
|
+
if getattr(sys, "frozen", False):
|
|
134
|
+
args = [sys.executable, "--run-script", executor_path]
|
|
135
|
+
else:
|
|
136
|
+
args = [sys.executable, executor_path]
|
|
126
137
|
process = subprocess.Popen(
|
|
127
138
|
args,
|
|
128
139
|
env=os.environ,
|
|
@@ -130,8 +141,8 @@ def run(
|
|
|
130
141
|
stdin=subprocess.PIPE,
|
|
131
142
|
stdout=subprocess.DEVNULL,
|
|
132
143
|
stderr=subprocess.DEVNULL,
|
|
133
|
-
cwd=os.path.dirname(
|
|
134
|
-
start_new_session=True,
|
|
144
|
+
cwd=os.path.dirname(executor_path),
|
|
145
|
+
start_new_session=True,
|
|
135
146
|
)
|
|
136
147
|
process.stdin.write(stdin_input)
|
|
137
148
|
process.stdin.close()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import platform
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _find_uv_binary() -> str:
|
|
7
|
+
"""
|
|
8
|
+
Locate the uv binary at runtime.
|
|
9
|
+
|
|
10
|
+
Resolves the binary in this order:
|
|
11
|
+
1. If running in a PyInstaller bundle, the uv binary is expected to be bundled under
|
|
12
|
+
`sys._MEIPASS/uv_bin/`.
|
|
13
|
+
2. Otherwise, check whether uv module is installed and use its helper function to find
|
|
14
|
+
the binary.
|
|
15
|
+
3. Finally, check if uv is available on the system PATH.
|
|
16
|
+
|
|
17
|
+
Returns
|
|
18
|
+
-------
|
|
19
|
+
str
|
|
20
|
+
Absolute path to the uv binary.
|
|
21
|
+
|
|
22
|
+
Raises
|
|
23
|
+
------
|
|
24
|
+
FileNotFoundError
|
|
25
|
+
If the uv binary cannot be located.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
# 1. Check for PyInstaller bundled uv binary.
|
|
29
|
+
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
|
|
30
|
+
# In case of single binary PyInstaller distribution, the uv binary is bundled
|
|
31
|
+
# under sys._MEIPASS/uv_bin/.
|
|
32
|
+
exe_suffix = ".exe" if platform.system() == "Windows" else ""
|
|
33
|
+
uv_path = os.path.join(sys._MEIPASS, "uv_bin", f"uv{exe_suffix}")
|
|
34
|
+
if not os.path.isfile(uv_path):
|
|
35
|
+
raise FileNotFoundError(
|
|
36
|
+
f"Bundled uv binary not found at {uv_path}, please ensure proper installation of the Nextmv CLI."
|
|
37
|
+
)
|
|
38
|
+
return uv_path
|
|
39
|
+
|
|
40
|
+
# 2. Fall back to finding uv via the installed module (if available).
|
|
41
|
+
try:
|
|
42
|
+
from uv import find_uv_bin # type: ignore[import]
|
|
43
|
+
|
|
44
|
+
return find_uv_bin()
|
|
45
|
+
except (ImportError, AttributeError):
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
# 3. Finally, check if uv is available on the system PATH.
|
|
49
|
+
from shutil import which
|
|
50
|
+
|
|
51
|
+
uv_path = which("uv")
|
|
52
|
+
if uv_path:
|
|
53
|
+
return uv_path
|
|
54
|
+
|
|
55
|
+
raise FileNotFoundError(
|
|
56
|
+
"uv binary not found. Please ensure that uv is installed and available "
|
|
57
|
+
+ "on your system (via PATH or as module in Python)."
|
|
58
|
+
)
|
|
@@ -992,11 +992,10 @@ class Output:
|
|
|
992
992
|
}
|
|
993
993
|
```
|
|
994
994
|
"""
|
|
995
|
-
output_format: ContentFormat | None =
|
|
995
|
+
output_format: ContentFormat | None = None
|
|
996
996
|
"""
|
|
997
|
-
Format of the output data.
|
|
998
|
-
`
|
|
999
|
-
cannot be `None`.
|
|
997
|
+
Format of the output data. When set to `ContentFormat.MULTI_FILE`, the
|
|
998
|
+
`solution_files` field must be specified and cannot be `None`.
|
|
1000
999
|
"""
|
|
1001
1000
|
solution: dict[str, Any] | Any | dict[str, list[dict[str, Any]]] | None = None
|
|
1002
1001
|
"""
|
|
@@ -1066,7 +1065,7 @@ class Output:
|
|
|
1066
1065
|
handle the serialization of the data.
|
|
1067
1066
|
"""
|
|
1068
1067
|
|
|
1069
|
-
def __post_init__(self):
|
|
1068
|
+
def __post_init__(self): # noqa: C901
|
|
1070
1069
|
"""
|
|
1071
1070
|
Initialize and validate the Output instance.
|
|
1072
1071
|
|
|
@@ -1085,7 +1084,7 @@ class Output:
|
|
|
1085
1084
|
new_options = copy.deepcopy(init_options)
|
|
1086
1085
|
self.options = new_options
|
|
1087
1086
|
|
|
1088
|
-
if type(self.output_format) is OutputFormat:
|
|
1087
|
+
if self.output_format is not None and type(self.output_format) is OutputFormat:
|
|
1089
1088
|
deprecated(name="OutputFormat", reason="`OutputFormat` is deprecated, use `ContentFormat` instead")
|
|
1090
1089
|
if self.output_format in {OutputFormat.TEXT, OutputFormat.CSV_ARCHIVE}:
|
|
1091
1090
|
deprecated(
|
|
@@ -1097,37 +1096,6 @@ class Output:
|
|
|
1097
1096
|
else:
|
|
1098
1097
|
raise ValueError(f"unsupported output_format: {self.output_format}")
|
|
1099
1098
|
|
|
1100
|
-
if self.solution is not None:
|
|
1101
|
-
if self.output_format == ContentFormat.JSON:
|
|
1102
|
-
try:
|
|
1103
|
-
_ = serialize_json(self.solution)
|
|
1104
|
-
except (TypeError, OverflowError) as e:
|
|
1105
|
-
raise ValueError(
|
|
1106
|
-
f"Output has `output_format` `ContentFormat.JSON` and "
|
|
1107
|
-
f"`Output.solution` is of type {type(self.solution)}, which is not JSON serializable"
|
|
1108
|
-
) from e
|
|
1109
|
-
|
|
1110
|
-
elif (
|
|
1111
|
-
type(self.output_format) is OutputFormat
|
|
1112
|
-
and self.output_format == OutputFormat.CSV_ARCHIVE
|
|
1113
|
-
and not isinstance(self.solution, dict)
|
|
1114
|
-
):
|
|
1115
|
-
raise ValueError(
|
|
1116
|
-
f"unsupported Output.solution type: {type(self.solution)} with "
|
|
1117
|
-
"output_format OutputFormat.CSV_ARCHIVE, supported type is `dict`"
|
|
1118
|
-
)
|
|
1119
|
-
|
|
1120
|
-
if self.solution_files is not None and self.output_format != ContentFormat.MULTI_FILE:
|
|
1121
|
-
raise ValueError(
|
|
1122
|
-
f"`solution_files` are not `None`, but `output_format` is different from `ContentFormat.MULTI_FILE`: "
|
|
1123
|
-
f"{self.output_format}. If you want to use `solution_files`, set `output_format` "
|
|
1124
|
-
"to `ContentFormat.MULTI_FILE`."
|
|
1125
|
-
)
|
|
1126
|
-
elif self.solution_files is not None and not isinstance(self.solution_files, list):
|
|
1127
|
-
raise TypeError(
|
|
1128
|
-
f"unsupported `Output.solution_files` type: {type(self.solution_files)}, supported type is `list`"
|
|
1129
|
-
)
|
|
1130
|
-
|
|
1131
1099
|
def to_dict(self) -> dict[str, Any]: # noqa: C901
|
|
1132
1100
|
"""
|
|
1133
1101
|
Convert the `Output` object to a dictionary.
|
|
@@ -1204,14 +1172,15 @@ class Output:
|
|
|
1204
1172
|
# Add the auxiliary configurations to the output dictionary if they are
|
|
1205
1173
|
# defined and not empty.
|
|
1206
1174
|
if (
|
|
1207
|
-
self.output_format
|
|
1175
|
+
self.output_format is not None
|
|
1176
|
+
and self.output_format == OutputFormat.CSV_ARCHIVE
|
|
1208
1177
|
and self.csv_configurations is not None
|
|
1209
1178
|
and self.csv_configurations != {}
|
|
1210
1179
|
):
|
|
1211
1180
|
output_dict["csv_configurations"] = self.csv_configurations
|
|
1212
1181
|
|
|
1213
1182
|
if (
|
|
1214
|
-
self.output_format == ContentFormat.JSON
|
|
1183
|
+
(self.output_format is None or self.output_format == ContentFormat.JSON)
|
|
1215
1184
|
and self.json_configurations is not None
|
|
1216
1185
|
and self.json_configurations != {}
|
|
1217
1186
|
):
|
|
@@ -1505,9 +1474,9 @@ class LocalOutputWriter(OutputWriter):
|
|
|
1505
1474
|
elif manifest is not None:
|
|
1506
1475
|
resolved_content_format = manifest.configuration.content.format
|
|
1507
1476
|
elif output is not None:
|
|
1508
|
-
if isinstance(output, Output):
|
|
1477
|
+
if isinstance(output, Output) and output.output_format is not None:
|
|
1509
1478
|
resolved_content_format = output.output_format
|
|
1510
|
-
elif isinstance(output,
|
|
1479
|
+
elif isinstance(output, (Output, dict, BaseModel)):
|
|
1511
1480
|
resolved_content_format = ContentFormat.JSON
|
|
1512
1481
|
else:
|
|
1513
1482
|
raise ValueError(
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
nextmv>=1.
|
|
1
|
+
nextmv>=1.7.0
|
|
2
2
|
plotly>=6.5.2
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
nextmv>=1.
|
|
1
|
+
nextmv>=1.7.0
|
|
2
2
|
plotly>=6.5.2
|
|
@@ -55,7 +55,6 @@ Homepage = "https://www.nextmv.io"
|
|
|
55
55
|
Documentation = "https://nextmv-py.docs.nextmv.io/en/latest/nextmv/"
|
|
56
56
|
Repository = "https://github.com/nextmv-io/nextmv-py"
|
|
57
57
|
|
|
58
|
-
# TODO: Uncomment when CLI is ready for general rollout.
|
|
59
58
|
[project.scripts]
|
|
60
59
|
nextmv = "nextmv.cli.main:main"
|
|
61
60
|
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for the nextmv CLI main module.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
import unittest
|
|
9
|
+
from unittest.mock import patch
|
|
10
|
+
|
|
11
|
+
from nextmv.cli.main import app, main
|
|
12
|
+
from typer.testing import CliRunner
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestCallback(unittest.TestCase):
|
|
16
|
+
"""Tests for the CLI callback behavior."""
|
|
17
|
+
|
|
18
|
+
def setUp(self):
|
|
19
|
+
self.runner = CliRunner()
|
|
20
|
+
self.app = app
|
|
21
|
+
|
|
22
|
+
@patch("nextmv.cli.main._go_cli_exists")
|
|
23
|
+
@patch("nextmv.cli.main.load_config")
|
|
24
|
+
def test_callback_skips_config_check_for_configure(self, mock_load_config, mock_go_cli_exists):
|
|
25
|
+
"""Test that the callback skips config check when running configuration."""
|
|
26
|
+
mock_load_config.return_value = {}
|
|
27
|
+
mock_go_cli_exists.return_value = False
|
|
28
|
+
|
|
29
|
+
# Running configuration should not trigger the error even with empty config
|
|
30
|
+
result = self.runner.invoke(self.app, ["configuration", "--help"])
|
|
31
|
+
self.assertEqual(result.exit_code, 0)
|
|
32
|
+
self.assertIn("Configure the CLI", result.output)
|
|
33
|
+
|
|
34
|
+
@patch("nextmv.cli.main._go_cli_exists")
|
|
35
|
+
@patch("nextmv.cli.main.load_config")
|
|
36
|
+
def test_callback_shows_error_when_no_config(self, mock_load_config, mock_go_cli_exists):
|
|
37
|
+
"""Test that the callback shows error when no config exists for other commands."""
|
|
38
|
+
mock_load_config.return_value = {}
|
|
39
|
+
mock_go_cli_exists.return_value = False
|
|
40
|
+
|
|
41
|
+
result = self.runner.invoke(self.app, ["version"])
|
|
42
|
+
self.assertEqual(result.exit_code, 0)
|
|
43
|
+
|
|
44
|
+
@patch("nextmv.cli.main._go_cli_exists")
|
|
45
|
+
@patch("nextmv.cli.main.load_config")
|
|
46
|
+
def test_callback_allows_command_when_config_exists(self, mock_load_config, mock_go_cli_exists):
|
|
47
|
+
"""Test that the callback allows commands when config exists."""
|
|
48
|
+
mock_load_config.return_value = {"api_key": "test_key"}
|
|
49
|
+
mock_go_cli_exists.return_value = False
|
|
50
|
+
|
|
51
|
+
result = self.runner.invoke(self.app, ["version"])
|
|
52
|
+
self.assertEqual(result.exit_code, 0)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TestHandleGoCli(unittest.TestCase):
|
|
56
|
+
"""Tests for the Go CLI handling behavior."""
|
|
57
|
+
|
|
58
|
+
def setUp(self):
|
|
59
|
+
self.runner = CliRunner()
|
|
60
|
+
self.app = app
|
|
61
|
+
|
|
62
|
+
@patch("nextmv.cli.main._go_cli_exists")
|
|
63
|
+
@patch("nextmv.cli.main.load_config")
|
|
64
|
+
def test_no_prompt_when_go_cli_not_exists(self, mock_load_config, mock_go_cli_exists):
|
|
65
|
+
"""Test that no prompt is shown when Go CLI does not exist."""
|
|
66
|
+
mock_go_cli_exists.return_value = False
|
|
67
|
+
mock_load_config.return_value = {"api_key": "test_key"}
|
|
68
|
+
|
|
69
|
+
result = self.runner.invoke(self.app, ["version"])
|
|
70
|
+
self.assertEqual(result.exit_code, 0)
|
|
71
|
+
self.assertNotIn("deprecated", result.output)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TestRunScript(unittest.TestCase):
|
|
75
|
+
"""Tests for the --run-script hidden flag."""
|
|
76
|
+
|
|
77
|
+
def _write_script(self, content: str) -> str:
|
|
78
|
+
"""Write content to a temporary script file and return its path."""
|
|
79
|
+
fd, path = tempfile.mkstemp(suffix=".py")
|
|
80
|
+
with os.fdopen(fd, "w") as f:
|
|
81
|
+
f.write(content)
|
|
82
|
+
return path
|
|
83
|
+
|
|
84
|
+
def test_run_script_executes_script(self):
|
|
85
|
+
"""Test that --run-script runs the given script."""
|
|
86
|
+
script = self._write_script("import sys; sys.exit(42)\n")
|
|
87
|
+
try:
|
|
88
|
+
with patch.object(sys, "argv", ["nextmv", "--run-script", script]):
|
|
89
|
+
with self.assertRaises(SystemExit) as cm:
|
|
90
|
+
main()
|
|
91
|
+
self.assertEqual(cm.exception.code, 42)
|
|
92
|
+
finally:
|
|
93
|
+
os.unlink(script)
|
|
94
|
+
|
|
95
|
+
def test_run_script_forwards_trailing_args(self):
|
|
96
|
+
"""Test that trailing arguments are visible to the script as sys.argv."""
|
|
97
|
+
# The script writes its own argv to a temp file so we can inspect it.
|
|
98
|
+
fd, result_file = tempfile.mkstemp(suffix=".txt")
|
|
99
|
+
os.close(fd)
|
|
100
|
+
script = self._write_script(f"import sys\nopen({result_file!r}, 'w').write(','.join(sys.argv))\n")
|
|
101
|
+
try:
|
|
102
|
+
with patch.object(sys, "argv", ["nextmv", "--run-script", script, "arg1", "arg2"]):
|
|
103
|
+
with self.assertRaises(SystemExit) as cm:
|
|
104
|
+
main()
|
|
105
|
+
self.assertEqual(cm.exception.code, 0)
|
|
106
|
+
with open(result_file) as f:
|
|
107
|
+
recorded = f.read().split(",")
|
|
108
|
+
# The script sees itself as argv[0] and its own trailing args after.
|
|
109
|
+
self.assertEqual(recorded[0], script)
|
|
110
|
+
self.assertEqual(recorded[1], "arg1")
|
|
111
|
+
self.assertEqual(recorded[2], "arg2")
|
|
112
|
+
finally:
|
|
113
|
+
os.unlink(script)
|
|
114
|
+
if os.path.exists(result_file):
|
|
115
|
+
os.unlink(result_file)
|
|
116
|
+
|
|
117
|
+
def test_run_script_missing_path_exits_with_error(self):
|
|
118
|
+
"""Test that omitting the script path prints an error and exits 1."""
|
|
119
|
+
with patch.object(sys, "argv", ["nextmv", "--run-script"]):
|
|
120
|
+
with self.assertRaises(SystemExit) as cm:
|
|
121
|
+
main()
|
|
122
|
+
self.assertEqual(cm.exception.code, 1)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class TestRunUv(unittest.TestCase):
|
|
126
|
+
"""Tests for the --run-uv hidden flag."""
|
|
127
|
+
|
|
128
|
+
def test_run_uv_missing_args_exits_with_error(self):
|
|
129
|
+
"""--run-uv with no further arguments prints an error and exits 1."""
|
|
130
|
+
with patch.object(sys, "argv", ["nextmv", "--run-uv"]):
|
|
131
|
+
with self.assertRaises(SystemExit) as cm:
|
|
132
|
+
main()
|
|
133
|
+
self.assertEqual(cm.exception.code, 1)
|
|
134
|
+
|
|
135
|
+
@patch("nextmv.cli.main._find_uv_binary")
|
|
136
|
+
@patch("os.execv")
|
|
137
|
+
def test_run_uv_calls_execv_with_correct_args(self, mock_execv, mock_find_uv):
|
|
138
|
+
"""--run-uv resolves the uv binary and calls os.execv with 'uv run <args>'."""
|
|
139
|
+
mock_find_uv.return_value = "/usr/bin/uv"
|
|
140
|
+
# os.execv normally replaces the process and never returns; raise SystemExit
|
|
141
|
+
# so that main() stops at that point rather than falling through to app().
|
|
142
|
+
mock_execv.side_effect = SystemExit(0)
|
|
143
|
+
|
|
144
|
+
with patch.object(sys, "argv", ["nextmv", "--run-uv", "script.py", "--option", "val"]):
|
|
145
|
+
with self.assertRaises(SystemExit) as cm:
|
|
146
|
+
main()
|
|
147
|
+
self.assertEqual(cm.exception.code, 0)
|
|
148
|
+
|
|
149
|
+
mock_find_uv.assert_called_once()
|
|
150
|
+
mock_execv.assert_called_once_with(
|
|
151
|
+
"/usr/bin/uv",
|
|
152
|
+
["/usr/bin/uv", "run", "script.py", "--option", "val"],
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
@patch("nextmv.cli.main._find_uv_binary")
|
|
156
|
+
@patch("os.execv")
|
|
157
|
+
def test_run_uv_single_extra_arg(self, mock_execv, mock_find_uv):
|
|
158
|
+
"""--run-uv works with a single extra argument."""
|
|
159
|
+
mock_find_uv.return_value = "/opt/uv/uv"
|
|
160
|
+
mock_execv.side_effect = SystemExit(0)
|
|
161
|
+
|
|
162
|
+
with patch.object(sys, "argv", ["nextmv", "--run-uv", "app.py"]):
|
|
163
|
+
with self.assertRaises(SystemExit):
|
|
164
|
+
main()
|
|
165
|
+
|
|
166
|
+
mock_execv.assert_called_once_with(
|
|
167
|
+
"/opt/uv/uv",
|
|
168
|
+
["/opt/uv/uv", "run", "app.py"],
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
@patch("nextmv.cli.main._find_uv_binary", side_effect=FileNotFoundError("uv not found"))
|
|
172
|
+
def test_run_uv_uv_binary_not_found_raises(self, mock_find_uv):
|
|
173
|
+
"""--run-uv propagates FileNotFoundError when the uv binary cannot be located."""
|
|
174
|
+
with patch.object(sys, "argv", ["nextmv", "--run-uv", "script.py"]):
|
|
175
|
+
with self.assertRaises(FileNotFoundError):
|
|
176
|
+
main()
|
|
@@ -10,10 +10,13 @@ import tempfile
|
|
|
10
10
|
import unittest
|
|
11
11
|
from unittest.mock import Mock, patch
|
|
12
12
|
|
|
13
|
+
import nextmv.local.runner as runner_module
|
|
13
14
|
from nextmv.local.local import NEXTMV_DIR, RUNS_KEY
|
|
14
15
|
from nextmv.local.runner import new_run, record_input, run
|
|
15
16
|
from nextmv.manifest import Manifest, ManifestRuntime
|
|
16
17
|
|
|
18
|
+
EXECUTOR_PATH = os.path.join(os.path.dirname(runner_module.__file__), "executor.py")
|
|
19
|
+
|
|
17
20
|
|
|
18
21
|
class TestLocalRunner(unittest.TestCase):
|
|
19
22
|
"""Test cases for the local runner module."""
|
|
@@ -268,8 +271,8 @@ print(json.dumps(output))
|
|
|
268
271
|
mock_popen.assert_called_once()
|
|
269
272
|
popen_args = mock_popen.call_args
|
|
270
273
|
|
|
271
|
-
# Check the command
|
|
272
|
-
self.assertEqual(popen_args[0][0], [sys.executable,
|
|
274
|
+
# Check the command (normal Python install, not frozen)
|
|
275
|
+
self.assertEqual(popen_args[0][0], [sys.executable, EXECUTOR_PATH])
|
|
273
276
|
|
|
274
277
|
# Check that stdin was written to
|
|
275
278
|
mock_process.stdin.write.assert_called_once()
|
|
@@ -296,6 +299,35 @@ print(json.dumps(output))
|
|
|
296
299
|
self.assertEqual(stdin_json["options"], options)
|
|
297
300
|
self.assertEqual(stdin_json["run_config"], run_config)
|
|
298
301
|
|
|
302
|
+
@patch("nextmv.local.runner.subprocess.Popen")
|
|
303
|
+
@patch("nextmv.local.runner.safe_id")
|
|
304
|
+
def test_run_function_execution_frozen(self, mock_safe_id, mock_popen):
|
|
305
|
+
"""Test that --run-script is used when running as a frozen PyInstaller binary."""
|
|
306
|
+
mock_safe_id.return_value = "test-run-id"
|
|
307
|
+
mock_process = Mock()
|
|
308
|
+
mock_process.stdin = Mock()
|
|
309
|
+
mock_popen.return_value = mock_process
|
|
310
|
+
|
|
311
|
+
manifest = Manifest(
|
|
312
|
+
files=["main.py"],
|
|
313
|
+
runtime=ManifestRuntime.PYTHON,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
run_config = {"format": {"input": {"type": "json"}, "output": {"type": "json"}}}
|
|
317
|
+
|
|
318
|
+
with patch.object(sys, "frozen", True, create=True):
|
|
319
|
+
run(
|
|
320
|
+
app_id="sample-app",
|
|
321
|
+
src=self.test_src,
|
|
322
|
+
manifest=manifest,
|
|
323
|
+
run_config=run_config,
|
|
324
|
+
input_data={},
|
|
325
|
+
options={},
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
popen_args = mock_popen.call_args
|
|
329
|
+
self.assertEqual(popen_args[0][0], [sys.executable, "--run-script", EXECUTOR_PATH])
|
|
330
|
+
|
|
299
331
|
@patch("nextmv.local.runner.subprocess.Popen")
|
|
300
332
|
@patch("nextmv.local.runner.safe_id")
|
|
301
333
|
def test_run_with_inputs_dir_path(self, mock_safe_id, mock_popen):
|