wavexis 2.0.2__tar.gz → 2.0.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.
Files changed (197) hide show
  1. {wavexis-2.0.2 → wavexis-2.0.4}/.github/workflows/release.yml +32 -1
  2. {wavexis-2.0.2 → wavexis-2.0.4}/CHANGELOG.md +21 -0
  3. {wavexis-2.0.2 → wavexis-2.0.4}/PKG-INFO +1 -1
  4. {wavexis-2.0.2 → wavexis-2.0.4}/pyproject.toml +1 -1
  5. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_global_options.py +28 -34
  6. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_interactions_crawl.py +6 -1
  7. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_new_features.py +3 -2
  8. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/__init__.py +1 -1
  9. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/crawl.py +4 -3
  10. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/eval.py +5 -1
  11. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/form.py +3 -2
  12. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/lighthouse.py +2 -1
  13. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/record.py +3 -2
  14. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/scrape.py +5 -1
  15. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/screencast.py +7 -5
  16. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/session.py +2 -1
  17. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/backend/bidi.py +9 -18
  18. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/backend/cdp.py +13 -14
  19. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/cli/app.py +245 -447
  20. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/plugins.py +1 -1
  21. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/repl.py +9 -4
  22. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/serve.py +1 -1
  23. {wavexis-2.0.2 → wavexis-2.0.4}/.dockerignore +0 -0
  24. {wavexis-2.0.2 → wavexis-2.0.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  25. {wavexis-2.0.2 → wavexis-2.0.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  26. {wavexis-2.0.2 → wavexis-2.0.4}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  27. {wavexis-2.0.2 → wavexis-2.0.4}/.github/workflows/ci.yml +0 -0
  28. {wavexis-2.0.2 → wavexis-2.0.4}/.github/workflows/docs.yml +0 -0
  29. {wavexis-2.0.2 → wavexis-2.0.4}/.gitignore +0 -0
  30. {wavexis-2.0.2 → wavexis-2.0.4}/CODE_OF_CONDUCT.md +0 -0
  31. {wavexis-2.0.2 → wavexis-2.0.4}/CONTRIBUTING.md +0 -0
  32. {wavexis-2.0.2 → wavexis-2.0.4}/Dockerfile +0 -0
  33. {wavexis-2.0.2 → wavexis-2.0.4}/LICENSE +0 -0
  34. {wavexis-2.0.2 → wavexis-2.0.4}/README.md +0 -0
  35. {wavexis-2.0.2 → wavexis-2.0.4}/SECURITY.md +0 -0
  36. {wavexis-2.0.2 → wavexis-2.0.4}/docs/api/actions.md +0 -0
  37. {wavexis-2.0.2 → wavexis-2.0.4}/docs/api/backends.md +0 -0
  38. {wavexis-2.0.2 → wavexis-2.0.4}/docs/api/cli.md +0 -0
  39. {wavexis-2.0.2 → wavexis-2.0.4}/docs/api/config.md +0 -0
  40. {wavexis-2.0.2 → wavexis-2.0.4}/docs/api/exceptions.md +0 -0
  41. {wavexis-2.0.2 → wavexis-2.0.4}/docs/api/multi.md +0 -0
  42. {wavexis-2.0.2 → wavexis-2.0.4}/docs/api/plugins.md +0 -0
  43. {wavexis-2.0.2 → wavexis-2.0.4}/docs/api/serve.md +0 -0
  44. {wavexis-2.0.2 → wavexis-2.0.4}/docs/cookbook/auth.md +0 -0
  45. {wavexis-2.0.2 → wavexis-2.0.4}/docs/cookbook/ci-cd.md +0 -0
  46. {wavexis-2.0.2 → wavexis-2.0.4}/docs/cookbook/css.md +0 -0
  47. {wavexis-2.0.2 → wavexis-2.0.4}/docs/cookbook/debug.md +0 -0
  48. {wavexis-2.0.2 → wavexis-2.0.4}/docs/cookbook/record-replay.md +0 -0
  49. {wavexis-2.0.2 → wavexis-2.0.4}/docs/cookbook/scraping.md +0 -0
  50. {wavexis-2.0.2 → wavexis-2.0.4}/docs/cookbook/screenshots.md +0 -0
  51. {wavexis-2.0.2 → wavexis-2.0.4}/docs/cookbook/serve-mode.md +0 -0
  52. {wavexis-2.0.2 → wavexis-2.0.4}/docs/guide/assert.md +0 -0
  53. {wavexis-2.0.2 → wavexis-2.0.4}/docs/guide/backends.md +0 -0
  54. {wavexis-2.0.2 → wavexis-2.0.4}/docs/guide/commands.md +0 -0
  55. {wavexis-2.0.2 → wavexis-2.0.4}/docs/guide/init.md +0 -0
  56. {wavexis-2.0.2 → wavexis-2.0.4}/docs/guide/installation.md +0 -0
  57. {wavexis-2.0.2 → wavexis-2.0.4}/docs/guide/multi.md +0 -0
  58. {wavexis-2.0.2 → wavexis-2.0.4}/docs/guide/perf.md +0 -0
  59. {wavexis-2.0.2 → wavexis-2.0.4}/docs/guide/plugins.md +0 -0
  60. {wavexis-2.0.2 → wavexis-2.0.4}/docs/guide/quickstart.md +0 -0
  61. {wavexis-2.0.2 → wavexis-2.0.4}/docs/guide/raw.md +0 -0
  62. {wavexis-2.0.2 → wavexis-2.0.4}/docs/guide/repl.md +0 -0
  63. {wavexis-2.0.2 → wavexis-2.0.4}/docs/guide/troubleshooting.md +0 -0
  64. {wavexis-2.0.2 → wavexis-2.0.4}/docs/index.md +0 -0
  65. {wavexis-2.0.2 → wavexis-2.0.4}/mkdocs.yml +0 -0
  66. {wavexis-2.0.2 → wavexis-2.0.4}/stubs/bidiwave/__init__.pyi +0 -0
  67. {wavexis-2.0.2 → wavexis-2.0.4}/stubs/cdpwave/__init__.pyi +0 -0
  68. {wavexis-2.0.2 → wavexis-2.0.4}/tests/__init__.py +0 -0
  69. {wavexis-2.0.2 → wavexis-2.0.4}/tests/conftest.py +0 -0
  70. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/__init__.py +0 -0
  71. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_a11y.py +0 -0
  72. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_animation.py +0 -0
  73. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_backend_selection.py +0 -0
  74. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_browser.py +0 -0
  75. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_console.py +0 -0
  76. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_css.py +0 -0
  77. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_debug.py +0 -0
  78. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_dialog.py +0 -0
  79. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_dom.py +0 -0
  80. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_dom_snapshot.py +0 -0
  81. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_emulation.py +0 -0
  82. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_emulation_advanced.py +0 -0
  83. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_eval.py +0 -0
  84. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_har.py +0 -0
  85. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_input.py +0 -0
  86. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_media.py +0 -0
  87. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_multi.py +0 -0
  88. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_navigate.py +0 -0
  89. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_network.py +0 -0
  90. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_network_advanced.py +0 -0
  91. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_overlay.py +0 -0
  92. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_pdf.py +0 -0
  93. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_perf.py +0 -0
  94. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_permissions.py +0 -0
  95. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_raw.py +0 -0
  96. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_record.py +0 -0
  97. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_scrape.py +0 -0
  98. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_screenshot.py +0 -0
  99. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_security.py +0 -0
  100. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_serve.py +0 -0
  101. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_service_worker.py +0 -0
  102. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_storage.py +0 -0
  103. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_tabs.py +0 -0
  104. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_webaudio.py +0 -0
  105. {wavexis-2.0.2 → wavexis-2.0.4}/tests/integration/test_webauthn.py +0 -0
  106. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/__init__.py +0 -0
  107. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_abstract_backend_phase5.py +0 -0
  108. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_actions.py +0 -0
  109. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_actions_phase5.py +0 -0
  110. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_animation_action.py +0 -0
  111. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_assert_multi.py +0 -0
  112. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_auth.py +0 -0
  113. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_backend_manager.py +0 -0
  114. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_bidi_backend.py +0 -0
  115. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_bidi_phase5.py +0 -0
  116. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_bluetooth_action.py +0 -0
  117. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_cast_action.py +0 -0
  118. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_cli_phase5.py +0 -0
  119. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_config.py +0 -0
  120. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_config_phase5.py +0 -0
  121. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_cookies_headers_actions.py +0 -0
  122. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_css_action.py +0 -0
  123. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_debug_action.py +0 -0
  124. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_dom_snapshot_action.py +0 -0
  125. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_download_screencast.py +0 -0
  126. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_emulation_action.py +0 -0
  127. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_error_handling.py +0 -0
  128. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_exceptions.py +0 -0
  129. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_format_record.py +0 -0
  130. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_init.py +0 -0
  131. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_input_action.py +0 -0
  132. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_manager.py +0 -0
  133. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_media_action.py +0 -0
  134. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_multi.py +0 -0
  135. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_multi_improvements.py +0 -0
  136. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_output.py +0 -0
  137. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_overlay_action.py +0 -0
  138. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_perf_action.py +0 -0
  139. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_perf_console.py +0 -0
  140. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_plugins.py +0 -0
  141. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_raw.py +0 -0
  142. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_record.py +0 -0
  143. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_repl.py +0 -0
  144. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_serve.py +0 -0
  145. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_service_worker_action.py +0 -0
  146. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_storage_action.py +0 -0
  147. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_webaudio_action.py +0 -0
  148. {wavexis-2.0.2 → wavexis-2.0.4}/tests/unit/test_webauthn_action.py +0 -0
  149. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/__main__.py +0 -0
  150. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/__init__.py +0 -0
  151. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/accessibility.py +0 -0
  152. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/animation.py +0 -0
  153. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/base.py +0 -0
  154. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/bluetooth.py +0 -0
  155. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/browser.py +0 -0
  156. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/cast.py +0 -0
  157. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/console.py +0 -0
  158. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/cookies.py +0 -0
  159. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/css.py +0 -0
  160. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/debug.py +0 -0
  161. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/dialog.py +0 -0
  162. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/dom.py +0 -0
  163. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/dom_snapshot.py +0 -0
  164. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/download.py +0 -0
  165. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/emulation.py +0 -0
  166. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/extract.py +0 -0
  167. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/har.py +0 -0
  168. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/headers.py +0 -0
  169. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/input.py +0 -0
  170. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/media.py +0 -0
  171. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/multi.py +0 -0
  172. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/navigate.py +0 -0
  173. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/network.py +0 -0
  174. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/overlay.py +0 -0
  175. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/pdf.py +0 -0
  176. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/performance.py +0 -0
  177. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/permissions.py +0 -0
  178. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/screenshot.py +0 -0
  179. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/security.py +0 -0
  180. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/service_worker.py +0 -0
  181. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/storage.py +0 -0
  182. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/tabs.py +0 -0
  183. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/wait.py +0 -0
  184. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/webaudio.py +0 -0
  185. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/webauthn.py +0 -0
  186. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/actions/websocket.py +0 -0
  187. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/auth.py +0 -0
  188. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/backend/__init__.py +0 -0
  189. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/backend/base.py +0 -0
  190. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/backend/manager.py +0 -0
  191. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/cli/__init__.py +0 -0
  192. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/config.py +0 -0
  193. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/exceptions.py +0 -0
  194. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/init.py +0 -0
  195. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/multi.py +0 -0
  196. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/output.py +0 -0
  197. {wavexis-2.0.2 → wavexis-2.0.4}/wavexis/record.py +0 -0
@@ -6,7 +6,7 @@ on:
6
6
  - "v*.*.*"
7
7
 
8
8
  permissions:
9
- contents: read
9
+ contents: write
10
10
  packages: write
11
11
 
12
12
  concurrency:
@@ -76,3 +76,34 @@ jobs:
76
76
  cache-from: type=gha
77
77
  cache-to: type=gha,mode=max
78
78
  continue-on-error: true
79
+
80
+ github-release:
81
+ name: Create GitHub Release
82
+ needs: build
83
+ runs-on: ubuntu-latest
84
+ permissions:
85
+ contents: write
86
+ steps:
87
+ - uses: actions/checkout@v4
88
+ - uses: actions/download-artifact@v4
89
+ with:
90
+ name: dist
91
+ path: dist/
92
+ - name: Extract changelog for this version
93
+ id: changelog
94
+ run: |
95
+ version="${GITHUB_REF_NAME#v}"
96
+ awk -v ver="## v${version}" '
97
+ $0 ~ ver { found=1; next }
98
+ /^## v/ && found { exit }
99
+ found { print }
100
+ ' CHANGELOG.md > release_notes.md
101
+ - uses: softprops/action-gh-release@v2
102
+ with:
103
+ name: ${{ github.ref_name }}
104
+ body_path: release_notes.md
105
+ draft: false
106
+ prerelease: false
107
+ files: |
108
+ dist/*.whl
109
+ dist/*.tar.gz
@@ -2,6 +2,27 @@
2
2
 
3
3
  All notable changes to wavexis are documented in this file.
4
4
 
5
+ ## v2.0.4 — 2026-07-07
6
+
7
+ ### Refactored
8
+
9
+ - Extracted `_run_async` helper to eliminate ~60 `try/except asyncio.run` boilerplate patterns in `cli/app.py` (−229 lines net)
10
+ - Added `github-release` job to `release.yml` workflow — automatically creates GitHub Release with changelog notes and dist artifacts on tag push
11
+ - Narrowed `contextlib.suppress(Exception)` to `contextlib.suppress(WavexisError, OSError)` in `repl.py`
12
+ - Narrowed `contextlib.suppress(Exception)` to `contextlib.suppress(WavexisError)` in `actions/lighthouse.py`
13
+ - Removed 9 local `import json as _json` in `backend/bidi.py` — uses top-level `json` import
14
+ - Fixed mypy type errors in `cdp.py`, `scrape.py`, `eval.py`, `cli/app.py`
15
+
16
+ ## v2.0.3 — 2026-07-06
17
+
18
+ ### Refactored
19
+
20
+ - 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)
21
+ - 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`
22
+ - Replaced 6 module-level mutable globals (`_preferred_backend`, `_verbose`, `_quiet`, `_headless`, `_timeout`, `_proxy`) with `CLIContext` dataclass + `contextvars.ContextVar` for thread-safe state management
23
+ - Removed unused `os` import in `backend/cdp.py` download handler
24
+ - Replaced local `import json as _json` with top-level `json` import in `backend/cdp.py`
25
+
5
26
  ## v2.0.2 — 2026-07-06
6
27
 
7
28
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavexis
3
- Version: 2.0.2
3
+ Version: 2.0.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
@@ -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.4"
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.4"
@@ -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,9 @@ 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
+ file_path = params.file
36
+ expression = await asyncio.to_thread(
37
+ lambda: Path(file_path).read_text(encoding="utf-8")
38
+ )
35
39
 
36
40
  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 {
@@ -9,6 +9,7 @@ from typing import Any
9
9
  from wavexis.actions.base import BaseAction
10
10
  from wavexis.backend.base import AbstractBackend
11
11
  from wavexis.config import BrowserOptions, WaitStrategy
12
+ from wavexis.exceptions import WavexisError
12
13
 
13
14
 
14
15
  @dataclass
@@ -290,7 +291,7 @@ class LighthouseAction(BaseAction[LighthouseParams, dict[str, Any]]):
290
291
  })()
291
292
  """
292
293
  console_errors: list[dict[str, Any]] = []
293
- with contextlib.suppress(Exception):
294
+ with contextlib.suppress(WavexisError):
294
295
  console_errors = await backend.capture_console(level="error")
295
296
 
296
297
  bp = await backend.eval(js, await_promise=False)
@@ -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,10 @@ 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
+ file_path = params.file
37
+ expression = await asyncio.to_thread(
38
+ lambda: Path(file_path).read_text(encoding="utf-8")
39
+ )
36
40
 
37
41
  if not expression:
38
42
  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(
@@ -1209,9 +1209,8 @@ class BiDiBackend(AbstractBackend):
1209
1209
  "})"
1210
1210
  )
1211
1211
  result = await self._client.script.evaluate(self._context, js)
1212
- import json as _json
1213
1212
  val = result.value if hasattr(result, "value") else result
1214
- return _json.loads(val) if isinstance(val, str) else dict(val)
1213
+ return json.loads(val) if isinstance(val, str) else dict(val)
1215
1214
 
1216
1215
  async def perf_trace(self, duration_ms: int = 3000) -> dict[str, Any]:
1217
1216
  """Capture a performance trace via CDP Tracing.
@@ -1346,8 +1345,7 @@ class BiDiBackend(AbstractBackend):
1346
1345
  val = result.value if hasattr(result, "value") else result
1347
1346
  if not val:
1348
1347
  raise RuntimeError(f"Element not found: {selector}")
1349
- import json as _json
1350
- return _json.loads(val) if isinstance(val, str) else dict(val)
1348
+ return json.loads(val) if isinstance(val, str) else dict(val)
1351
1349
 
1352
1350
  async def css_get_stylesheets(self) -> list[dict[str, Any]]:
1353
1351
  """List all stylesheets in the page via JS.
@@ -1369,9 +1367,8 @@ class BiDiBackend(AbstractBackend):
1369
1367
  "}}))"
1370
1368
  )
1371
1369
  result = await self._client.script.evaluate(self._context, js)
1372
- import json as _json
1373
1370
  val = result.value if hasattr(result, "value") else result
1374
- return _json.loads(val) if isinstance(val, str) else list(val)
1371
+ return json.loads(val) if isinstance(val, str) else list(val)
1375
1372
 
1376
1373
  async def css_get_rules(self, stylesheet_id: str) -> list[dict[str, Any]]:
1377
1374
  """Get CSS rules from a stylesheet by index via JS.
@@ -1396,9 +1393,8 @@ class BiDiBackend(AbstractBackend):
1396
1393
  f"}})()"
1397
1394
  )
1398
1395
  result = await self._client.script.evaluate(self._context, js)
1399
- import json as _json
1400
1396
  val = result.value if hasattr(result, "value") else result
1401
- return _json.loads(val) if isinstance(val, str) else list(val)
1397
+ return json.loads(val) if isinstance(val, str) else list(val)
1402
1398
 
1403
1399
  async def css_get_computed(self, selector: str) -> dict[str, Any]:
1404
1400
  """Get computed styles for an element via JS getComputedStyle.
@@ -1429,8 +1425,7 @@ class BiDiBackend(AbstractBackend):
1429
1425
  val = result.value if hasattr(result, "value") else result
1430
1426
  if not val:
1431
1427
  raise RuntimeError(f"Element not found: {selector}")
1432
- import json as _json
1433
- return _json.loads(val) if isinstance(val, str) else dict(val)
1428
+ return json.loads(val) if isinstance(val, str) else dict(val)
1434
1429
 
1435
1430
  # ── Debugging ──────────────────────────────────────────
1436
1431
 
@@ -1721,9 +1716,8 @@ class BiDiBackend(AbstractBackend):
1721
1716
  "})"
1722
1717
  )
1723
1718
  result = await self._client.script.evaluate(self._context, js)
1724
- import json as _json
1725
1719
  val = result.value if hasattr(result, "value") else result
1726
- return _json.loads(val) if isinstance(val, str) else list(val)
1720
+ return json.loads(val) if isinstance(val, str) else list(val)
1727
1721
 
1728
1722
  async def cache_storage_entries(
1729
1723
  self, cache_name: str,
@@ -1749,9 +1743,8 @@ class BiDiBackend(AbstractBackend):
1749
1743
  f"}})"
1750
1744
  )
1751
1745
  result = await self._client.script.evaluate(self._context, js)
1752
- import json as _json
1753
1746
  val = result.value if hasattr(result, "value") else result
1754
- return _json.loads(val) if isinstance(val, str) else list(val)
1747
+ return json.loads(val) if isinstance(val, str) else list(val)
1755
1748
 
1756
1749
  async def cache_storage_delete(self, cache_name: str) -> None:
1757
1750
  """Delete a cache by name via JS Cache API.
@@ -1845,9 +1838,8 @@ class BiDiBackend(AbstractBackend):
1845
1838
  "})"
1846
1839
  )
1847
1840
  result = await self._client.script.evaluate(self._context, js)
1848
- import json as _json
1849
1841
  val = result.value if hasattr(result, "value") else result
1850
- return _json.loads(val) if isinstance(val, str) else list(val)
1842
+ return json.loads(val) if isinstance(val, str) else list(val)
1851
1843
 
1852
1844
  async def sw_unregister(self, registration_id: str) -> None:
1853
1845
  """Unregister a service worker by scope via JS.
@@ -1910,9 +1902,8 @@ class BiDiBackend(AbstractBackend):
1910
1902
  "})"
1911
1903
  )
1912
1904
  result = await self._client.script.evaluate(self._context, js)
1913
- import json as _json
1914
1905
  val = result.value if hasattr(result, "value") else result
1915
- return _json.loads(val) if isinstance(val, str) else list(val)
1906
+ return json.loads(val) if isinstance(val, str) else list(val)
1916
1907
 
1917
1908
  async def animation_pause(self, animation_id: str) -> None:
1918
1909
  """Pause an animation by index via JS.
@@ -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,12 @@ 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
+ def _list_files() -> list[Path]:
1389
+ return list(Path(download_dir).iterdir())
1390
+
1391
+ for fpath in await asyncio.to_thread(_list_files):
1392
+ if await asyncio.to_thread(Path(fpath).is_file):
1393
+ return await asyncio.to_thread(Path(fpath).read_bytes)
1393
1394
 
1394
1395
  return b""
1395
1396
 
@@ -1585,10 +1586,8 @@ class CDPBackend(AbstractBackend):
1585
1586
  zf = zipfile.ZipFile(io.BytesIO(raw))
1586
1587
  for name in zf.namelist():
1587
1588
  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:
1589
+ trace_events.extend(json.loads(content).get("traceEvents", []))
1590
+ except (zipfile.BadZipFile, json.JSONDecodeError, KeyError, ValueError):
1592
1591
  trace_events.append({"raw_size": len(raw)})
1593
1592
  else:
1594
1593
  trace_events.append({"error": "No stream handle in tracingComplete"})
@@ -2323,7 +2322,7 @@ class CDPBackend(AbstractBackend):
2323
2322
  )
2324
2323
  for ev in events:
2325
2324
  players.append(dict(ev.get("player", ev)))
2326
- except Exception:
2325
+ except (TimeoutError, KeyError, TypeError):
2327
2326
  pass
2328
2327
  return players
2329
2328
 
@@ -2340,7 +2339,7 @@ class CDPBackend(AbstractBackend):
2340
2339
  for ev in events:
2341
2340
  if ev.get("playerId") == player_id:
2342
2341
  messages.append(dict(ev))
2343
- except Exception:
2342
+ except (TimeoutError, KeyError, TypeError):
2344
2343
  pass
2345
2344
  return messages
2346
2345