wavexis 2.0.2__tar.gz → 2.0.3__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.0.2 → wavexis-2.0.3}/CHANGELOG.md +10 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/PKG-INFO +1 -1
- {wavexis-2.0.2 → wavexis-2.0.3}/pyproject.toml +1 -1
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_global_options.py +28 -34
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_interactions_crawl.py +6 -1
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_new_features.py +3 -2
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/__init__.py +1 -1
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/crawl.py +4 -3
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/eval.py +4 -1
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/form.py +3 -2
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/record.py +3 -2
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/scrape.py +4 -1
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/screencast.py +7 -5
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/session.py +2 -1
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/backend/cdp.py +10 -14
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/cli/app.py +71 -44
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/plugins.py +1 -1
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/repl.py +8 -3
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/serve.py +1 -1
- {wavexis-2.0.2 → wavexis-2.0.3}/.dockerignore +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/.github/workflows/ci.yml +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/.github/workflows/docs.yml +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/.github/workflows/release.yml +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/.gitignore +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/CODE_OF_CONDUCT.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/CONTRIBUTING.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/Dockerfile +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/LICENSE +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/README.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/SECURITY.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/api/actions.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/api/backends.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/api/cli.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/api/config.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/api/exceptions.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/api/multi.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/api/plugins.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/api/serve.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/cookbook/auth.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/cookbook/ci-cd.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/cookbook/css.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/cookbook/debug.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/cookbook/record-replay.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/cookbook/scraping.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/cookbook/screenshots.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/cookbook/serve-mode.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/guide/assert.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/guide/backends.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/guide/commands.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/guide/init.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/guide/installation.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/guide/multi.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/guide/perf.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/guide/plugins.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/guide/quickstart.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/guide/raw.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/guide/repl.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/guide/troubleshooting.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/docs/index.md +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/mkdocs.yml +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/stubs/bidiwave/__init__.pyi +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/stubs/cdpwave/__init__.pyi +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/__init__.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/conftest.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/__init__.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_a11y.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_animation.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_backend_selection.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_browser.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_console.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_css.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_debug.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_dialog.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_dom.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_dom_snapshot.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_emulation.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_emulation_advanced.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_eval.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_har.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_input.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_media.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_multi.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_navigate.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_network.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_network_advanced.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_overlay.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_pdf.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_perf.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_permissions.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_raw.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_record.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_scrape.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_screenshot.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_security.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_serve.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_service_worker.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_storage.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_tabs.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_webaudio.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_webauthn.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/__init__.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_abstract_backend_phase5.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_actions.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_actions_phase5.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_animation_action.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_assert_multi.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_auth.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_backend_manager.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_bidi_backend.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_bidi_phase5.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_bluetooth_action.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_cast_action.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_cli_phase5.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_config.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_config_phase5.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_cookies_headers_actions.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_css_action.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_debug_action.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_dom_snapshot_action.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_download_screencast.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_emulation_action.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_error_handling.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_exceptions.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_format_record.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_init.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_input_action.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_manager.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_media_action.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_multi.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_multi_improvements.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_output.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_overlay_action.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_perf_action.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_perf_console.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_plugins.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_raw.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_record.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_repl.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_serve.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_service_worker_action.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_storage_action.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_webaudio_action.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_webauthn_action.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/__main__.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/__init__.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/accessibility.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/animation.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/base.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/bluetooth.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/browser.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/cast.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/console.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/cookies.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/css.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/debug.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/dialog.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/dom.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/dom_snapshot.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/download.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/emulation.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/extract.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/har.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/headers.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/input.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/lighthouse.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/media.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/multi.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/navigate.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/network.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/overlay.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/pdf.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/performance.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/permissions.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/screenshot.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/security.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/service_worker.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/storage.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/tabs.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/wait.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/webaudio.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/webauthn.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/websocket.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/auth.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/backend/__init__.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/backend/base.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/backend/bidi.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/backend/manager.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/cli/__init__.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/config.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/exceptions.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/init.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/multi.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/output.py +0 -0
- {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/record.py +0 -0
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to wavexis are documented in this file.
|
|
4
4
|
|
|
5
|
+
## v2.0.3 — 2026-07-06
|
|
6
|
+
|
|
7
|
+
### Refactored
|
|
8
|
+
|
|
9
|
+
- Removed all `# noqa` lint suppressions — converted Typer argument annotations to `Annotated` types (B008), replaced sync file I/O in async functions with `asyncio.to_thread` (ASYNC230, ASYNC240), fixed mutable default argument (B006)
|
|
10
|
+
- Narrowed broad `except Exception` clauses to specific exception types (`WavexisError`, `OSError`, `json.JSONDecodeError`, `KeyError`, `TypeError`, `TimeoutError`, `zipfile.BadZipFile`, `yaml.YAMLError`) across `cli/app.py`, `repl.py`, `serve.py`, `plugins.py`, `actions/record.py`, `actions/form.py`, `actions/crawl.py`, `actions/session.py`, `backend/cdp.py`
|
|
11
|
+
- Replaced 6 module-level mutable globals (`_preferred_backend`, `_verbose`, `_quiet`, `_headless`, `_timeout`, `_proxy`) with `CLIContext` dataclass + `contextvars.ContextVar` for thread-safe state management
|
|
12
|
+
- Removed unused `os` import in `backend/cdp.py` download handler
|
|
13
|
+
- Replaced local `import json as _json` with top-level `json` import in `backend/cdp.py`
|
|
14
|
+
|
|
5
15
|
## v2.0.2 — 2026-07-06
|
|
6
16
|
|
|
7
17
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wavexis
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.3
|
|
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
|
|
@@ -15,6 +15,13 @@ pytestmark = pytest.mark.unit
|
|
|
15
15
|
_cli = sys.modules["wavexis.cli.app"]
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
def _fresh_ctx() -> _cli.CLIContext:
|
|
19
|
+
"""Create a fresh CLIContext and set it as the current context."""
|
|
20
|
+
ctx = _cli.CLIContext()
|
|
21
|
+
_cli._ctx.set(ctx)
|
|
22
|
+
return ctx
|
|
23
|
+
|
|
24
|
+
|
|
18
25
|
class TestBrowserOptions:
|
|
19
26
|
"""Tests for BrowserOptions with proxy and timeout fields."""
|
|
20
27
|
|
|
@@ -45,32 +52,27 @@ class TestBrowserOptionsHelper:
|
|
|
45
52
|
"""Tests for _browser_options() CLI helper."""
|
|
46
53
|
|
|
47
54
|
def test_default_options(self) -> None:
|
|
48
|
-
|
|
49
|
-
_cli._timeout = 30000
|
|
50
|
-
_cli._proxy = None
|
|
55
|
+
_fresh_ctx()
|
|
51
56
|
opts = _cli._browser_options()
|
|
52
57
|
assert opts.headless is True
|
|
53
58
|
assert opts.timeout == 30000
|
|
54
59
|
assert opts.proxy is None
|
|
55
60
|
|
|
56
61
|
def test_headed_options(self) -> None:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
_cli._proxy = None
|
|
62
|
+
ctx = _fresh_ctx()
|
|
63
|
+
ctx.headless = False
|
|
60
64
|
opts = _cli._browser_options()
|
|
61
65
|
assert opts.headless is False
|
|
62
66
|
|
|
63
67
|
def test_proxy_options(self) -> None:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
_cli._proxy = "http://proxy:8080"
|
|
68
|
+
ctx = _fresh_ctx()
|
|
69
|
+
ctx.proxy = "http://proxy:8080"
|
|
67
70
|
opts = _cli._browser_options()
|
|
68
71
|
assert opts.proxy == "http://proxy:8080"
|
|
69
72
|
|
|
70
73
|
def test_timeout_options(self) -> None:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
_cli._proxy = None
|
|
74
|
+
ctx = _fresh_ctx()
|
|
75
|
+
ctx.timeout = 60000
|
|
74
76
|
opts = _cli._browser_options()
|
|
75
77
|
assert opts.timeout == 60000
|
|
76
78
|
|
|
@@ -80,15 +82,12 @@ class TestLoadGlobalConfig:
|
|
|
80
82
|
|
|
81
83
|
def test_no_config_file(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
82
84
|
monkeypatch.setattr(Path, "home", lambda: Path("/nonexistent_home_12345"))
|
|
83
|
-
|
|
84
|
-
_cli._headless = True
|
|
85
|
-
_cli._timeout = 30000
|
|
86
|
-
_cli._proxy = None
|
|
85
|
+
ctx = _fresh_ctx()
|
|
87
86
|
_cli._load_global_config()
|
|
88
|
-
assert
|
|
89
|
-
assert
|
|
90
|
-
assert
|
|
91
|
-
assert
|
|
87
|
+
assert ctx.preferred_backend is None
|
|
88
|
+
assert ctx.headless is True
|
|
89
|
+
assert ctx.timeout == 30000
|
|
90
|
+
assert ctx.proxy is None
|
|
92
91
|
|
|
93
92
|
def test_loads_config(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
94
93
|
config_dir = tmp_path / ".wavexis"
|
|
@@ -100,16 +99,13 @@ class TestLoadGlobalConfig:
|
|
|
100
99
|
)
|
|
101
100
|
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
|
102
101
|
|
|
103
|
-
|
|
104
|
-
_cli._headless = True
|
|
105
|
-
_cli._timeout = 30000
|
|
106
|
-
_cli._proxy = None
|
|
102
|
+
ctx = _fresh_ctx()
|
|
107
103
|
_cli._load_global_config()
|
|
108
104
|
|
|
109
|
-
assert
|
|
110
|
-
assert
|
|
111
|
-
assert
|
|
112
|
-
assert
|
|
105
|
+
assert ctx.preferred_backend == "bidi"
|
|
106
|
+
assert ctx.headless is False
|
|
107
|
+
assert ctx.timeout == 60000
|
|
108
|
+
assert ctx.proxy == "http://proxy:9090"
|
|
113
109
|
|
|
114
110
|
def test_cli_overrides_config(
|
|
115
111
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
@@ -120,14 +116,12 @@ class TestLoadGlobalConfig:
|
|
|
120
116
|
config_file.write_text("backend: bidi\ntimeout: 60000\n", encoding="utf-8")
|
|
121
117
|
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
|
122
118
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
_cli._timeout = 30000
|
|
126
|
-
_cli._proxy = None
|
|
119
|
+
ctx = _fresh_ctx()
|
|
120
|
+
ctx.preferred_backend = "cdp"
|
|
127
121
|
_cli._load_global_config()
|
|
128
122
|
|
|
129
|
-
assert
|
|
130
|
-
assert
|
|
123
|
+
assert ctx.preferred_backend == "cdp"
|
|
124
|
+
assert ctx.timeout == 60000
|
|
131
125
|
|
|
132
126
|
|
|
133
127
|
class TestConfigCommand:
|
|
@@ -9,6 +9,7 @@ import pytest
|
|
|
9
9
|
from wavexis.actions.crawl import CrawlAction, CrawlParams
|
|
10
10
|
from wavexis.actions.input import InputAction
|
|
11
11
|
from wavexis.config import InputParams, WaitStrategy
|
|
12
|
+
from wavexis.exceptions import WavexisError
|
|
12
13
|
|
|
13
14
|
pytestmark = pytest.mark.unit
|
|
14
15
|
|
|
@@ -182,6 +183,8 @@ class TestCrawlAction:
|
|
|
182
183
|
side_effect=[
|
|
183
184
|
"Page 1",
|
|
184
185
|
["https://example.com/about", "https://other.com/page"],
|
|
186
|
+
"About",
|
|
187
|
+
[],
|
|
185
188
|
]
|
|
186
189
|
)
|
|
187
190
|
|
|
@@ -205,6 +208,8 @@ class TestCrawlAction:
|
|
|
205
208
|
side_effect=[
|
|
206
209
|
"Blog",
|
|
207
210
|
["https://example.com/blog/post1", "https://example.com/about"],
|
|
211
|
+
"Blog Post 1",
|
|
212
|
+
[],
|
|
208
213
|
]
|
|
209
214
|
)
|
|
210
215
|
|
|
@@ -242,7 +247,7 @@ class TestCrawlAction:
|
|
|
242
247
|
|
|
243
248
|
async def test_crawl_handles_navigation_error(self) -> None:
|
|
244
249
|
backend = MagicMock()
|
|
245
|
-
backend.navigate = AsyncMock(side_effect=
|
|
250
|
+
backend.navigate = AsyncMock(side_effect=WavexisError("Network error"))
|
|
246
251
|
backend.eval = AsyncMock()
|
|
247
252
|
|
|
248
253
|
params = CrawlParams(
|
|
@@ -20,6 +20,7 @@ from wavexis.actions.session import (
|
|
|
20
20
|
)
|
|
21
21
|
from wavexis.actions.websocket import WebSocketInterceptAction, WebSocketParams
|
|
22
22
|
from wavexis.config import WaitStrategy
|
|
23
|
+
from wavexis.exceptions import WavexisError
|
|
23
24
|
|
|
24
25
|
pytestmark = pytest.mark.unit
|
|
25
26
|
|
|
@@ -68,7 +69,7 @@ class TestSessionSave:
|
|
|
68
69
|
backend.close = AsyncMock()
|
|
69
70
|
backend.get_cookies = AsyncMock(return_value=[])
|
|
70
71
|
backend.storage_list = AsyncMock(side_effect=[{}, {}])
|
|
71
|
-
backend.eval = AsyncMock(side_effect=
|
|
72
|
+
backend.eval = AsyncMock(side_effect=WavexisError("no page"))
|
|
72
73
|
|
|
73
74
|
with tempfile.NamedTemporaryFile(
|
|
74
75
|
suffix=".json", delete=False, mode="w"
|
|
@@ -273,7 +274,7 @@ class TestFormAction:
|
|
|
273
274
|
backend.launch = AsyncMock()
|
|
274
275
|
backend.close = AsyncMock()
|
|
275
276
|
backend.navigate = AsyncMock()
|
|
276
|
-
backend.fill = AsyncMock(side_effect=[None,
|
|
277
|
+
backend.fill = AsyncMock(side_effect=[None, WavexisError("not found")])
|
|
277
278
|
|
|
278
279
|
params = FormParams(
|
|
279
280
|
url="https://app.com/register",
|
|
@@ -11,6 +11,7 @@ from urllib.parse import urljoin, urlparse
|
|
|
11
11
|
from wavexis.actions.base import BaseAction
|
|
12
12
|
from wavexis.backend.base import AbstractBackend
|
|
13
13
|
from wavexis.config import WaitStrategy
|
|
14
|
+
from wavexis.exceptions import WavexisError
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
@dataclass
|
|
@@ -63,16 +64,16 @@ class CrawlAction(BaseAction[CrawlParams, list[dict[str, Any]]]):
|
|
|
63
64
|
|
|
64
65
|
try:
|
|
65
66
|
await backend.navigate(url, self.params.wait)
|
|
66
|
-
except
|
|
67
|
+
except WavexisError:
|
|
67
68
|
continue
|
|
68
69
|
|
|
69
70
|
title = ""
|
|
70
71
|
links: list[str] = []
|
|
71
|
-
with contextlib.suppress(
|
|
72
|
+
with contextlib.suppress(WavexisError):
|
|
72
73
|
title = await backend.eval(
|
|
73
74
|
"document.title", await_promise=False
|
|
74
75
|
)
|
|
75
|
-
with contextlib.suppress(
|
|
76
|
+
with contextlib.suppress(WavexisError):
|
|
76
77
|
raw_links = await backend.eval(
|
|
77
78
|
"Array.from(document.querySelectorAll('a[href]')).map(a => a.href)",
|
|
78
79
|
await_promise=False,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
@@ -31,6 +32,8 @@ class EvalAction(BaseAction[EvalParams, Any]):
|
|
|
31
32
|
|
|
32
33
|
expression = params.expression
|
|
33
34
|
if params.file:
|
|
34
|
-
expression =
|
|
35
|
+
expression = await asyncio.to_thread(
|
|
36
|
+
lambda: Path(params.file).read_text(encoding="utf-8")
|
|
37
|
+
)
|
|
35
38
|
|
|
36
39
|
return await backend.eval(expression, await_promise=params.await_promise)
|
|
@@ -8,6 +8,7 @@ from typing import Any
|
|
|
8
8
|
from wavexis.actions.base import BaseAction
|
|
9
9
|
from wavexis.backend.base import AbstractBackend
|
|
10
10
|
from wavexis.config import BrowserOptions, WaitStrategy
|
|
11
|
+
from wavexis.exceptions import WavexisError
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
@dataclass
|
|
@@ -52,7 +53,7 @@ class FormAction(BaseAction[FormParams, dict[str, Any]]):
|
|
|
52
53
|
try:
|
|
53
54
|
await backend.fill(selector, value)
|
|
54
55
|
filled += 1
|
|
55
|
-
except
|
|
56
|
+
except WavexisError:
|
|
56
57
|
pass
|
|
57
58
|
|
|
58
59
|
submitted = False
|
|
@@ -60,7 +61,7 @@ class FormAction(BaseAction[FormParams, dict[str, Any]]):
|
|
|
60
61
|
try:
|
|
61
62
|
await backend.click(self.params.submit)
|
|
62
63
|
submitted = True
|
|
63
|
-
except
|
|
64
|
+
except WavexisError:
|
|
64
65
|
pass
|
|
65
66
|
|
|
66
67
|
return {
|
|
@@ -11,6 +11,7 @@ import yaml
|
|
|
11
11
|
|
|
12
12
|
from wavexis.backend.base import AbstractBackend
|
|
13
13
|
from wavexis.config import BrowserOptions, WaitStrategy
|
|
14
|
+
from wavexis.exceptions import WavexisError
|
|
14
15
|
|
|
15
16
|
_RECORD_SCRIPT = """
|
|
16
17
|
(function() {
|
|
@@ -142,10 +143,10 @@ async def record_session(
|
|
|
142
143
|
events = json.loads(raw)
|
|
143
144
|
elif isinstance(raw, list):
|
|
144
145
|
events = raw
|
|
145
|
-
except
|
|
146
|
+
except (json.JSONDecodeError, TypeError, WavexisError):
|
|
146
147
|
pass
|
|
147
148
|
|
|
148
|
-
with contextlib.suppress(
|
|
149
|
+
with contextlib.suppress(WavexisError):
|
|
149
150
|
await backend.close()
|
|
150
151
|
|
|
151
152
|
return events_to_yaml(events, url)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
@@ -32,7 +33,9 @@ class ScrapeAction(BaseAction[ScrapeParams, list[dict[str, Any]]]):
|
|
|
32
33
|
expression = params.expression
|
|
33
34
|
|
|
34
35
|
if params.file:
|
|
35
|
-
expression =
|
|
36
|
+
expression = await asyncio.to_thread(
|
|
37
|
+
lambda: Path(params.file).read_text(encoding="utf-8")
|
|
38
|
+
)
|
|
36
39
|
|
|
37
40
|
if not expression:
|
|
38
41
|
expression = "document.title"
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import asyncio
|
|
6
|
+
from pathlib import Path
|
|
6
7
|
|
|
7
8
|
from wavexis.actions.base import BaseAction
|
|
8
9
|
from wavexis.backend.base import AbstractBackend
|
|
@@ -38,13 +39,14 @@ class ScreencastAction(BaseAction[ScreencastParams, list[str]]):
|
|
|
38
39
|
finally:
|
|
39
40
|
await backend.close()
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
await asyncio.to_thread(
|
|
43
|
+
lambda: Path(self._output_dir).mkdir(parents=True, exist_ok=True)
|
|
44
|
+
)
|
|
42
45
|
saved: list[str] = []
|
|
43
46
|
for i, frame in enumerate(frames):
|
|
44
47
|
ext = "png" if self.params.format == "png" else "jpg"
|
|
45
48
|
fname = f"frame_{i:05d}.{ext}"
|
|
46
|
-
fpath =
|
|
47
|
-
|
|
48
|
-
f.write(frame)
|
|
49
|
+
fpath = str(Path(self._output_dir) / fname)
|
|
50
|
+
await asyncio.to_thread(Path(fpath).write_bytes, frame)
|
|
49
51
|
saved.append(fpath)
|
|
50
52
|
return saved
|
|
@@ -11,6 +11,7 @@ from typing import Any
|
|
|
11
11
|
from wavexis.actions.base import BaseAction
|
|
12
12
|
from wavexis.backend.base import AbstractBackend
|
|
13
13
|
from wavexis.config import CookieParams
|
|
14
|
+
from wavexis.exceptions import WavexisError
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
@dataclass
|
|
@@ -69,7 +70,7 @@ class SessionSaveAction(BaseAction[Path, str]):
|
|
|
69
70
|
local_storage = await backend.storage_list("local")
|
|
70
71
|
session_storage = await backend.storage_list("session")
|
|
71
72
|
url = ""
|
|
72
|
-
with contextlib.suppress(
|
|
73
|
+
with contextlib.suppress(WavexisError):
|
|
73
74
|
url = await backend.eval("window.location.href", await_promise=False)
|
|
74
75
|
|
|
75
76
|
data = SessionData(
|
|
@@ -4,7 +4,9 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import base64
|
|
7
|
+
import json
|
|
7
8
|
import time
|
|
9
|
+
from pathlib import Path
|
|
8
10
|
from typing import Any
|
|
9
11
|
|
|
10
12
|
from wavexis.backend.base import AbstractBackend
|
|
@@ -1267,11 +1269,10 @@ class CDPBackend(AbstractBackend):
|
|
|
1267
1269
|
"""
|
|
1268
1270
|
if self._session is None:
|
|
1269
1271
|
raise NavigationError("", "Session not initialized.")
|
|
1270
|
-
import json as _json
|
|
1271
1272
|
|
|
1272
1273
|
body = response.get("body", "")
|
|
1273
1274
|
if isinstance(body, (dict, list)):
|
|
1274
|
-
body =
|
|
1275
|
+
body = json.dumps(body)
|
|
1275
1276
|
body_b64 = base64.b64encode(body.encode("utf-8")).decode("ascii")
|
|
1276
1277
|
|
|
1277
1278
|
fulfilled: list[bool] = [False]
|
|
@@ -1374,7 +1375,6 @@ class CDPBackend(AbstractBackend):
|
|
|
1374
1375
|
"""
|
|
1375
1376
|
if self._session is None:
|
|
1376
1377
|
raise NavigationError("", "Session not initialized.")
|
|
1377
|
-
import os
|
|
1378
1378
|
import tempfile
|
|
1379
1379
|
|
|
1380
1380
|
download_dir = tempfile.mkdtemp()
|
|
@@ -1385,11 +1385,9 @@ class CDPBackend(AbstractBackend):
|
|
|
1385
1385
|
|
|
1386
1386
|
await asyncio.sleep(2)
|
|
1387
1387
|
|
|
1388
|
-
for
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
with open(fpath, "rb") as f: # noqa: ASYNC230
|
|
1392
|
-
return f.read()
|
|
1388
|
+
for fpath in await asyncio.to_thread(list, Path(download_dir).iterdir):
|
|
1389
|
+
if await asyncio.to_thread(Path(fpath).is_file):
|
|
1390
|
+
return await asyncio.to_thread(Path(fpath).read_bytes)
|
|
1393
1391
|
|
|
1394
1392
|
return b""
|
|
1395
1393
|
|
|
@@ -1585,10 +1583,8 @@ class CDPBackend(AbstractBackend):
|
|
|
1585
1583
|
zf = zipfile.ZipFile(io.BytesIO(raw))
|
|
1586
1584
|
for name in zf.namelist():
|
|
1587
1585
|
content = zf.read(name).decode("utf-8", errors="replace")
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
trace_events.extend(_json.loads(content).get("traceEvents", []))
|
|
1591
|
-
except Exception:
|
|
1586
|
+
trace_events.extend(json.loads(content).get("traceEvents", []))
|
|
1587
|
+
except (zipfile.BadZipFile, json.JSONDecodeError, KeyError, ValueError):
|
|
1592
1588
|
trace_events.append({"raw_size": len(raw)})
|
|
1593
1589
|
else:
|
|
1594
1590
|
trace_events.append({"error": "No stream handle in tracingComplete"})
|
|
@@ -2323,7 +2319,7 @@ class CDPBackend(AbstractBackend):
|
|
|
2323
2319
|
)
|
|
2324
2320
|
for ev in events:
|
|
2325
2321
|
players.append(dict(ev.get("player", ev)))
|
|
2326
|
-
except
|
|
2322
|
+
except (TimeoutError, KeyError, TypeError):
|
|
2327
2323
|
pass
|
|
2328
2324
|
return players
|
|
2329
2325
|
|
|
@@ -2340,7 +2336,7 @@ class CDPBackend(AbstractBackend):
|
|
|
2340
2336
|
for ev in events:
|
|
2341
2337
|
if ev.get("playerId") == player_id:
|
|
2342
2338
|
messages.append(dict(ev))
|
|
2343
|
-
except
|
|
2339
|
+
except (TimeoutError, KeyError, TypeError):
|
|
2344
2340
|
pass
|
|
2345
2341
|
return messages
|
|
2346
2342
|
|