wavexis 2.2.3__tar.gz → 2.2.5__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.
- {wavexis-2.2.3 → wavexis-2.2.5}/CHANGELOG.md +19 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/PKG-INFO +1 -1
- {wavexis-2.2.3 → wavexis-2.2.5}/pyproject.toml +1 -1
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/__init__.py +1 -1
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/cli/_capture.py +18 -9
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/cli/_shared.py +31 -5
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/cli/_workflow.py +22 -2
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/exceptions.py +28 -3
- {wavexis-2.2.3 → wavexis-2.2.5}/.dockerignore +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/.github/workflows/ci.yml +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/.github/workflows/docs.yml +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/.github/workflows/release.yml +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/.gitignore +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/CODE_OF_CONDUCT.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/CONTRIBUTING.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/Dockerfile +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/LICENSE +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/README.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/SECURITY.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/api/actions.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/api/backends.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/api/cli.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/api/config.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/api/exceptions.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/api/multi.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/api/plugins.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/api/serve.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/cookbook/auth.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/cookbook/ci-cd.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/cookbook/css.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/cookbook/debug.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/cookbook/record-replay.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/cookbook/scraping.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/cookbook/screenshots.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/cookbook/serve-mode.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/guide/assert.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/guide/backends.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/guide/commands.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/guide/init.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/guide/installation.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/guide/multi.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/guide/perf.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/guide/plugins.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/guide/quickstart.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/guide/raw.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/guide/repl.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/guide/troubleshooting.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/docs/index.md +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/mkdocs.yml +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/stubs/bidiwave/__init__.pyi +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/stubs/cdpwave/__init__.pyi +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/__init__.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/conftest.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/__init__.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_a11y.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_animation.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_backend_selection.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_browser.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_console.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_css.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_debug.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_dialog.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_dom.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_dom_snapshot.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_emulation.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_emulation_advanced.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_eval.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_har.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_input.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_media.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_multi.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_navigate.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_network.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_network_advanced.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_overlay.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_pdf.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_perf.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_permissions.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_raw.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_record.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_scrape.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_screenshot.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_security.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_serve.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_service_worker.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_storage.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_tabs.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_webaudio.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/integration/test_webauthn.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/__init__.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_abstract_backend_phase5.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_actions.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_actions_phase5.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_animation_action.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_assert_multi.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_auth.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_backend_manager.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_bidi_backend.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_bidi_phase5.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_bluetooth_action.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_cast_action.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_cleanup.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_cli_phase5.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_config.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_config_phase5.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_cookies_headers_actions.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_css_action.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_debug_action.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_dom_snapshot_action.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_download_screencast.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_emulation_action.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_error_handling.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_exceptions.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_format_record.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_global_options.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_init.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_input_action.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_interactions_crawl.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_manager.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_media_action.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_multi.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_multi_improvements.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_new_features.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_output.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_overlay_action.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_perf_action.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_perf_console.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_plugins.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_raw.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_record.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_repl.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_serve.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_service_worker_action.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_storage_action.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_webaudio_action.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/tests/unit/test_webauthn_action.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/__main__.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/__init__.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/accessibility.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/animation.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/base.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/bluetooth.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/browser.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/cast.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/console.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/cookies.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/crawl.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/css.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/debug.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/dialog.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/dom.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/dom_snapshot.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/download.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/emulation.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/eval.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/extract.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/form.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/har.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/headers.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/input.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/lighthouse.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/media.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/multi.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/navigate.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/network.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/overlay.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/pdf.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/performance.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/permissions.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/record.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/scrape.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/screencast.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/screenshot.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/security.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/service_worker.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/session.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/storage.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/tabs.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/wait.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/webaudio.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/webauthn.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/actions/websocket.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/auth.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/backend/__init__.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/backend/base.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/backend/bidi.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/backend/cdp.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/backend/manager.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/cleanup.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/cli/__init__.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/cli/_advanced.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/cli/_config.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/cli/_debug.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/cli/_emulation.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/cli/_experimental.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/cli/_input.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/cli/_navigation.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/cli/_network.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/cli/_perf.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/cli/_serve.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/cli/_session.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/cli/app.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/config.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/init.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/multi.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/output.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/plugins.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/record.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/repl.py +0 -0
- {wavexis-2.2.3 → wavexis-2.2.5}/wavexis/serve.py +0 -0
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to wavexis are documented in this file.
|
|
4
4
|
|
|
5
|
+
## v2.2.5 — 2026-07-07
|
|
6
|
+
|
|
7
|
+
### Improved
|
|
8
|
+
|
|
9
|
+
- Error messages now include actionable hints for common failures
|
|
10
|
+
- `ElementNotFoundError` suggests `wavexis dom` and `wavexis screenshot` for debugging
|
|
11
|
+
- `WaitTimeoutError` suggests increasing `--timeout` and alternative wait strategies
|
|
12
|
+
- `NavigationError` suggests checking URL, `--timeout`, and `--proxy`
|
|
13
|
+
- `SessionNotInitializedError` suggests `--headed` and checking Chromium installation
|
|
14
|
+
- `BackendNotAvailableError` shows install commands for both backends
|
|
15
|
+
- `MultiConfigError` suggests `--dry-run` to validate config
|
|
16
|
+
|
|
17
|
+
## v2.2.4 — 2026-07-07
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Progress reporting for long-running operations (`batch`, `multi`, `scrape`)
|
|
22
|
+
- `_progress(current, total, label)` helper shows `[n/total] — label` unless `--quiet`
|
|
23
|
+
|
|
5
24
|
## v2.2.3 — 2026-07-07
|
|
6
25
|
|
|
7
26
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wavexis
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.5
|
|
4
4
|
Summary: Browser automation CLI — wraps cdpwave and bidiwave, no Node.js, no Chromium download
|
|
5
5
|
Project-URL: Homepage, https://github.com/MathiasPaulenko/wavexis
|
|
6
6
|
Project-URL: Repository, https://github.com/MathiasPaulenko/wavexis
|
|
@@ -18,7 +18,9 @@ from wavexis.actions.screenshot import ScreenshotAction
|
|
|
18
18
|
from wavexis.cli._shared import (
|
|
19
19
|
Output,
|
|
20
20
|
_browser_options,
|
|
21
|
+
_echo,
|
|
21
22
|
_get_backend,
|
|
23
|
+
_progress,
|
|
22
24
|
_run_async,
|
|
23
25
|
_write_json_output,
|
|
24
26
|
app,
|
|
@@ -376,17 +378,24 @@ async def _scrape(
|
|
|
376
378
|
) -> list[dict[str, Any]]:
|
|
377
379
|
"""Async helper for scraping."""
|
|
378
380
|
backend = _get_backend()
|
|
381
|
+
total = len(urls)
|
|
382
|
+
_echo(f"Scraping {total} URL(s)…")
|
|
379
383
|
try:
|
|
380
384
|
await backend.launch(_browser_options())
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
385
|
+
results: list[dict[str, Any]] = []
|
|
386
|
+
for i, url in enumerate(urls):
|
|
387
|
+
_progress(i + 1, total, url)
|
|
388
|
+
params = ScrapeParams(
|
|
389
|
+
urls=[url],
|
|
390
|
+
expression=expression,
|
|
391
|
+
file=file,
|
|
392
|
+
output_format="json",
|
|
393
|
+
selector=selector,
|
|
394
|
+
wait=WaitStrategy(strategy="load"),
|
|
395
|
+
)
|
|
396
|
+
result = await ScrapeAction(params).execute(backend)
|
|
397
|
+
results.extend(result)
|
|
398
|
+
return results
|
|
390
399
|
finally:
|
|
391
400
|
await backend.close()
|
|
392
401
|
|
|
@@ -21,6 +21,7 @@ from wavexis.exceptions import (
|
|
|
21
21
|
ElementNotFoundError,
|
|
22
22
|
MultiConfigError,
|
|
23
23
|
NavigationError,
|
|
24
|
+
SessionNotInitializedError,
|
|
24
25
|
WaitTimeoutError,
|
|
25
26
|
WavexisError,
|
|
26
27
|
)
|
|
@@ -39,6 +40,7 @@ __all__ = [
|
|
|
39
40
|
"MultiConfigError",
|
|
40
41
|
"NavigationError",
|
|
41
42
|
"Output",
|
|
43
|
+
"SessionNotInitializedError",
|
|
42
44
|
"WaitTimeoutError",
|
|
43
45
|
"WavexisError",
|
|
44
46
|
"_browser_options",
|
|
@@ -48,6 +50,7 @@ __all__ = [
|
|
|
48
50
|
"_get_ctx",
|
|
49
51
|
"_handle_error",
|
|
50
52
|
"_load_global_config",
|
|
53
|
+
"_progress",
|
|
51
54
|
"_run_async",
|
|
52
55
|
"_write_json_output",
|
|
53
56
|
"app",
|
|
@@ -185,6 +188,21 @@ def _echo(msg: str) -> None:
|
|
|
185
188
|
typer.echo(msg)
|
|
186
189
|
|
|
187
190
|
|
|
191
|
+
def _progress(current: int, total: int, label: str = "") -> None:
|
|
192
|
+
"""Print a progress indicator unless quiet mode is active.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
current: Current item index (1-based).
|
|
196
|
+
total: Total number of items.
|
|
197
|
+
label: Optional label to prepend (e.g. URL or action name).
|
|
198
|
+
"""
|
|
199
|
+
ctx = _get_ctx()
|
|
200
|
+
if ctx.quiet:
|
|
201
|
+
return
|
|
202
|
+
suffix = f" — {label}" if label else ""
|
|
203
|
+
typer.echo(f"[{current}/{total}]{suffix}")
|
|
204
|
+
|
|
205
|
+
|
|
188
206
|
def _handle_error(e: Exception) -> None:
|
|
189
207
|
"""Handle a WavexisError with the correct exit code and message.
|
|
190
208
|
|
|
@@ -193,20 +211,28 @@ def _handle_error(e: Exception) -> None:
|
|
|
193
211
|
"""
|
|
194
212
|
if isinstance(e, BackendNotAvailableError):
|
|
195
213
|
Output.error(
|
|
196
|
-
"No backend available
|
|
214
|
+
"No backend available.\n"
|
|
215
|
+
"Hint: Install cdpwave with `pip install wavexis[cdp]`, "
|
|
216
|
+
"or bidiwave with `pip install wavexis[bidi]`."
|
|
197
217
|
)
|
|
198
218
|
raise typer.Exit(EXIT_BACKEND_ERROR) from e
|
|
219
|
+
if isinstance(e, SessionNotInitializedError):
|
|
220
|
+
Output.error(str(e))
|
|
221
|
+
raise typer.Exit(EXIT_BROWSER_ERROR) from e
|
|
199
222
|
if isinstance(e, NavigationError):
|
|
200
|
-
Output.error(
|
|
223
|
+
Output.error(str(e))
|
|
201
224
|
raise typer.Exit(EXIT_BROWSER_ERROR) from e
|
|
202
225
|
if isinstance(e, WaitTimeoutError):
|
|
203
|
-
Output.error(
|
|
226
|
+
Output.error(str(e))
|
|
204
227
|
raise typer.Exit(EXIT_BROWSER_ERROR) from e
|
|
205
228
|
if isinstance(e, ElementNotFoundError):
|
|
206
|
-
Output.error(
|
|
229
|
+
Output.error(str(e))
|
|
207
230
|
raise typer.Exit(EXIT_BROWSER_ERROR) from e
|
|
208
231
|
if isinstance(e, MultiConfigError):
|
|
209
|
-
Output.error(
|
|
232
|
+
Output.error(
|
|
233
|
+
f"Invalid multi config: {e}\n"
|
|
234
|
+
"Hint: Run `wavexis multi <config> --dry-run` to validate the config."
|
|
235
|
+
)
|
|
210
236
|
raise typer.Exit(EXIT_CONFIG_ERROR) from e
|
|
211
237
|
if isinstance(e, WavexisError):
|
|
212
238
|
Output.error(str(e))
|
|
@@ -19,6 +19,7 @@ from wavexis.cli._shared import (
|
|
|
19
19
|
_echo,
|
|
20
20
|
_get_backend,
|
|
21
21
|
_handle_error,
|
|
22
|
+
_progress,
|
|
22
23
|
_run_async,
|
|
23
24
|
app,
|
|
24
25
|
)
|
|
@@ -118,10 +119,20 @@ async def _multi(config_path: Any, parallel: bool = False) -> list[Any]:
|
|
|
118
119
|
from wavexis.multi import execute_actions, parse_yaml
|
|
119
120
|
|
|
120
121
|
actions = parse_yaml(config_path)
|
|
122
|
+
total = len(actions)
|
|
123
|
+
_echo(f"Executing {total} action(s)…")
|
|
121
124
|
backend = _get_backend()
|
|
122
125
|
await backend.launch(_browser_options())
|
|
123
126
|
try:
|
|
124
|
-
|
|
127
|
+
results: list[Any] = []
|
|
128
|
+
if parallel:
|
|
129
|
+
results = await execute_actions(actions, backend, parallel=True)
|
|
130
|
+
else:
|
|
131
|
+
for i, action in enumerate(actions):
|
|
132
|
+
_progress(i + 1, total, str(action))
|
|
133
|
+
result = await execute_actions([action], backend, parallel=False)
|
|
134
|
+
results.extend(result)
|
|
135
|
+
return results
|
|
125
136
|
finally:
|
|
126
137
|
await backend.close()
|
|
127
138
|
|
|
@@ -215,13 +226,22 @@ async def _batch(
|
|
|
215
226
|
List of results (or exceptions) in the same order as urls.
|
|
216
227
|
"""
|
|
217
228
|
semaphore = asyncio.Semaphore(parallel)
|
|
229
|
+
total = len(urls)
|
|
230
|
+
completed = 0
|
|
231
|
+
lock = asyncio.Lock()
|
|
218
232
|
|
|
219
233
|
async def _run_one(url: str) -> Any:
|
|
234
|
+
nonlocal completed
|
|
220
235
|
async with semaphore:
|
|
221
236
|
try:
|
|
222
|
-
|
|
237
|
+
result = await _batch_single(url, action, out_dir, expression)
|
|
238
|
+
return result
|
|
223
239
|
except (WavexisError, OSError) as exc:
|
|
224
240
|
return exc
|
|
241
|
+
finally:
|
|
242
|
+
async with lock:
|
|
243
|
+
completed += 1
|
|
244
|
+
_progress(completed, total, url)
|
|
225
245
|
|
|
226
246
|
tasks = [_run_one(u) for u in urls]
|
|
227
247
|
return await asyncio.gather(*tasks)
|
|
@@ -41,6 +41,21 @@ class BackendNotSupportedError(WavexisError):
|
|
|
41
41
|
class SessionNotInitializedError(WavexisError):
|
|
42
42
|
"""Raised when a backend method is called before launch()."""
|
|
43
43
|
|
|
44
|
+
def __init__(self, msg: str | None = None) -> None:
|
|
45
|
+
"""Initialize the error.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
msg: Optional custom message.
|
|
49
|
+
"""
|
|
50
|
+
if msg:
|
|
51
|
+
super().__init__(msg)
|
|
52
|
+
else:
|
|
53
|
+
super().__init__(
|
|
54
|
+
"Browser session not initialized. Call launch() first.\n"
|
|
55
|
+
"Hint: This usually means the browser failed to start. "
|
|
56
|
+
"Try --headed to see the browser window, or check if Chromium is installed."
|
|
57
|
+
)
|
|
58
|
+
|
|
44
59
|
|
|
45
60
|
class NavigationError(WavexisError):
|
|
46
61
|
"""Raised when navigation fails or times out."""
|
|
@@ -52,7 +67,11 @@ class NavigationError(WavexisError):
|
|
|
52
67
|
url: The URL that failed to navigate to.
|
|
53
68
|
reason: The failure reason.
|
|
54
69
|
"""
|
|
55
|
-
super().__init__(
|
|
70
|
+
super().__init__(
|
|
71
|
+
f"Navigation to '{url}' failed: {reason}\n"
|
|
72
|
+
f"Hint: Check the URL is reachable, try --timeout 60000, "
|
|
73
|
+
f"or use --proxy if behind a firewall."
|
|
74
|
+
)
|
|
56
75
|
|
|
57
76
|
|
|
58
77
|
class WaitTimeoutError(WavexisError):
|
|
@@ -66,7 +85,9 @@ class WaitTimeoutError(WavexisError):
|
|
|
66
85
|
timeout_ms: The timeout duration in milliseconds.
|
|
67
86
|
"""
|
|
68
87
|
super().__init__(
|
|
69
|
-
f"Wait strategy '{strategy}' timed out after {timeout_ms}ms"
|
|
88
|
+
f"Wait strategy '{strategy}' timed out after {timeout_ms}ms\n"
|
|
89
|
+
f"Hint: Increase timeout with --timeout {max(timeout_ms * 2, 60000)}, "
|
|
90
|
+
f"or use a different wait strategy (load, domcontentloaded, networkidle)."
|
|
70
91
|
)
|
|
71
92
|
|
|
72
93
|
|
|
@@ -79,7 +100,11 @@ class ElementNotFoundError(WavexisError):
|
|
|
79
100
|
Args:
|
|
80
101
|
selector: The CSS selector that matched no elements.
|
|
81
102
|
"""
|
|
82
|
-
super().__init__(
|
|
103
|
+
super().__init__(
|
|
104
|
+
f"No element found matching selector: {selector}\n"
|
|
105
|
+
f"Hint: Verify the selector with `wavexis dom --selector '{selector}'`, "
|
|
106
|
+
f"or use `wavexis screenshot` to inspect the page visually."
|
|
107
|
+
)
|
|
83
108
|
|
|
84
109
|
|
|
85
110
|
class ActionError(WavexisError):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|