minitest-cli 0.34.0__tar.gz → 0.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.
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/PKG-INFO +30 -2
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/README.md +29 -1
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/pyproject.toml +1 -1
- minitest_cli-0.35.0/src/minitest_cli/commands/app_network_helpers.py +114 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/apps.py +9 -2
- minitest_cli-0.35.0/src/minitest_cli/commands/private_network.py +143 -0
- minitest_cli-0.35.0/src/minitest_cli/commands/private_network_helpers.py +93 -0
- minitest_cli-0.35.0/src/minitest_cli/commands/residential_proxy.py +121 -0
- minitest_cli-0.35.0/tests/_network_settings_server.py +106 -0
- minitest_cli-0.35.0/tests/test_private_network_commands.py +118 -0
- minitest_cli-0.35.0/tests/test_residential_proxy_commands.py +89 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/uv.lock +1 -1
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/.env.example +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/.github/workflows/ci.yml +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/.github/workflows/install-scripts.yml +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/.github/workflows/release.yml +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/.gitignore +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/.opencode/skill/release/SKILL.md +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/AGENTS.md +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/RELEASE.md +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/install.ps1 +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/install.sh +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/pyrightconfig.json +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/renovate.json +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/__init__.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/api/__init__.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/api/apps_manager_client.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/api/client.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/api/errors.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/assets/__init__.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/assets/callback.html +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/__init__.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/_response_errors.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/app_knowledge.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/app_knowledge_helpers.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/app_skill.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/app_skill_helpers.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/app_skill_secret.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/app_skill_write.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/apps_dependencies.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/apps_get.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/apps_helpers.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/apps_table.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/auth.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/auth_api_key.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/batch.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/batch_helpers.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/batch_warnings.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/build.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/build_commit.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/build_fix_prompts.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/build_helpers.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/capabilities.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/commit_helpers.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/deprecated_alias.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/draft_feature.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/draft_feature_apply.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/draft_feature_conflicts.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/draft_feature_helpers.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/draft_feature_show.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/env.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/env_helpers.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/flow_types.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/flow_types_helpers.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/flow_types_write.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/init.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/init_helpers.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/init_playbook.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/issues.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/issues_fix.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/issues_helpers.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/issues_projection.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/issues_service.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/maintenance.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/maintenance_callbacks.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/maintenance_helpers.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/maintenance_state.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/recording_timeline.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/run.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/run_cancel.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/run_commit.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/run_commit_helpers.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/run_display.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/run_feedback.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/run_helpers.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/run_recording.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/run_recording_helpers.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/run_tagged.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/run_targets.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/screens.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/screens_detail.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/screens_format.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/screens_helpers.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/screens_tree.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/skill.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/tags.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/tags_helpers.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/test_file.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/test_file_helpers.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/test_file_list.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/test_profile.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/test_profile_card.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/test_profile_default.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/test_profile_helpers.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/test_profile_list.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/test_profile_update.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/upgrade.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/user_story.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/user_story_app_skills.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/user_story_bindings.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/user_story_camera.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/user_story_create.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/user_story_criteria.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/user_story_criterion_edits.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/user_story_delete.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/user_story_device_count.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/user_story_helpers.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/user_story_modify.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/user_story_overrides.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/user_story_profiles.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/user_story_setup_commit.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/user_story_update.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/verdicts.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/verdicts_helpers.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/commands/verdicts_projection.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/core/__init__.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/core/app_context.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/core/auth.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/core/config.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/core/credentials.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/core/oauth.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/core/tenants.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/core/token_exchange.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/main.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/models/__init__.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/models/app.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/models/app_env_vars.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/models/app_failure.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/models/app_screen.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/models/base.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/models/batch.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/models/build.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/models/commit_build.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/models/draft_feature.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/models/flow_type.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/models/issues.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/models/recording.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/models/story_run.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/models/tag.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/models/targets.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/models/user_story.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/utils/__init__.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/utils/confirm.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/utils/dependency_sim.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/utils/mermaid.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/utils/output.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/utils/skill_refresh.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/utils/update_check.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/src/minitest_cli/utils/video_frames.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/__init__.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/_commit_transport.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/conftest.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_api_client_auth.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_app_knowledge_commands.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_app_skill_commands.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_apps_commands.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_apps_dependencies.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_auth.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_auth_api_key.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_auth_commands.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_batch_commands.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_build_commands.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_build_fix_prompts.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_capabilities_command.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_channel_header.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_code_quality.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_commit_commands.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_deprecated_commands.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_deprecated_flags.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_draft_feature_commands.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_env.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_flow_types_commands.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_init_command.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_issues.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_issues_commands.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_mermaid.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_native_device_types.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_network_errors.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_recording_timeline.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_response_errors.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_run_cancel.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_run_commands.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_run_frames_command.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_screens_commands.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_skill_command.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_suite_change_provenance.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_tags_commands.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_test_file_commands.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_test_profile_commands.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_upgrade_command.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_user_story_bindings_commands.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_user_story_commands.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_user_story_setup_commit.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_verdicts.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_verdicts_actionable.py +0 -0
- {minitest_cli-0.34.0 → minitest_cli-0.35.0}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: minitest-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.35.0
|
|
4
4
|
Summary: Minitest CLI – command-line interface for the Minitest testing platform
|
|
5
5
|
Project-URL: Homepage, https://minitap.ai/
|
|
6
6
|
Project-URL: Source, https://github.com/minitap-ai/minitest-cli
|
|
@@ -166,7 +166,7 @@ app defaults.
|
|
|
166
166
|
| Command | Description |
|
|
167
167
|
| ---------------- | ------------------------- |
|
|
168
168
|
| `minitest auth` | Authentication management |
|
|
169
|
-
| `minitest apps` | List, inspect, and create apps |
|
|
169
|
+
| `minitest apps` | List, inspect, and create apps; residential proxy and private network settings |
|
|
170
170
|
| `minitest scenario` | Create, update, list and delete scenarios |
|
|
171
171
|
| `minitest scenario-binding` | Bind test accounts, test files or app skills to scenarios |
|
|
172
172
|
| `minitest draft` | Drafts — branches of the test suite |
|
|
@@ -198,6 +198,34 @@ stderr and leave `--json` stdout unchanged. Switch to the new names.
|
|
|
198
198
|
| `run cancel --srp`, `run recording --srp` | `--target-id` |
|
|
199
199
|
| `--type` on `scenario create/update/list` | `--tag` |
|
|
200
200
|
|
|
201
|
+
## App network settings
|
|
202
|
+
|
|
203
|
+
Two per-app settings decide how test devices reach the network. Anyone on the
|
|
204
|
+
app can read them; only workspace **owners and admins** can change them (other
|
|
205
|
+
roles get `Only workspace owners and admins can change this setting`, exit 2).
|
|
206
|
+
`set` reads the current settings first and changes only what you pass.
|
|
207
|
+
Domains are an exact host (`api.stg.example.com`) or `*.example.com`, which
|
|
208
|
+
matches subdomains only, not `example.com` itself.
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
# Residential proxy: reach the listed domains from a residential IP in one country
|
|
212
|
+
minitest --app <app-id> apps residential-proxy show
|
|
213
|
+
minitest --app <app-id> apps residential-proxy set [--enable|--disable] [--country tr] \
|
|
214
|
+
[--domain ipinfo.io ...] [--remove-domain <d> ...] [--clear-domains]
|
|
215
|
+
|
|
216
|
+
# Private network: reach internal domains through a connector running in your network
|
|
217
|
+
minitest --app <app-id> apps private-network show
|
|
218
|
+
minitest --app <app-id> apps private-network connectors
|
|
219
|
+
minitest --app <app-id> apps private-network set [--enable|--disable] [--connector <name-or-id>] \
|
|
220
|
+
[--domain api.stg.corp.internal ...] [--remove-domain <d> ...] [--clear-domains] [--port 443 ...]
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`--clear-domains --domain a --domain b` replaces the list; `--port` replaces the
|
|
224
|
+
ports (default 443). Turning the private network on needs a connector and at
|
|
225
|
+
least one domain. Connectors cannot be created from the CLI: a workspace owner
|
|
226
|
+
or admin creates them in the webapp, under **Workspace settings → Private
|
|
227
|
+
network**.
|
|
228
|
+
|
|
201
229
|
## CLI-only maintenance
|
|
202
230
|
|
|
203
231
|
`minitest maintenance` lets a coding agent keep Minitest scenarios in sync
|
|
@@ -133,7 +133,7 @@ app defaults.
|
|
|
133
133
|
| Command | Description |
|
|
134
134
|
| ---------------- | ------------------------- |
|
|
135
135
|
| `minitest auth` | Authentication management |
|
|
136
|
-
| `minitest apps` | List, inspect, and create apps |
|
|
136
|
+
| `minitest apps` | List, inspect, and create apps; residential proxy and private network settings |
|
|
137
137
|
| `minitest scenario` | Create, update, list and delete scenarios |
|
|
138
138
|
| `minitest scenario-binding` | Bind test accounts, test files or app skills to scenarios |
|
|
139
139
|
| `minitest draft` | Drafts — branches of the test suite |
|
|
@@ -165,6 +165,34 @@ stderr and leave `--json` stdout unchanged. Switch to the new names.
|
|
|
165
165
|
| `run cancel --srp`, `run recording --srp` | `--target-id` |
|
|
166
166
|
| `--type` on `scenario create/update/list` | `--tag` |
|
|
167
167
|
|
|
168
|
+
## App network settings
|
|
169
|
+
|
|
170
|
+
Two per-app settings decide how test devices reach the network. Anyone on the
|
|
171
|
+
app can read them; only workspace **owners and admins** can change them (other
|
|
172
|
+
roles get `Only workspace owners and admins can change this setting`, exit 2).
|
|
173
|
+
`set` reads the current settings first and changes only what you pass.
|
|
174
|
+
Domains are an exact host (`api.stg.example.com`) or `*.example.com`, which
|
|
175
|
+
matches subdomains only, not `example.com` itself.
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
# Residential proxy: reach the listed domains from a residential IP in one country
|
|
179
|
+
minitest --app <app-id> apps residential-proxy show
|
|
180
|
+
minitest --app <app-id> apps residential-proxy set [--enable|--disable] [--country tr] \
|
|
181
|
+
[--domain ipinfo.io ...] [--remove-domain <d> ...] [--clear-domains]
|
|
182
|
+
|
|
183
|
+
# Private network: reach internal domains through a connector running in your network
|
|
184
|
+
minitest --app <app-id> apps private-network show
|
|
185
|
+
minitest --app <app-id> apps private-network connectors
|
|
186
|
+
minitest --app <app-id> apps private-network set [--enable|--disable] [--connector <name-or-id>] \
|
|
187
|
+
[--domain api.stg.corp.internal ...] [--remove-domain <d> ...] [--clear-domains] [--port 443 ...]
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`--clear-domains --domain a --domain b` replaces the list; `--port` replaces the
|
|
191
|
+
ports (default 443). Turning the private network on needs a connector and at
|
|
192
|
+
least one domain. Connectors cannot be created from the CLI: a workspace owner
|
|
193
|
+
or admin creates them in the webapp, under **Workspace settings → Private
|
|
194
|
+
network**.
|
|
195
|
+
|
|
168
196
|
## CLI-only maintenance
|
|
169
197
|
|
|
170
198
|
`minitest maintenance` lets a coding agent keep Minitest scenarios in sync
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Shared plumbing for the per-app network settings (residential proxy, private network)."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from minitest_cli.api.client import ApiClient
|
|
11
|
+
from minitest_cli.api.errors import format_network_error
|
|
12
|
+
from minitest_cli.commands._response_errors import extract_detail
|
|
13
|
+
from minitest_cli.core.app_context import resolve_app_id
|
|
14
|
+
from minitest_cli.core.auth import require_auth
|
|
15
|
+
from minitest_cli.core.config import Settings
|
|
16
|
+
from minitest_cli.utils.output import print_error
|
|
17
|
+
|
|
18
|
+
EXIT_GENERAL_ERROR = 1
|
|
19
|
+
EXIT_AUTH_ERROR = 2
|
|
20
|
+
EXIT_NETWORK_ERROR = 3
|
|
21
|
+
EXIT_NOT_FOUND = 4
|
|
22
|
+
|
|
23
|
+
ADMIN_ONLY_MESSAGE = "Only workspace owners and admins can change this setting."
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def is_json_mode() -> bool:
|
|
27
|
+
return typer.Context.json_mode # type: ignore[attr-defined]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def resolve_target() -> tuple[Settings, str]:
|
|
31
|
+
settings: Settings = typer.Context.settings # type: ignore[attr-defined]
|
|
32
|
+
require_auth(settings)
|
|
33
|
+
return settings, resolve_app_id(settings, typer.Context.app_flag) # type: ignore[attr-defined]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def fail(message: str, code: int = EXIT_GENERAL_ERROR) -> typer.Exit:
|
|
37
|
+
print_error(message)
|
|
38
|
+
return typer.Exit(code=code)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _raise_for_status(resp: httpx.Response) -> None:
|
|
42
|
+
if resp.status_code < 400:
|
|
43
|
+
return
|
|
44
|
+
detail = extract_detail(resp)
|
|
45
|
+
if resp.status_code == 403:
|
|
46
|
+
raise fail(f"{ADMIN_ONLY_MESSAGE} ({detail})", EXIT_AUTH_ERROR)
|
|
47
|
+
if resp.status_code == 401:
|
|
48
|
+
raise fail(f"Authentication failed: {detail}", EXIT_AUTH_ERROR)
|
|
49
|
+
if resp.status_code == 404:
|
|
50
|
+
raise fail(f"Not found: {detail}", EXIT_NOT_FOUND)
|
|
51
|
+
if resp.status_code in (400, 409, 422):
|
|
52
|
+
raise fail(f"Rejected by the server ({resp.status_code}): {detail}")
|
|
53
|
+
raise fail(f"API error ({resp.status_code}): {detail}", EXIT_NETWORK_ERROR)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def call_api(
|
|
57
|
+
settings: Settings, method: str, path: str, body: dict[str, Any] | None = None
|
|
58
|
+
) -> dict[str, Any]:
|
|
59
|
+
async def _run() -> dict[str, Any]:
|
|
60
|
+
async with ApiClient(settings) as client:
|
|
61
|
+
resp = await client.request(method, path, json=body)
|
|
62
|
+
_raise_for_status(resp)
|
|
63
|
+
try:
|
|
64
|
+
return resp.json()
|
|
65
|
+
except ValueError as exc:
|
|
66
|
+
raise fail(
|
|
67
|
+
f"Unexpected non-JSON response ({resp.status_code}): {resp.text[:200]}",
|
|
68
|
+
EXIT_NETWORK_ERROR,
|
|
69
|
+
) from exc
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
return asyncio.run(_run())
|
|
73
|
+
except httpx.HTTPError as exc:
|
|
74
|
+
raise fail(format_network_error(exc), EXIT_NETWORK_ERROR) from exc
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def merge_domains(
|
|
78
|
+
current: list[str],
|
|
79
|
+
*,
|
|
80
|
+
add: list[str] | None,
|
|
81
|
+
remove: list[str] | None,
|
|
82
|
+
clear: bool,
|
|
83
|
+
pattern: str,
|
|
84
|
+
max_domains: int,
|
|
85
|
+
) -> list[str]:
|
|
86
|
+
"""Apply ``--clear-domains``, ``--remove-domain`` then ``--domain`` and validate the result."""
|
|
87
|
+
added = [_normalize(d) for d in add or []]
|
|
88
|
+
invalid = [d for d in added if not re.fullmatch(pattern, d)]
|
|
89
|
+
if invalid:
|
|
90
|
+
raise fail(
|
|
91
|
+
f"Invalid domain(s): {', '.join(invalid)}. Use a lowercase host like api.example.com, "
|
|
92
|
+
"or *.example.com to match its subdomains (not example.com itself)."
|
|
93
|
+
)
|
|
94
|
+
removed = {_normalize(d) for d in remove or []}
|
|
95
|
+
unknown = sorted(removed - set(current))
|
|
96
|
+
if unknown:
|
|
97
|
+
raise fail(f"Not in the current domain list: {', '.join(unknown)}.")
|
|
98
|
+
base = [] if clear else [d for d in current if d not in removed]
|
|
99
|
+
merged = list(dict.fromkeys([*base, *added]))
|
|
100
|
+
if len(merged) > max_domains:
|
|
101
|
+
raise fail(f"Too many domains: {len(merged)} (max {max_domains}).")
|
|
102
|
+
return merged
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _normalize(domain: str) -> str:
|
|
106
|
+
return domain.strip().lower().rstrip(".")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def on_off(value: bool) -> str:
|
|
110
|
+
return "on" if value else "off"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def domain_list(domains: list[str]) -> str:
|
|
114
|
+
return ", ".join(domains) if domains else "-"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""App management commands: list, get, create, dependencies."""
|
|
1
|
+
"""App management commands: list, get, create, dependencies, network settings."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
from pathlib import Path
|
|
@@ -10,7 +10,12 @@ import typer
|
|
|
10
10
|
from minitest_cli.api.apps_manager_client import AppsManagerClient # noqa: F401 re-exported for tests
|
|
11
11
|
from minitest_cli.api.client import ApiClient
|
|
12
12
|
from minitest_cli.api.errors import format_network_error
|
|
13
|
-
from minitest_cli.commands import
|
|
13
|
+
from minitest_cli.commands import (
|
|
14
|
+
apps_dependencies,
|
|
15
|
+
apps_get,
|
|
16
|
+
private_network,
|
|
17
|
+
residential_proxy,
|
|
18
|
+
)
|
|
14
19
|
from minitest_cli.commands.apps_helpers import create_app_request
|
|
15
20
|
from minitest_cli.commands.apps_table import APP_TABLE_HEADERS, app_row
|
|
16
21
|
from minitest_cli.core.auth import require_auth
|
|
@@ -30,6 +35,8 @@ EXIT_NETWORK_ERROR = 3
|
|
|
30
35
|
app = typer.Typer(name="apps", help="App management.")
|
|
31
36
|
app.command(name="dependencies")(apps_dependencies.dependencies)
|
|
32
37
|
app.command(name="get")(apps_get.get_app)
|
|
38
|
+
app.add_typer(residential_proxy.app)
|
|
39
|
+
app.add_typer(private_network.app)
|
|
33
40
|
|
|
34
41
|
|
|
35
42
|
@app.callback()
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""'apps private-network': reach a customer's internal domains through a connector."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Any
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from minitest_cli.commands.app_network_helpers import (
|
|
8
|
+
call_api,
|
|
9
|
+
fail,
|
|
10
|
+
is_json_mode,
|
|
11
|
+
merge_domains,
|
|
12
|
+
resolve_target,
|
|
13
|
+
)
|
|
14
|
+
from minitest_cli.commands.private_network_helpers import (
|
|
15
|
+
CREATE_IN_WEBAPP_HINT,
|
|
16
|
+
UNAVAILABLE_MESSAGE,
|
|
17
|
+
render_config,
|
|
18
|
+
render_connectors,
|
|
19
|
+
resolve_connector,
|
|
20
|
+
)
|
|
21
|
+
from minitest_cli.core.config import Settings
|
|
22
|
+
from minitest_cli.utils.output import print_success
|
|
23
|
+
|
|
24
|
+
app = typer.Typer(
|
|
25
|
+
name="private-network",
|
|
26
|
+
help=(
|
|
27
|
+
"Private network of the app (--app): devices reach internal domains through a "
|
|
28
|
+
"connector running in your network. Changing it needs the OWNER or ADMIN role. "
|
|
29
|
+
f"{CREATE_IN_WEBAPP_HINT}"
|
|
30
|
+
),
|
|
31
|
+
no_args_is_help=True,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _path(app_id: str) -> str:
|
|
36
|
+
return f"/api/v1/apps/{app_id}/private-network"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.command(name="show")
|
|
40
|
+
def show() -> None:
|
|
41
|
+
"""Show the app's private network settings."""
|
|
42
|
+
settings, app_id = resolve_target()
|
|
43
|
+
config = call_api(settings, "GET", _path(app_id))
|
|
44
|
+
render_config(config)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.command(name="connectors")
|
|
48
|
+
def connectors() -> None:
|
|
49
|
+
"""List the workspace connectors the app can route through (read-only)."""
|
|
50
|
+
settings, app_id = resolve_target()
|
|
51
|
+
listing = call_api(settings, "GET", f"{_path(app_id)}/connectors")
|
|
52
|
+
render_connectors(listing)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.command(name="set")
|
|
56
|
+
def set_config(
|
|
57
|
+
enable: Annotated[
|
|
58
|
+
bool | None,
|
|
59
|
+
typer.Option(
|
|
60
|
+
"--enable/--disable",
|
|
61
|
+
help="Switch it on or off. On needs a connector and at least one domain.",
|
|
62
|
+
),
|
|
63
|
+
] = None,
|
|
64
|
+
connector: Annotated[
|
|
65
|
+
str | None,
|
|
66
|
+
typer.Option("--connector", help="Connector name or id (see 'connectors')."),
|
|
67
|
+
] = None,
|
|
68
|
+
domain: Annotated[
|
|
69
|
+
list[str] | None,
|
|
70
|
+
typer.Option(
|
|
71
|
+
"--domain",
|
|
72
|
+
help="Internal domain to reach (repeatable): host, or *.host for its subdomains only.",
|
|
73
|
+
),
|
|
74
|
+
] = None,
|
|
75
|
+
remove_domain: Annotated[
|
|
76
|
+
list[str] | None,
|
|
77
|
+
typer.Option("--remove-domain", help="Domain to stop reaching (repeatable)."),
|
|
78
|
+
] = None,
|
|
79
|
+
clear_domains: Annotated[
|
|
80
|
+
bool,
|
|
81
|
+
typer.Option("--clear-domains", help="Drop every current domain before adding --domain."),
|
|
82
|
+
] = False,
|
|
83
|
+
port: Annotated[
|
|
84
|
+
list[int] | None,
|
|
85
|
+
typer.Option(
|
|
86
|
+
"--port",
|
|
87
|
+
min=1,
|
|
88
|
+
max=65535,
|
|
89
|
+
help="Port devices may reach (repeatable). Replaces the current ports; default 443.",
|
|
90
|
+
),
|
|
91
|
+
] = None,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Change the app's private network settings; options not passed stay as they are."""
|
|
94
|
+
if not any([enable is not None, connector, domain, remove_domain, clear_domains, port]):
|
|
95
|
+
raise fail(
|
|
96
|
+
"Nothing to change: pass --enable/--disable, --connector, a domain option or --port."
|
|
97
|
+
)
|
|
98
|
+
settings, app_id = resolve_target()
|
|
99
|
+
current = call_api(settings, "GET", _path(app_id))
|
|
100
|
+
body = _build_body(settings, app_id, current, enable, connector, port)
|
|
101
|
+
body["domains"] = merge_domains(
|
|
102
|
+
current["domains"],
|
|
103
|
+
add=domain,
|
|
104
|
+
remove=remove_domain,
|
|
105
|
+
clear=clear_domains,
|
|
106
|
+
pattern=current["domainPattern"],
|
|
107
|
+
max_domains=current["maxDomains"],
|
|
108
|
+
)
|
|
109
|
+
if body["enabled"]:
|
|
110
|
+
if not current["available"]:
|
|
111
|
+
raise fail(UNAVAILABLE_MESSAGE)
|
|
112
|
+
if not body["connectorId"]:
|
|
113
|
+
raise fail(
|
|
114
|
+
"Turning it on needs a connector: pass --connector <name-or-id> "
|
|
115
|
+
f"(see 'minitest apps private-network connectors'). {CREATE_IN_WEBAPP_HINT}"
|
|
116
|
+
)
|
|
117
|
+
if not body["domains"]:
|
|
118
|
+
raise fail("Turning it on needs at least one --domain.")
|
|
119
|
+
|
|
120
|
+
updated = call_api(settings, "PUT", _path(app_id), body)
|
|
121
|
+
if not is_json_mode():
|
|
122
|
+
print_success("Private network updated.")
|
|
123
|
+
render_config(updated)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _build_body(
|
|
127
|
+
settings: Settings,
|
|
128
|
+
app_id: str,
|
|
129
|
+
current: dict[str, Any],
|
|
130
|
+
enable: bool | None,
|
|
131
|
+
connector: str | None,
|
|
132
|
+
port: list[int] | None,
|
|
133
|
+
) -> dict[str, Any]:
|
|
134
|
+
if connector:
|
|
135
|
+
listing = call_api(settings, "GET", f"{_path(app_id)}/connectors")
|
|
136
|
+
connector_id = resolve_connector(listing["connectors"], connector)["id"]
|
|
137
|
+
else:
|
|
138
|
+
connector_id = current["connector"]["id"] if current["connector"] else None
|
|
139
|
+
return {
|
|
140
|
+
"enabled": current["enabled"] if enable is None else enable,
|
|
141
|
+
"connectorId": connector_id,
|
|
142
|
+
"ports": list(dict.fromkeys(port)) if port else current["ports"] or [443],
|
|
143
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Rendering and connector lookup for 'apps private-network'."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from minitest_cli.commands.app_network_helpers import domain_list, fail, is_json_mode, on_off
|
|
6
|
+
from minitest_cli.utils.output import print_info, print_json, print_table, print_warning
|
|
7
|
+
|
|
8
|
+
UNAVAILABLE_MESSAGE = "This workspace cannot use private network. Contact Minitap to enable it."
|
|
9
|
+
CREATE_IN_WEBAPP_HINT = (
|
|
10
|
+
"Connectors can only be created by a workspace owner or admin in the webapp: "
|
|
11
|
+
"Workspace settings → Private network."
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def resolve_connector(connectors: list[dict[str, Any]], ref: str) -> dict[str, Any]:
|
|
16
|
+
"""Find a connector by id, or by name (case-insensitive)."""
|
|
17
|
+
if not connectors:
|
|
18
|
+
raise fail(f"This workspace has no connector. {CREATE_IN_WEBAPP_HINT}")
|
|
19
|
+
by_id = [c for c in connectors if c["id"] == ref]
|
|
20
|
+
matches = by_id or [c for c in connectors if c["name"].casefold() == ref.casefold()]
|
|
21
|
+
if len(matches) == 1:
|
|
22
|
+
return matches[0]
|
|
23
|
+
if matches:
|
|
24
|
+
ids = ", ".join(c["id"] for c in matches)
|
|
25
|
+
raise fail(f"Several connectors are named '{ref}': pass one of these ids instead: {ids}.")
|
|
26
|
+
names = ", ".join(f"{c['name']} ({c['status']})" for c in connectors)
|
|
27
|
+
raise fail(f"No connector named '{ref}'. Available: {names}. {CREATE_IN_WEBAPP_HINT}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def without_token_prefix(config: dict[str, Any]) -> dict[str, Any]:
|
|
31
|
+
connector = config.get("connector")
|
|
32
|
+
if not connector:
|
|
33
|
+
return config
|
|
34
|
+
return {**config, "connector": {k: v for k, v in connector.items() if k != "tokenPrefix"}}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def render_config(config: dict[str, Any]) -> None:
|
|
38
|
+
config = without_token_prefix(config)
|
|
39
|
+
if not config["available"]:
|
|
40
|
+
print_warning(UNAVAILABLE_MESSAGE)
|
|
41
|
+
if is_json_mode():
|
|
42
|
+
print_json(config)
|
|
43
|
+
warn_connector_state(config)
|
|
44
|
+
return
|
|
45
|
+
connector = config["connector"]
|
|
46
|
+
print(f"Enabled: {on_off(config['enabled'])}") # noqa: T201
|
|
47
|
+
if connector:
|
|
48
|
+
print(f"Connector: {connector['name']} ({connector['status']}) {connector['id']}") # noqa: T201
|
|
49
|
+
else:
|
|
50
|
+
print("Connector: -") # noqa: T201
|
|
51
|
+
print(f"Domains: {domain_list(config['domains'])}") # noqa: T201
|
|
52
|
+
print(f"Ports: {', '.join(str(p) for p in config['ports']) or '-'}") # noqa: T201
|
|
53
|
+
warn_connector_state(config)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def warn_connector_state(config: dict[str, Any]) -> None:
|
|
57
|
+
connector = config["connector"]
|
|
58
|
+
if config["enabled"] and connector and connector["status"] != "online":
|
|
59
|
+
print_warning(
|
|
60
|
+
f"Connector '{connector['name']}' is {connector['status']}: runs cannot reach "
|
|
61
|
+
"the internal domains until it is connected."
|
|
62
|
+
)
|
|
63
|
+
refused = config["domainsRefusedByConnector"]
|
|
64
|
+
if refused:
|
|
65
|
+
print_warning(
|
|
66
|
+
f"The connector's own allowlist refuses: {', '.join(refused)}. Add them on the "
|
|
67
|
+
"connector machine, or the devices will not reach them."
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def render_connectors(listing: dict[str, Any]) -> None:
|
|
72
|
+
if not listing["available"]:
|
|
73
|
+
print_warning(UNAVAILABLE_MESSAGE)
|
|
74
|
+
connectors = listing["connectors"]
|
|
75
|
+
if not connectors:
|
|
76
|
+
print_info(f"No connector yet. {CREATE_IN_WEBAPP_HINT}")
|
|
77
|
+
if is_json_mode():
|
|
78
|
+
print_json(listing)
|
|
79
|
+
return
|
|
80
|
+
if not connectors:
|
|
81
|
+
return
|
|
82
|
+
rows = [
|
|
83
|
+
[
|
|
84
|
+
c["name"],
|
|
85
|
+
c["status"],
|
|
86
|
+
domain_list(c["domains"]),
|
|
87
|
+
", ".join(str(p) for p in c["ports"]) or "-",
|
|
88
|
+
c.get("lastSeenAt") or "-",
|
|
89
|
+
c["id"],
|
|
90
|
+
]
|
|
91
|
+
for c in connectors
|
|
92
|
+
]
|
|
93
|
+
print_table(["Name", "Status", "Domains", "Ports", "Last seen", "ID"], rows, title="Connectors")
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""'apps residential-proxy': route some of an app's device traffic through residential IPs."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Any
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from minitest_cli.commands.app_network_helpers import (
|
|
8
|
+
call_api,
|
|
9
|
+
domain_list,
|
|
10
|
+
fail,
|
|
11
|
+
is_json_mode,
|
|
12
|
+
merge_domains,
|
|
13
|
+
on_off,
|
|
14
|
+
resolve_target,
|
|
15
|
+
)
|
|
16
|
+
from minitest_cli.utils.output import print_json, print_success, print_warning
|
|
17
|
+
|
|
18
|
+
app = typer.Typer(
|
|
19
|
+
name="residential-proxy",
|
|
20
|
+
help=(
|
|
21
|
+
"Residential proxy of the app (--app): devices reach the listed domains from a "
|
|
22
|
+
"residential IP in the chosen country. Changing it needs the OWNER or ADMIN role."
|
|
23
|
+
),
|
|
24
|
+
no_args_is_help=True,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
UNAVAILABLE_MESSAGE = (
|
|
28
|
+
"This workspace cannot use the residential proxy. Contact Minitap to enable it."
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _path(app_id: str) -> str:
|
|
33
|
+
return f"/api/v1/apps/{app_id}/residential-proxy"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _render(config: dict[str, Any]) -> None:
|
|
37
|
+
if not config["available"]:
|
|
38
|
+
print_warning(UNAVAILABLE_MESSAGE)
|
|
39
|
+
if is_json_mode():
|
|
40
|
+
print_json(config)
|
|
41
|
+
return
|
|
42
|
+
print(f"Enabled: {on_off(config['enabled'])}") # noqa: T201
|
|
43
|
+
print(f"Country: {config['country']}") # noqa: T201
|
|
44
|
+
print(f"Domains: {domain_list(config['allowedDomains'])}") # noqa: T201
|
|
45
|
+
print(f"Countries available: {', '.join(config['availableCountries'])}") # noqa: T201
|
|
46
|
+
print(f"Price: {config['usdPerGb']} USD per GB") # noqa: T201
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@app.command(name="show")
|
|
50
|
+
def show() -> None:
|
|
51
|
+
"""Show the app's residential proxy settings."""
|
|
52
|
+
settings, app_id = resolve_target()
|
|
53
|
+
config = call_api(settings, "GET", _path(app_id))
|
|
54
|
+
_render(config)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@app.command(name="set")
|
|
58
|
+
def set_config(
|
|
59
|
+
enable: Annotated[
|
|
60
|
+
bool | None,
|
|
61
|
+
typer.Option("--enable/--disable", help="Switch the proxy on or off."),
|
|
62
|
+
] = None,
|
|
63
|
+
country: Annotated[
|
|
64
|
+
str | None,
|
|
65
|
+
typer.Option("--country", help="Exit country code, e.g. tr (see 'show')."),
|
|
66
|
+
] = None,
|
|
67
|
+
domain: Annotated[
|
|
68
|
+
list[str] | None,
|
|
69
|
+
typer.Option(
|
|
70
|
+
"--domain",
|
|
71
|
+
help="Domain to route through the proxy (repeatable): host or *.host for subdomains.",
|
|
72
|
+
),
|
|
73
|
+
] = None,
|
|
74
|
+
remove_domain: Annotated[
|
|
75
|
+
list[str] | None,
|
|
76
|
+
typer.Option("--remove-domain", help="Domain to stop routing (repeatable)."),
|
|
77
|
+
] = None,
|
|
78
|
+
clear_domains: Annotated[
|
|
79
|
+
bool,
|
|
80
|
+
typer.Option("--clear-domains", help="Drop every current domain before adding --domain."),
|
|
81
|
+
] = False,
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Change the app's residential proxy settings; options not passed stay as they are."""
|
|
84
|
+
if (
|
|
85
|
+
enable is None
|
|
86
|
+
and country is None
|
|
87
|
+
and not domain
|
|
88
|
+
and not remove_domain
|
|
89
|
+
and not clear_domains
|
|
90
|
+
):
|
|
91
|
+
raise fail("Nothing to change: pass --enable/--disable, --country or a domain option.")
|
|
92
|
+
settings, app_id = resolve_target()
|
|
93
|
+
current = call_api(settings, "GET", _path(app_id))
|
|
94
|
+
if not current["available"]:
|
|
95
|
+
raise fail(UNAVAILABLE_MESSAGE)
|
|
96
|
+
|
|
97
|
+
chosen_country = country.strip().lower() if country else current["country"]
|
|
98
|
+
if chosen_country not in current["availableCountries"]:
|
|
99
|
+
raise fail(
|
|
100
|
+
f"Unknown country '{chosen_country}'. Pick one of: "
|
|
101
|
+
f"{', '.join(current['availableCountries'])}."
|
|
102
|
+
)
|
|
103
|
+
body = {
|
|
104
|
+
"enabled": current["enabled"] if enable is None else enable,
|
|
105
|
+
"country": chosen_country,
|
|
106
|
+
"allowedDomains": merge_domains(
|
|
107
|
+
current["allowedDomains"],
|
|
108
|
+
add=domain,
|
|
109
|
+
remove=remove_domain,
|
|
110
|
+
clear=clear_domains,
|
|
111
|
+
pattern=current["domainPattern"],
|
|
112
|
+
max_domains=current["maxDomains"],
|
|
113
|
+
),
|
|
114
|
+
}
|
|
115
|
+
if body["enabled"] and not body["allowedDomains"]:
|
|
116
|
+
print_warning("The proxy is on but no domain is listed, so no traffic goes through it.")
|
|
117
|
+
|
|
118
|
+
updated = call_api(settings, "PUT", _path(app_id), body)
|
|
119
|
+
if not is_json_mode():
|
|
120
|
+
print_success("Residential proxy updated.")
|
|
121
|
+
_render(updated)
|