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.
Files changed (197) hide show
  1. {wavexis-2.0.2 → wavexis-2.0.3}/CHANGELOG.md +10 -0
  2. {wavexis-2.0.2 → wavexis-2.0.3}/PKG-INFO +1 -1
  3. {wavexis-2.0.2 → wavexis-2.0.3}/pyproject.toml +1 -1
  4. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_global_options.py +28 -34
  5. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_interactions_crawl.py +6 -1
  6. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_new_features.py +3 -2
  7. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/__init__.py +1 -1
  8. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/crawl.py +4 -3
  9. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/eval.py +4 -1
  10. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/form.py +3 -2
  11. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/record.py +3 -2
  12. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/scrape.py +4 -1
  13. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/screencast.py +7 -5
  14. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/session.py +2 -1
  15. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/backend/cdp.py +10 -14
  16. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/cli/app.py +71 -44
  17. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/plugins.py +1 -1
  18. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/repl.py +8 -3
  19. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/serve.py +1 -1
  20. {wavexis-2.0.2 → wavexis-2.0.3}/.dockerignore +0 -0
  21. {wavexis-2.0.2 → wavexis-2.0.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  22. {wavexis-2.0.2 → wavexis-2.0.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  23. {wavexis-2.0.2 → wavexis-2.0.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  24. {wavexis-2.0.2 → wavexis-2.0.3}/.github/workflows/ci.yml +0 -0
  25. {wavexis-2.0.2 → wavexis-2.0.3}/.github/workflows/docs.yml +0 -0
  26. {wavexis-2.0.2 → wavexis-2.0.3}/.github/workflows/release.yml +0 -0
  27. {wavexis-2.0.2 → wavexis-2.0.3}/.gitignore +0 -0
  28. {wavexis-2.0.2 → wavexis-2.0.3}/CODE_OF_CONDUCT.md +0 -0
  29. {wavexis-2.0.2 → wavexis-2.0.3}/CONTRIBUTING.md +0 -0
  30. {wavexis-2.0.2 → wavexis-2.0.3}/Dockerfile +0 -0
  31. {wavexis-2.0.2 → wavexis-2.0.3}/LICENSE +0 -0
  32. {wavexis-2.0.2 → wavexis-2.0.3}/README.md +0 -0
  33. {wavexis-2.0.2 → wavexis-2.0.3}/SECURITY.md +0 -0
  34. {wavexis-2.0.2 → wavexis-2.0.3}/docs/api/actions.md +0 -0
  35. {wavexis-2.0.2 → wavexis-2.0.3}/docs/api/backends.md +0 -0
  36. {wavexis-2.0.2 → wavexis-2.0.3}/docs/api/cli.md +0 -0
  37. {wavexis-2.0.2 → wavexis-2.0.3}/docs/api/config.md +0 -0
  38. {wavexis-2.0.2 → wavexis-2.0.3}/docs/api/exceptions.md +0 -0
  39. {wavexis-2.0.2 → wavexis-2.0.3}/docs/api/multi.md +0 -0
  40. {wavexis-2.0.2 → wavexis-2.0.3}/docs/api/plugins.md +0 -0
  41. {wavexis-2.0.2 → wavexis-2.0.3}/docs/api/serve.md +0 -0
  42. {wavexis-2.0.2 → wavexis-2.0.3}/docs/cookbook/auth.md +0 -0
  43. {wavexis-2.0.2 → wavexis-2.0.3}/docs/cookbook/ci-cd.md +0 -0
  44. {wavexis-2.0.2 → wavexis-2.0.3}/docs/cookbook/css.md +0 -0
  45. {wavexis-2.0.2 → wavexis-2.0.3}/docs/cookbook/debug.md +0 -0
  46. {wavexis-2.0.2 → wavexis-2.0.3}/docs/cookbook/record-replay.md +0 -0
  47. {wavexis-2.0.2 → wavexis-2.0.3}/docs/cookbook/scraping.md +0 -0
  48. {wavexis-2.0.2 → wavexis-2.0.3}/docs/cookbook/screenshots.md +0 -0
  49. {wavexis-2.0.2 → wavexis-2.0.3}/docs/cookbook/serve-mode.md +0 -0
  50. {wavexis-2.0.2 → wavexis-2.0.3}/docs/guide/assert.md +0 -0
  51. {wavexis-2.0.2 → wavexis-2.0.3}/docs/guide/backends.md +0 -0
  52. {wavexis-2.0.2 → wavexis-2.0.3}/docs/guide/commands.md +0 -0
  53. {wavexis-2.0.2 → wavexis-2.0.3}/docs/guide/init.md +0 -0
  54. {wavexis-2.0.2 → wavexis-2.0.3}/docs/guide/installation.md +0 -0
  55. {wavexis-2.0.2 → wavexis-2.0.3}/docs/guide/multi.md +0 -0
  56. {wavexis-2.0.2 → wavexis-2.0.3}/docs/guide/perf.md +0 -0
  57. {wavexis-2.0.2 → wavexis-2.0.3}/docs/guide/plugins.md +0 -0
  58. {wavexis-2.0.2 → wavexis-2.0.3}/docs/guide/quickstart.md +0 -0
  59. {wavexis-2.0.2 → wavexis-2.0.3}/docs/guide/raw.md +0 -0
  60. {wavexis-2.0.2 → wavexis-2.0.3}/docs/guide/repl.md +0 -0
  61. {wavexis-2.0.2 → wavexis-2.0.3}/docs/guide/troubleshooting.md +0 -0
  62. {wavexis-2.0.2 → wavexis-2.0.3}/docs/index.md +0 -0
  63. {wavexis-2.0.2 → wavexis-2.0.3}/mkdocs.yml +0 -0
  64. {wavexis-2.0.2 → wavexis-2.0.3}/stubs/bidiwave/__init__.pyi +0 -0
  65. {wavexis-2.0.2 → wavexis-2.0.3}/stubs/cdpwave/__init__.pyi +0 -0
  66. {wavexis-2.0.2 → wavexis-2.0.3}/tests/__init__.py +0 -0
  67. {wavexis-2.0.2 → wavexis-2.0.3}/tests/conftest.py +0 -0
  68. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/__init__.py +0 -0
  69. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_a11y.py +0 -0
  70. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_animation.py +0 -0
  71. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_backend_selection.py +0 -0
  72. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_browser.py +0 -0
  73. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_console.py +0 -0
  74. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_css.py +0 -0
  75. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_debug.py +0 -0
  76. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_dialog.py +0 -0
  77. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_dom.py +0 -0
  78. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_dom_snapshot.py +0 -0
  79. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_emulation.py +0 -0
  80. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_emulation_advanced.py +0 -0
  81. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_eval.py +0 -0
  82. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_har.py +0 -0
  83. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_input.py +0 -0
  84. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_media.py +0 -0
  85. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_multi.py +0 -0
  86. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_navigate.py +0 -0
  87. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_network.py +0 -0
  88. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_network_advanced.py +0 -0
  89. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_overlay.py +0 -0
  90. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_pdf.py +0 -0
  91. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_perf.py +0 -0
  92. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_permissions.py +0 -0
  93. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_raw.py +0 -0
  94. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_record.py +0 -0
  95. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_scrape.py +0 -0
  96. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_screenshot.py +0 -0
  97. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_security.py +0 -0
  98. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_serve.py +0 -0
  99. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_service_worker.py +0 -0
  100. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_storage.py +0 -0
  101. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_tabs.py +0 -0
  102. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_webaudio.py +0 -0
  103. {wavexis-2.0.2 → wavexis-2.0.3}/tests/integration/test_webauthn.py +0 -0
  104. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/__init__.py +0 -0
  105. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_abstract_backend_phase5.py +0 -0
  106. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_actions.py +0 -0
  107. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_actions_phase5.py +0 -0
  108. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_animation_action.py +0 -0
  109. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_assert_multi.py +0 -0
  110. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_auth.py +0 -0
  111. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_backend_manager.py +0 -0
  112. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_bidi_backend.py +0 -0
  113. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_bidi_phase5.py +0 -0
  114. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_bluetooth_action.py +0 -0
  115. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_cast_action.py +0 -0
  116. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_cli_phase5.py +0 -0
  117. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_config.py +0 -0
  118. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_config_phase5.py +0 -0
  119. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_cookies_headers_actions.py +0 -0
  120. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_css_action.py +0 -0
  121. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_debug_action.py +0 -0
  122. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_dom_snapshot_action.py +0 -0
  123. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_download_screencast.py +0 -0
  124. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_emulation_action.py +0 -0
  125. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_error_handling.py +0 -0
  126. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_exceptions.py +0 -0
  127. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_format_record.py +0 -0
  128. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_init.py +0 -0
  129. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_input_action.py +0 -0
  130. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_manager.py +0 -0
  131. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_media_action.py +0 -0
  132. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_multi.py +0 -0
  133. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_multi_improvements.py +0 -0
  134. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_output.py +0 -0
  135. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_overlay_action.py +0 -0
  136. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_perf_action.py +0 -0
  137. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_perf_console.py +0 -0
  138. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_plugins.py +0 -0
  139. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_raw.py +0 -0
  140. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_record.py +0 -0
  141. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_repl.py +0 -0
  142. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_serve.py +0 -0
  143. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_service_worker_action.py +0 -0
  144. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_storage_action.py +0 -0
  145. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_webaudio_action.py +0 -0
  146. {wavexis-2.0.2 → wavexis-2.0.3}/tests/unit/test_webauthn_action.py +0 -0
  147. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/__main__.py +0 -0
  148. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/__init__.py +0 -0
  149. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/accessibility.py +0 -0
  150. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/animation.py +0 -0
  151. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/base.py +0 -0
  152. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/bluetooth.py +0 -0
  153. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/browser.py +0 -0
  154. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/cast.py +0 -0
  155. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/console.py +0 -0
  156. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/cookies.py +0 -0
  157. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/css.py +0 -0
  158. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/debug.py +0 -0
  159. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/dialog.py +0 -0
  160. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/dom.py +0 -0
  161. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/dom_snapshot.py +0 -0
  162. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/download.py +0 -0
  163. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/emulation.py +0 -0
  164. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/extract.py +0 -0
  165. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/har.py +0 -0
  166. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/headers.py +0 -0
  167. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/input.py +0 -0
  168. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/lighthouse.py +0 -0
  169. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/media.py +0 -0
  170. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/multi.py +0 -0
  171. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/navigate.py +0 -0
  172. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/network.py +0 -0
  173. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/overlay.py +0 -0
  174. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/pdf.py +0 -0
  175. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/performance.py +0 -0
  176. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/permissions.py +0 -0
  177. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/screenshot.py +0 -0
  178. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/security.py +0 -0
  179. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/service_worker.py +0 -0
  180. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/storage.py +0 -0
  181. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/tabs.py +0 -0
  182. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/wait.py +0 -0
  183. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/webaudio.py +0 -0
  184. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/webauthn.py +0 -0
  185. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/actions/websocket.py +0 -0
  186. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/auth.py +0 -0
  187. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/backend/__init__.py +0 -0
  188. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/backend/base.py +0 -0
  189. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/backend/bidi.py +0 -0
  190. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/backend/manager.py +0 -0
  191. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/cli/__init__.py +0 -0
  192. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/config.py +0 -0
  193. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/exceptions.py +0 -0
  194. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/init.py +0 -0
  195. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/multi.py +0 -0
  196. {wavexis-2.0.2 → wavexis-2.0.3}/wavexis/output.py +0 -0
  197. {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.2
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "wavexis"
7
- version = "2.0.2"
7
+ version = "2.0.3"
8
8
  description = "Browser automation CLI — wraps cdpwave and bidiwave, no Node.js, no Chromium download"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -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
- _cli._headless = True
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
- _cli._headless = False
58
- _cli._timeout = 30000
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
- _cli._headless = True
65
- _cli._timeout = 30000
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
- _cli._headless = True
72
- _cli._timeout = 60000
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
- _cli._preferred_backend = None
84
- _cli._headless = True
85
- _cli._timeout = 30000
86
- _cli._proxy = None
85
+ ctx = _fresh_ctx()
87
86
  _cli._load_global_config()
88
- assert _cli._preferred_backend is None
89
- assert _cli._headless is True
90
- assert _cli._timeout == 30000
91
- assert _cli._proxy is None
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
- _cli._preferred_backend = None
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 _cli._preferred_backend == "bidi"
110
- assert _cli._headless is False
111
- assert _cli._timeout == 60000
112
- assert _cli._proxy == "http://proxy:9090"
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
- _cli._preferred_backend = "cdp"
124
- _cli._headless = True
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 _cli._preferred_backend == "cdp"
130
- assert _cli._timeout == 60000
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=Exception("Network error"))
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=Exception("no page"))
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, Exception("not found")])
277
+ backend.fill = AsyncMock(side_effect=[None, WavexisError("not found")])
277
278
 
