wavexis 2.2.2__tar.gz → 2.2.4__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.2 → wavexis-2.2.4}/CHANGELOG.md +15 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/PKG-INFO +1 -1
- {wavexis-2.2.2 → wavexis-2.2.4}/pyproject.toml +1 -1
- wavexis-2.2.4/tests/unit/test_cleanup.py +53 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/__init__.py +1 -1
- wavexis-2.2.4/wavexis/cleanup.py +96 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/cli/_capture.py +18 -9
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/cli/_shared.py +25 -2
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/cli/_workflow.py +22 -2
- {wavexis-2.2.2 → wavexis-2.2.4}/.dockerignore +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/.github/workflows/ci.yml +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/.github/workflows/docs.yml +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/.github/workflows/release.yml +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/.gitignore +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/CODE_OF_CONDUCT.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/CONTRIBUTING.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/Dockerfile +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/LICENSE +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/README.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/SECURITY.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/api/actions.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/api/backends.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/api/cli.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/api/config.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/api/exceptions.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/api/multi.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/api/plugins.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/api/serve.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/cookbook/auth.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/cookbook/ci-cd.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/cookbook/css.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/cookbook/debug.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/cookbook/record-replay.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/cookbook/scraping.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/cookbook/screenshots.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/cookbook/serve-mode.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/guide/assert.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/guide/backends.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/guide/commands.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/guide/init.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/guide/installation.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/guide/multi.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/guide/perf.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/guide/plugins.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/guide/quickstart.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/guide/raw.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/guide/repl.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/guide/troubleshooting.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/docs/index.md +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/mkdocs.yml +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/stubs/bidiwave/__init__.pyi +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/stubs/cdpwave/__init__.pyi +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/__init__.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/conftest.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/__init__.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_a11y.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_animation.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_backend_selection.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_browser.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_console.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_css.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_debug.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_dialog.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_dom.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_dom_snapshot.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_emulation.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_emulation_advanced.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_eval.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_har.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_input.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_media.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_multi.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_navigate.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_network.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_network_advanced.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_overlay.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_pdf.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_perf.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_permissions.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_raw.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_record.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_scrape.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_screenshot.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_security.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_serve.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_service_worker.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_storage.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_tabs.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_webaudio.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/integration/test_webauthn.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/__init__.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_abstract_backend_phase5.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_actions.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_actions_phase5.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_animation_action.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_assert_multi.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_auth.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_backend_manager.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_bidi_backend.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_bidi_phase5.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_bluetooth_action.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_cast_action.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_cli_phase5.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_config.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_config_phase5.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_cookies_headers_actions.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_css_action.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_debug_action.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_dom_snapshot_action.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_download_screencast.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_emulation_action.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_error_handling.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_exceptions.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_format_record.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_global_options.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_init.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_input_action.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_interactions_crawl.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_manager.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_media_action.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_multi.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_multi_improvements.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_new_features.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_output.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_overlay_action.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_perf_action.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_perf_console.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_plugins.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_raw.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_record.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_repl.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_serve.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_service_worker_action.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_storage_action.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_webaudio_action.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/tests/unit/test_webauthn_action.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/__main__.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/__init__.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/accessibility.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/animation.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/base.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/bluetooth.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/browser.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/cast.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/console.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/cookies.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/crawl.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/css.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/debug.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/dialog.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/dom.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/dom_snapshot.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/download.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/emulation.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/eval.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/extract.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/form.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/har.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/headers.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/input.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/lighthouse.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/media.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/multi.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/navigate.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/network.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/overlay.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/pdf.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/performance.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/permissions.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/record.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/scrape.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/screencast.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/screenshot.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/security.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/service_worker.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/session.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/storage.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/tabs.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/wait.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/webaudio.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/webauthn.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/actions/websocket.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/auth.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/backend/__init__.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/backend/base.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/backend/bidi.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/backend/cdp.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/backend/manager.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/cli/__init__.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/cli/_advanced.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/cli/_config.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/cli/_debug.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/cli/_emulation.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/cli/_experimental.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/cli/_input.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/cli/_navigation.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/cli/_network.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/cli/_perf.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/cli/_serve.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/cli/_session.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/cli/app.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/config.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/exceptions.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/init.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/multi.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/output.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/plugins.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/record.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/repl.py +0 -0
- {wavexis-2.2.2 → wavexis-2.2.4}/wavexis/serve.py +0 -0
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to wavexis are documented in this file.
|
|
4
4
|
|
|
5
|
+
## v2.2.4 — 2026-07-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Progress reporting for long-running operations (`batch`, `multi`, `scrape`)
|
|
10
|
+
- `_progress(current, total, label)` helper shows `[n/total] — label` unless `--quiet`
|
|
11
|
+
|
|
12
|
+
## v2.2.3 — 2026-07-07
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Resource cleanup module (`wavexis.cleanup`) with `atexit` and signal handlers
|
|
17
|
+
- Backends are automatically registered for cleanup on crash or signal (SIGINT, SIGTERM, SIGBREAK)
|
|
18
|
+
- Orphaned browser processes are closed even when the process is killed unexpectedly
|
|
19
|
+
|
|
5
20
|
## v2.2.2 — 2026-07-07
|
|
6
21
|
|
|
7
22
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wavexis
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.4
|
|
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
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Unit tests for resource cleanup module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
import wavexis.cleanup as cleanup_mod
|
|
10
|
+
from wavexis.cleanup import (
|
|
11
|
+
_cleanup_sync,
|
|
12
|
+
register_backend,
|
|
13
|
+
unregister_backend,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.mark.unit
|
|
18
|
+
class TestCleanup:
|
|
19
|
+
"""Tests for browser cleanup on crash."""
|
|
20
|
+
|
|
21
|
+
def setup_method(self) -> None:
|
|
22
|
+
"""Reset cleanup state before each test."""
|
|
23
|
+
cleanup_mod._cleanup_done = False
|
|
24
|
+
cleanup_mod._registered_backends.clear()
|
|
25
|
+
|
|
26
|
+
def test_register_and_unregister(self) -> None:
|
|
27
|
+
backend = MagicMock()
|
|
28
|
+
register_backend(backend)
|
|
29
|
+
unregister_backend(backend)
|
|
30
|
+
|
|
31
|
+
def test_cleanup_with_no_backends(self) -> None:
|
|
32
|
+
_cleanup_sync()
|
|
33
|
+
|
|
34
|
+
def test_cleanup_closes_registered_backends(self) -> None:
|
|
35
|
+
backend = MagicMock()
|
|
36
|
+
backend.close = AsyncMock()
|
|
37
|
+
register_backend(backend)
|
|
38
|
+
_cleanup_sync()
|
|
39
|
+
backend.close.assert_called_once()
|
|
40
|
+
|
|
41
|
+
def test_cleanup_is_idempotent(self) -> None:
|
|
42
|
+
backend = MagicMock()
|
|
43
|
+
backend.close = AsyncMock()
|
|
44
|
+
register_backend(backend)
|
|
45
|
+
_cleanup_sync()
|
|
46
|
+
_cleanup_sync()
|
|
47
|
+
backend.close.assert_called_once()
|
|
48
|
+
|
|
49
|
+
def test_cleanup_swallows_errors(self) -> None:
|
|
50
|
+
backend = MagicMock()
|
|
51
|
+
backend.close = AsyncMock(side_effect=RuntimeError("boom"))
|
|
52
|
+
register_backend(backend)
|
|
53
|
+
_cleanup_sync()
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Resource cleanup for orphaned browser processes.
|
|
2
|
+
|
|
3
|
+
Registers atexit and signal handlers to ensure browser backends are closed
|
|
4
|
+
even when the process crashes or is interrupted.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import atexit
|
|
10
|
+
import contextlib
|
|
11
|
+
import logging
|
|
12
|
+
import signal
|
|
13
|
+
import sys
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
_registered_backends: list[Any] = []
|
|
19
|
+
_cleanup_done = False
|
|
20
|
+
|
|
21
|
+
_SIGNAL_NAMES: dict[int, str] = {
|
|
22
|
+
signal.SIGINT: "SIGINT",
|
|
23
|
+
signal.SIGTERM: "SIGTERM",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
with contextlib.suppress(AttributeError):
|
|
27
|
+
_SIGNAL_NAMES[signal.SIGBREAK] = "SIGBREAK"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def register_backend(backend: Any) -> None:
|
|
31
|
+
"""Register a backend for cleanup on exit.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
backend: A backend instance with an async ``close()`` method.
|
|
35
|
+
"""
|
|
36
|
+
_registered_backends.append(backend)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def unregister_backend(backend: Any) -> None:
|
|
40
|
+
"""Unregister a backend after it has been closed normally.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
backend: The backend instance to remove from cleanup tracking.
|
|
44
|
+
"""
|
|
45
|
+
if backend in _registered_backends:
|
|
46
|
+
_registered_backends.remove(backend)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _cleanup_sync() -> None:
|
|
50
|
+
"""Synchronously attempt to close all registered backends.
|
|
51
|
+
|
|
52
|
+
Uses ``asyncio.run`` if there are backends to close.
|
|
53
|
+
"""
|
|
54
|
+
global _cleanup_done
|
|
55
|
+
if _cleanup_done:
|
|
56
|
+
return
|
|
57
|
+
_cleanup_done = True
|
|
58
|
+
|
|
59
|
+
if not _registered_backends:
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
import asyncio
|
|
63
|
+
|
|
64
|
+
async def _close_all() -> None:
|
|
65
|
+
for backend in _registered_backends:
|
|
66
|
+
with contextlib.suppress(Exception):
|
|
67
|
+
await backend.close()
|
|
68
|
+
|
|
69
|
+
with contextlib.suppress(Exception):
|
|
70
|
+
asyncio.run(_close_all())
|
|
71
|
+
|
|
72
|
+
_registered_backends.clear()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _signal_handler(signum: int, frame: Any) -> None:
|
|
76
|
+
"""Handle signals by cleaning up and re-raising.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
signum: Signal number received.
|
|
80
|
+
frame: Current stack frame.
|
|
81
|
+
"""
|
|
82
|
+
_cleanup_sync()
|
|
83
|
+
name = _SIGNAL_NAMES.get(signum, str(signum))
|
|
84
|
+
sys.stderr.write(f"\nwavexis: received {name}, cleaning up…\n")
|
|
85
|
+
sys.exit(128 + signum)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _setup_signal_handlers() -> None:
|
|
89
|
+
"""Register signal handlers for cleanup."""
|
|
90
|
+
for sig in _SIGNAL_NAMES:
|
|
91
|
+
with contextlib.suppress(OSError, ValueError):
|
|
92
|
+
signal.signal(sig, _signal_handler)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
atexit.register(_cleanup_sync)
|
|
96
|
+
_setup_signal_handlers()
|
|
@@ -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
|
|
|
@@ -11,6 +11,7 @@ from typing import Any
|
|
|
11
11
|
import typer
|
|
12
12
|
|
|
13
13
|
from wavexis.backend.manager import get_manager
|
|
14
|
+
from wavexis.cleanup import register_backend, unregister_backend
|
|
14
15
|
from wavexis.config import (
|
|
15
16
|
DEVICE_PRESETS,
|
|
16
17
|
BrowserOptions,
|
|
@@ -47,10 +48,12 @@ __all__ = [
|
|
|
47
48
|
"_get_ctx",
|
|
48
49
|
"_handle_error",
|
|
49
50
|
"_load_global_config",
|
|
51
|
+
"_progress",
|
|
50
52
|
"_run_async",
|
|
51
53
|
"_write_json_output",
|
|
52
54
|
"app",
|
|
53
55
|
"get_manager",
|
|
56
|
+
"unregister_backend",
|
|
54
57
|
]
|
|
55
58
|
|
|
56
59
|
EXIT_SUCCESS = 0
|
|
@@ -183,6 +186,21 @@ def _echo(msg: str) -> None:
|
|
|
183
186
|
typer.echo(msg)
|
|
184
187
|
|
|
185
188
|
|
|
189
|
+
def _progress(current: int, total: int, label: str = "") -> None:
|
|
190
|
+
"""Print a progress indicator unless quiet mode is active.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
current: Current item index (1-based).
|
|
194
|
+
total: Total number of items.
|
|
195
|
+
label: Optional label to prepend (e.g. URL or action name).
|
|
196
|
+
"""
|
|
197
|
+
ctx = _get_ctx()
|
|
198
|
+
if ctx.quiet:
|
|
199
|
+
return
|
|
200
|
+
suffix = f" — {label}" if label else ""
|
|
201
|
+
typer.echo(f"[{current}/{total}]{suffix}")
|
|
202
|
+
|
|
203
|
+
|
|
186
204
|
def _handle_error(e: Exception) -> None:
|
|
187
205
|
"""Handle a WavexisError with the correct exit code and message.
|
|
188
206
|
|
|
@@ -229,8 +247,13 @@ def _run_async(coro: Any) -> Any:
|
|
|
229
247
|
|
|
230
248
|
|
|
231
249
|
def _get_backend() -> Any:
|
|
232
|
-
"""Select a backend using the preferred backend if set.
|
|
233
|
-
|
|
250
|
+
"""Select a backend using the preferred backend if set.
|
|
251
|
+
|
|
252
|
+
Registers the backend for automatic cleanup on crash or signal.
|
|
253
|
+
"""
|
|
254
|
+
backend = get_manager().select(_get_ctx().preferred_backend)
|
|
255
|
+
register_backend(backend)
|
|
256
|
+
return backend
|
|
234
257
|
|
|
235
258
|
|
|
236
259
|
def _browser_options() -> BrowserOptions:
|
|
@@ -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)
|
|
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
|