wavexis 2.0.0__py3-none-any.whl
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/__init__.py +3 -0
- wavexis/__main__.py +6 -0
- wavexis/actions/__init__.py +1 -0
- wavexis/actions/accessibility.py +62 -0
- wavexis/actions/animation.py +59 -0
- wavexis/actions/base.py +31 -0
- wavexis/actions/bluetooth.py +68 -0
- wavexis/actions/browser.py +39 -0
- wavexis/actions/cast.py +67 -0
- wavexis/actions/console.py +57 -0
- wavexis/actions/cookies.py +57 -0
- wavexis/actions/crawl.py +105 -0
- wavexis/actions/css.py +107 -0
- wavexis/actions/debug.py +145 -0
- wavexis/actions/dialog.py +59 -0
- wavexis/actions/dom.py +70 -0
- wavexis/actions/dom_snapshot.py +45 -0
- wavexis/actions/download.py +45 -0
- wavexis/actions/emulation.py +45 -0
- wavexis/actions/eval.py +36 -0
- wavexis/actions/extract.py +86 -0
- wavexis/actions/form.py +73 -0
- wavexis/actions/har.py +27 -0
- wavexis/actions/headers.py +52 -0
- wavexis/actions/input.py +72 -0
- wavexis/actions/lighthouse.py +312 -0
- wavexis/actions/media.py +62 -0
- wavexis/actions/multi.py +40 -0
- wavexis/actions/navigate.py +98 -0
- wavexis/actions/network.py +70 -0
- wavexis/actions/overlay.py +69 -0
- wavexis/actions/pdf.py +32 -0
- wavexis/actions/performance.py +80 -0
- wavexis/actions/permissions.py +59 -0
- wavexis/actions/record.py +151 -0
- wavexis/actions/scrape.py +50 -0
- wavexis/actions/screencast.py +50 -0
- wavexis/actions/screenshot.py +37 -0
- wavexis/actions/security.py +59 -0
- wavexis/actions/service_worker.py +69 -0
- wavexis/actions/session.py +114 -0
- wavexis/actions/storage.py +92 -0
- wavexis/actions/tabs.py +53 -0
- wavexis/actions/wait.py +23 -0
- wavexis/actions/webaudio.py +62 -0
- wavexis/actions/webauthn.py +96 -0
- wavexis/actions/websocket.py +110 -0
- wavexis/auth.py +86 -0
- wavexis/backend/__init__.py +1 -0
- wavexis/backend/base.py +838 -0
- wavexis/backend/bidi.py +2160 -0
- wavexis/backend/cdp.py +2413 -0
- wavexis/backend/manager.py +120 -0
- wavexis/cli/__init__.py +8 -0
- wavexis/cli/app.py +3874 -0
- wavexis/config.py +651 -0
- wavexis/exceptions.py +95 -0
- wavexis/init.py +177 -0
- wavexis/multi.py +305 -0
- wavexis/output.py +195 -0
- wavexis/plugins.py +250 -0
- wavexis/record.py +114 -0
- wavexis/repl.py +225 -0
- wavexis/serve.py +665 -0
- wavexis-2.0.0.dist-info/METADATA +463 -0
- wavexis-2.0.0.dist-info/RECORD +69 -0
- wavexis-2.0.0.dist-info/WHEEL +4 -0
- wavexis-2.0.0.dist-info/entry_points.txt +2 -0
- wavexis-2.0.0.dist-info/licenses/LICENSE +21 -0
wavexis/__init__.py
ADDED
wavexis/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Actions layer for wavexis."""
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Accessibility action for retrieving a11y tree, node, and ancestors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from wavexis.actions.base import BaseAction
|
|
8
|
+
from wavexis.backend.base import AbstractBackend
|
|
9
|
+
from wavexis.config import WaitStrategy
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AccessibilityAction(BaseAction[Any, Any]):
|
|
13
|
+
"""Action for accessibility tree operations."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
params: Any,
|
|
18
|
+
action: str = "tree",
|
|
19
|
+
node_id: str = "",
|
|
20
|
+
url: str = "",
|
|
21
|
+
wait: WaitStrategy | None = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Initialize the accessibility action.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
params: Action parameters.
|
|
27
|
+
action: Accessibility action type ("tree" or "node").
|
|
28
|
+
node_id: Node ID for node-specific actions.
|
|
29
|
+
url: URL to navigate to before the action.
|
|
30
|
+
wait: Wait strategy after navigation.
|
|
31
|
+
"""
|
|
32
|
+
self.params = params
|
|
33
|
+
self._action = action
|
|
34
|
+
self._node_id = node_id
|
|
35
|
+
self._url = url
|
|
36
|
+
self._wait = wait or WaitStrategy(strategy="load")
|
|
37
|
+
|
|
38
|
+
async def execute(self, backend: AbstractBackend) -> Any:
|
|
39
|
+
"""Execute the accessibility action on the backend.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
backend: The browser backend to use.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Accessibility tree dict, node dict, or list of ancestor dicts.
|
|
46
|
+
"""
|
|
47
|
+
from wavexis.config import BrowserOptions
|
|
48
|
+
|
|
49
|
+
await backend.launch(BrowserOptions())
|
|
50
|
+
try:
|
|
51
|
+
if self._url:
|
|
52
|
+
await backend.navigate(self._url, self._wait)
|
|
53
|
+
if self._action == "tree":
|
|
54
|
+
return await backend.a11y_tree()
|
|
55
|
+
elif self._action == "node":
|
|
56
|
+
return await backend.a11y_node(self._node_id)
|
|
57
|
+
elif self._action == "ancestors":
|
|
58
|
+
return await backend.a11y_ancestors(self._node_id)
|
|
59
|
+
else:
|
|
60
|
+
raise ValueError(f"Unknown a11y action: {self._action}")
|
|
61
|
+
finally:
|
|
62
|
+
await backend.close()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Animation action for listing, pausing, playing, and seeking animations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from wavexis.actions.base import BaseAction
|
|
8
|
+
from wavexis.backend.base import AbstractBackend
|
|
9
|
+
from wavexis.config import AnimationParams
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AnimationAction(BaseAction[AnimationParams, Any]):
|
|
13
|
+
"""Action for animation operations."""
|
|
14
|
+
|
|
15
|
+
async def execute(self, backend: AbstractBackend) -> Any:
|
|
16
|
+
"""Execute the animation action on the backend.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
backend: An AbstractBackend instance.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Result of the animation operation.
|
|
23
|
+
"""
|
|
24
|
+
try:
|
|
25
|
+
await backend.launch(self.params.browser)
|
|
26
|
+
if self.params.url:
|
|
27
|
+
await backend.navigate(self.params.url, self.params.wait)
|
|
28
|
+
|
|
29
|
+
action = self.params.action
|
|
30
|
+
|
|
31
|
+
if action == "list":
|
|
32
|
+
return await backend.animation_list()
|
|
33
|
+
|
|
34
|
+
if action == "pause":
|
|
35
|
+
if not self.params.animation_id:
|
|
36
|
+
raise ValueError("animation_id is required for pause action")
|
|
37
|
+
await backend.animation_pause(self.params.animation_id)
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
if action == "play":
|
|
41
|
+
if not self.params.animation_id:
|
|
42
|
+
raise ValueError("animation_id is required for play action")
|
|
43
|
+
await backend.animation_play(self.params.animation_id)
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
if action == "seek":
|
|
47
|
+
if not self.params.animation_id or self.params.time_ms is None:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
"animation_id and time_ms are required for seek action"
|
|
50
|
+
)
|
|
51
|
+
await backend.animation_seek(
|
|
52
|
+
self.params.animation_id, self.params.time_ms
|
|
53
|
+
)
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
raise ValueError(f"Unknown animation action: {action}")
|
|
57
|
+
|
|
58
|
+
finally:
|
|
59
|
+
await backend.close()
|
wavexis/actions/base.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Base action class for wavexis actions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Generic, TypeVar
|
|
7
|
+
|
|
8
|
+
from wavexis.backend.base import AbstractBackend
|
|
9
|
+
|
|
10
|
+
P = TypeVar("P")
|
|
11
|
+
R = TypeVar("R")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseAction(ABC, Generic[P, R]):
|
|
15
|
+
"""Abstract base class for all wavexis actions.
|
|
16
|
+
|
|
17
|
+
An action encapsulates a single operation (e.g. screenshot, eval, pdf)
|
|
18
|
+
that is executed against a backend.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, params: P) -> None:
|
|
22
|
+
"""Initialize the action with parameters.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
params: Action-specific parameters.
|
|
26
|
+
"""
|
|
27
|
+
self.params = params
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
async def execute(self, backend: AbstractBackend) -> R:
|
|
31
|
+
"""Execute the action against the given backend."""
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Bluetooth action for BLE emulation (experimental)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from wavexis.actions.base import BaseAction
|
|
9
|
+
from wavexis.backend.base import AbstractBackend
|
|
10
|
+
from wavexis.config import BrowserOptions, WaitStrategy
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class BluetoothParams:
|
|
15
|
+
"""Parameters for Bluetooth emulation operations.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
url: URL to navigate to before Bluetooth operations.
|
|
19
|
+
action: Bluetooth action — "emulate", "stop".
|
|
20
|
+
name: Device name for "emulate" action.
|
|
21
|
+
address: Device MAC address for "emulate" action.
|
|
22
|
+
wait: Wait strategy after navigation.
|
|
23
|
+
browser: Browser launch options.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
url: str = ""
|
|
27
|
+
action: str = "emulate"
|
|
28
|
+
name: str | None = None
|
|
29
|
+
address: str = "00:00:00:00:00:01"
|
|
30
|
+
wait: WaitStrategy = field(default_factory=WaitStrategy)
|
|
31
|
+
browser: BrowserOptions = field(default_factory=BrowserOptions)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BluetoothAction(BaseAction[BluetoothParams, Any]):
|
|
35
|
+
"""Action for Bluetooth emulation operations (experimental)."""
|
|
36
|
+
|
|
37
|
+
async def execute(self, backend: AbstractBackend) -> Any:
|
|
38
|
+
"""Execute the Bluetooth action on the backend.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
backend: An AbstractBackend instance.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Result of the Bluetooth operation.
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
await backend.launch(self.params.browser)
|
|
48
|
+
if self.params.url:
|
|
49
|
+
await backend.navigate(self.params.url, self.params.wait)
|
|
50
|
+
|
|
51
|
+
action = self.params.action
|
|
52
|
+
|
|
53
|
+
if action == "emulate":
|
|
54
|
+
if not self.params.name:
|
|
55
|
+
raise ValueError("name is required for emulate action")
|
|
56
|
+
await backend.bluetooth_emulate(
|
|
57
|
+
self.params.name, self.params.address
|
|
58
|
+
)
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
if action == "stop":
|
|
62
|
+
await backend.bluetooth_stop()
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
raise ValueError(f"Unknown Bluetooth action: {action}")
|
|
66
|
+
|
|
67
|
+
finally:
|
|
68
|
+
await backend.close()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Browser action for context and window management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from wavexis.actions.base import BaseAction
|
|
8
|
+
from wavexis.backend.base import AbstractBackend
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BrowserAction(BaseAction[str, Any]):
|
|
12
|
+
"""Action for browser management operations.
|
|
13
|
+
|
|
14
|
+
Supports contexts (new/list/close), window bounds (get/set), and version.
|
|
15
|
+
The params string specifies the action: "new_context", "list_contexts",
|
|
16
|
+
"close_context", "get_window", "set_window", or "version".
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
async def execute(self, backend: AbstractBackend) -> Any:
|
|
20
|
+
"""Execute the browser action.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
backend: The browser backend to use.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Action-dependent result (str, list, dict, or None).
|
|
27
|
+
"""
|
|
28
|
+
action = self.params
|
|
29
|
+
|
|
30
|
+
if action == "version":
|
|
31
|
+
return await backend.browser_version()
|
|
32
|
+
|
|
33
|
+
if action == "new_context":
|
|
34
|
+
return await backend.new_context()
|
|
35
|
+
|
|
36
|
+
if action == "list_contexts":
|
|
37
|
+
return await backend.list_contexts()
|
|
38
|
+
|
|
39
|
+
return None
|
wavexis/actions/cast.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Cast action for listing sinks and controlling tab mirroring (experimental)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from wavexis.actions.base import BaseAction
|
|
9
|
+
from wavexis.backend.base import AbstractBackend
|
|
10
|
+
from wavexis.config import BrowserOptions, WaitStrategy
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class CastParams:
|
|
15
|
+
"""Parameters for Cast operations.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
url: URL to navigate to before Cast operations.
|
|
19
|
+
action: Cast action — "list", "start-tab", "stop".
|
|
20
|
+
sink_name: Cast sink name for "start-tab" action.
|
|
21
|
+
wait: Wait strategy after navigation.
|
|
22
|
+
browser: Browser launch options.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
url: str = ""
|
|
26
|
+
action: str = "list"
|
|
27
|
+
sink_name: str | None = None
|
|
28
|
+
wait: WaitStrategy = field(default_factory=WaitStrategy)
|
|
29
|
+
browser: BrowserOptions = field(default_factory=BrowserOptions)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CastAction(BaseAction[CastParams, Any]):
|
|
33
|
+
"""Action for Cast operations (experimental)."""
|
|
34
|
+
|
|
35
|
+
async def execute(self, backend: AbstractBackend) -> Any:
|
|
36
|
+
"""Execute the Cast action on the backend.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
backend: An AbstractBackend instance.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Result of the Cast operation.
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
await backend.launch(self.params.browser)
|
|
46
|
+
if self.params.url:
|
|
47
|
+
await backend.navigate(self.params.url, self.params.wait)
|
|
48
|
+
|
|
49
|
+
action = self.params.action
|
|
50
|
+
|
|
51
|
+
if action == "list":
|
|
52
|
+
return await backend.cast_list()
|
|
53
|
+
|
|
54
|
+
if action == "start-tab":
|
|
55
|
+
if not self.params.sink_name:
|
|
56
|
+
raise ValueError("sink_name is required for start-tab action")
|
|
57
|
+
await backend.cast_start_tab(self.params.sink_name)
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
if action == "stop":
|
|
61
|
+
await backend.cast_stop()
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
raise ValueError(f"Unknown Cast action: {action}")
|
|
65
|
+
|
|
66
|
+
finally:
|
|
67
|
+
await backend.close()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Console action for capturing console messages and browser logs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from wavexis.actions.base import BaseAction
|
|
9
|
+
from wavexis.backend.base import AbstractBackend
|
|
10
|
+
from wavexis.config import WaitStrategy
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ConsoleParams:
|
|
15
|
+
"""Parameters for console capture.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
url: URL to navigate to before capturing.
|
|
19
|
+
level: Minimum log level — "all", "error", "warning", "info".
|
|
20
|
+
wait: Wait strategy after navigation.
|
|
21
|
+
capture: What to capture — "console", "logs", or "both".
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
url: str = ""
|
|
25
|
+
level: str = "all"
|
|
26
|
+
wait: WaitStrategy | None = None
|
|
27
|
+
capture: str = "console"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ConsoleAction(BaseAction[ConsoleParams, dict[str, Any]]):
|
|
31
|
+
"""Action for capturing console messages and browser logs.
|
|
32
|
+
|
|
33
|
+
Navigates to the URL, then captures console messages and/or log entries.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
async def execute(self, backend: AbstractBackend) -> dict[str, Any]:
|
|
37
|
+
"""Execute the console action.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
backend: The browser backend to use.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Dict with "console" and/or "logs" keys containing entry lists.
|
|
44
|
+
"""
|
|
45
|
+
params = self.params
|
|
46
|
+
if params.url:
|
|
47
|
+
await backend.navigate(params.url, params.wait)
|
|
48
|
+
|
|
49
|
+
result: dict[str, Any] = {}
|
|
50
|
+
|
|
51
|
+
if params.capture in ("console", "both"):
|
|
52
|
+
result["console"] = await backend.capture_console(level=params.level)
|
|
53
|
+
|
|
54
|
+
if params.capture in ("logs", "both"):
|
|
55
|
+
result["logs"] = await backend.capture_logs()
|
|
56
|
+
|
|
57
|
+
return result
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Cookie action for browser cookie operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from wavexis.actions.base import BaseAction
|
|
8
|
+
from wavexis.backend.base import AbstractBackend
|
|
9
|
+
from wavexis.config import CookieActionParams
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CookieAction(BaseAction[CookieActionParams, Any]):
|
|
13
|
+
"""Action for cookie operations: get, set, delete, clear.
|
|
14
|
+
|
|
15
|
+
Navigates to the URL in params, then performs the requested
|
|
16
|
+
cookie operation against the backend.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
async def execute(self, backend: AbstractBackend) -> Any:
|
|
20
|
+
"""Execute the cookie action on the backend.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
backend: An AbstractBackend instance.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Result of the cookie operation.
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
ValueError: If required parameters are missing.
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
await backend.launch(self.params.browser)
|
|
33
|
+
if self.params.url:
|
|
34
|
+
await backend.navigate(self.params.url, self.params.wait)
|
|
35
|
+
|
|
36
|
+
action = self.params.action
|
|
37
|
+
|
|
38
|
+
if action == "get":
|
|
39
|
+
return await backend.get_cookies()
|
|
40
|
+
|
|
41
|
+
if action == "set":
|
|
42
|
+
await backend.set_cookie(self.params.cookie)
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
if action == "delete":
|
|
46
|
+
if not self.params.name:
|
|
47
|
+
raise ValueError("name is required for delete action")
|
|
48
|
+
await backend.delete_cookie(self.params.name, self.params.domain)
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
if action == "clear":
|
|
52
|
+
await backend.clear_cookies()
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
raise ValueError(f"Unknown cookie action: {action}")
|
|
56
|
+
finally:
|
|
57
|
+
await backend.close()
|
wavexis/actions/crawl.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Crawl action for spidering a website with depth limit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any
|
|
9
|
+
from urllib.parse import urljoin, urlparse
|
|
10
|
+
|
|
11
|
+
from wavexis.actions.base import BaseAction
|
|
12
|
+
from wavexis.backend.base import AbstractBackend
|
|
13
|
+
from wavexis.config import WaitStrategy
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class CrawlParams:
|
|
18
|
+
"""Parameters for crawling a website.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
start_url: Starting URL for the crawl.
|
|
22
|
+
max_depth: Maximum crawl depth (1 = start page only, 2 = start + links).
|
|
23
|
+
max_pages: Maximum number of pages to visit.
|
|
24
|
+
same_origin: If True, only crawl links on the same origin.
|
|
25
|
+
url_pattern: Regex pattern to filter URLs (empty = all).
|
|
26
|
+
wait: Wait strategy after each navigation.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
start_url: str = ""
|
|
30
|
+
max_depth: int = 2
|
|
31
|
+
max_pages: int = 50
|
|
32
|
+
same_origin: bool = True
|
|
33
|
+
url_pattern: str = ""
|
|
34
|
+
wait: WaitStrategy = field(default_factory=WaitStrategy)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CrawlAction(BaseAction[CrawlParams, list[dict[str, Any]]]):
|
|
38
|
+
"""Action for crawling a website and collecting page data."""
|
|
39
|
+
|
|
40
|
+
async def execute(
|
|
41
|
+
self, backend: AbstractBackend
|
|
42
|
+
) -> list[dict[str, Any]]:
|
|
43
|
+
"""Execute the crawl on the backend.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
backend: The browser backend to use.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
List of page dicts with url, title, links, and depth.
|
|
50
|
+
"""
|
|
51
|
+
results: list[dict[str, Any]] = []
|
|
52
|
+
visited: set[str] = set()
|
|
53
|
+
queue: list[tuple[str, int]] = [(self.params.start_url, 0)]
|
|
54
|
+
origin = urlparse(self.params.start_url).netloc
|
|
55
|
+
pattern = re.compile(self.params.url_pattern) if self.params.url_pattern else None
|
|
56
|
+
|
|
57
|
+
while queue and len(results) < self.params.max_pages:
|
|
58
|
+
url, depth = queue.pop(0)
|
|
59
|
+
normalized = url.rstrip("/")
|
|
60
|
+
if normalized in visited or depth > self.params.max_depth:
|
|
61
|
+
continue
|
|
62
|
+
visited.add(normalized)
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
await backend.navigate(url, self.params.wait)
|
|
66
|
+
except Exception:
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
title = ""
|
|
70
|
+
links: list[str] = []
|
|
71
|
+
with contextlib.suppress(Exception):
|
|
72
|
+
title = await backend.eval(
|
|
73
|
+
"document.title", await_promise=False
|
|
74
|
+
)
|
|
75
|
+
with contextlib.suppress(Exception):
|
|
76
|
+
raw_links = await backend.eval(
|
|
77
|
+
"Array.from(document.querySelectorAll('a[href]')).map(a => a.href)",
|
|
78
|
+
await_promise=False,
|
|
79
|
+
)
|
|
80
|
+
if isinstance(raw_links, list):
|
|
81
|
+
links = [str(link) for link in raw_links if isinstance(link, str)]
|
|
82
|
+
|
|
83
|
+
page_data: dict[str, Any] = {
|
|
84
|
+
"url": url,
|
|
85
|
+
"title": title,
|
|
86
|
+
"depth": depth,
|
|
87
|
+
"links_found": len(links),
|
|
88
|
+
}
|
|
89
|
+
results.append(page_data)
|
|
90
|
+
|
|
91
|
+
if depth < self.params.max_depth:
|
|
92
|
+
for link in links:
|
|
93
|
+
absolute = urljoin(url, link)
|
|
94
|
+
parsed = urlparse(absolute)
|
|
95
|
+
if parsed.scheme not in ("http", "https"):
|
|
96
|
+
continue
|
|
97
|
+
if self.params.same_origin and parsed.netloc != origin:
|
|
98
|
+
continue
|
|
99
|
+
if pattern and not pattern.search(absolute):
|
|
100
|
+
continue
|
|
101
|
+
norm = absolute.rstrip("/")
|
|
102
|
+
if norm not in visited:
|
|
103
|
+
queue.append((absolute, depth + 1))
|
|
104
|
+
|
|
105
|
+
return results
|
wavexis/actions/css.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""CSS action for inspecting styles, stylesheets, rules, and computed styles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from wavexis.actions.base import BaseAction
|
|
9
|
+
from wavexis.backend.base import AbstractBackend
|
|
10
|
+
from wavexis.config import BrowserOptions, CSSParams, WaitStrategy
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class CSSActionParams:
|
|
15
|
+
"""Parameters for CSS inspection operations.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
url: URL to navigate to before CSS inspection.
|
|
19
|
+
selector: CSS selector for the target element.
|
|
20
|
+
stylesheet_id: Stylesheet ID for rules action.
|
|
21
|
+
action: CSS action — "styles", "stylesheets", "rules", "computed".
|
|
22
|
+
wait: Wait strategy after navigation.
|
|
23
|
+
browser: Browser launch options.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
url: str = ""
|
|
27
|
+
selector: str | None = None
|
|
28
|
+
stylesheet_id: str | None = None
|
|
29
|
+
action: str = "styles"
|
|
30
|
+
wait: WaitStrategy = field(default_factory=WaitStrategy)
|
|
31
|
+
browser: BrowserOptions = field(default_factory=BrowserOptions)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CSSAction(BaseAction[CSSActionParams, dict[str, Any] | list[dict[str, Any]]]):
|
|
35
|
+
"""Action for CSS inspection operations."""
|
|
36
|
+
|
|
37
|
+
async def execute(
|
|
38
|
+
self, backend: AbstractBackend
|
|
39
|
+
) -> dict[str, Any] | list[dict[str, Any]]:
|
|
40
|
+
"""Execute the CSS action on the backend.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
backend: The browser backend to use.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Dict or list of dicts containing CSS data.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
ValueError: If the action is not recognized or required params missing.
|
|
50
|
+
"""
|
|
51
|
+
await backend.launch(self.params.browser)
|
|
52
|
+
try:
|
|
53
|
+
await backend.navigate(self.params.url, self.params.wait)
|
|
54
|
+
return await self._run_action(backend)
|
|
55
|
+
finally:
|
|
56
|
+
await backend.close()
|
|
57
|
+
|
|
58
|
+
async def _run_action(
|
|
59
|
+
self, backend: AbstractBackend
|
|
60
|
+
) -> dict[str, Any] | list[dict[str, Any]]:
|
|
61
|
+
"""Execute the CSS action against the backend.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
backend: The browser backend to use.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
CSS data as a dict or list of dicts depending on the action.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
ValueError: If required parameters are missing for the action.
|
|
71
|
+
"""
|
|
72
|
+
action = self.params.action
|
|
73
|
+
if action == "styles":
|
|
74
|
+
if not self.params.selector:
|
|
75
|
+
raise ValueError("selector is required for styles action")
|
|
76
|
+
return await backend.css_get_styles(self.params.selector)
|
|
77
|
+
if action == "stylesheets":
|
|
78
|
+
return await backend.css_get_stylesheets()
|
|
79
|
+
if action == "rules":
|
|
80
|
+
if not self.params.stylesheet_id:
|
|
81
|
+
raise ValueError("stylesheet_id is required for rules action")
|
|
82
|
+
return await backend.css_get_rules(self.params.stylesheet_id)
|
|
83
|
+
if action == "computed":
|
|
84
|
+
if not self.params.selector:
|
|
85
|
+
raise ValueError("selector is required for computed action")
|
|
86
|
+
return await backend.css_get_computed(self.params.selector)
|
|
87
|
+
raise ValueError(f"Unknown CSS action: {action}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def css_action_from_config(params: CSSParams) -> CSSAction:
|
|
91
|
+
"""Create a CSSAction from CSSParams config dataclass.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
params: CSSParams from wavexis.config.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
CSSAction instance with mapped parameters.
|
|
98
|
+
"""
|
|
99
|
+
action_params = CSSActionParams(
|
|
100
|
+
url=params.url,
|
|
101
|
+
selector=params.selector,
|
|
102
|
+
stylesheet_id=params.stylesheet_id,
|
|
103
|
+
action=params.action,
|
|
104
|
+
wait=params.wait,
|
|
105
|
+
browser=params.browser,
|
|
106
|
+
)
|
|
107
|
+
return CSSAction(action_params)
|