278
279
  params = FormParams(
279
280
  url="https://app.com/register",
@@ -1,3 +1,3 @@
1
1
  """wavexis — CLI de automatización de navegador."""
2
2
 
3
- __version__ = "2.0.2"
3
+ __version__ = "2.0.3"
@@ -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 Exception:
67
+ except WavexisError:
67
68
  continue
68
69
 
69
70
  title = ""
70
71
  links: list[str] = []
71
- with contextlib.suppress(Exception):
72
+ with contextlib.suppress(WavexisError):
72
73
  title = await backend.eval(
73
74
  "document.title", await_promise=False
74
75
  )
75
- with contextlib.suppress(Exception):
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 = Path(params.file).read_text(encoding="utf-8") # noqa: ASYNC240
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 Exception:
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 Exception:
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 Exception:
146
+ except (json.JSONDecodeError, TypeError, WavexisError):
146
147
  pass
147
148
 
148
- with contextlib.suppress(Exception):
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 = Path(params.file).read_text(encoding="utf-8") # noqa: ASYNC240
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 os
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
- os.makedirs(self._output_dir, exist_ok=True)
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 = os.path.join(self._output_dir, fname)
47
- with open(fpath, "wb") as f: # noqa: ASYNC230
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(Exception):
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 = _json.dumps(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 fname in os.listdir(download_dir):
1389
- fpath = os.path.join(download_dir, fname)
1390
- if os.path.isfile(fpath): # noqa: ASYNC240
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
- import json as _json
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 Exception:
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 Exception:
2339
+ except (TimeoutError, KeyError, TypeError):
2344
2340
  pass
2345
2341
  return messages
2346
2342