webtap-tool 0.11.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.
- webtap/VISION.md +246 -0
- webtap/__init__.py +84 -0
- webtap/__main__.py +6 -0
- webtap/api/__init__.py +9 -0
- webtap/api/app.py +26 -0
- webtap/api/models.py +69 -0
- webtap/api/server.py +111 -0
- webtap/api/sse.py +182 -0
- webtap/api/state.py +89 -0
- webtap/app.py +79 -0
- webtap/cdp/README.md +275 -0
- webtap/cdp/__init__.py +12 -0
- webtap/cdp/har.py +302 -0
- webtap/cdp/schema/README.md +41 -0
- webtap/cdp/schema/cdp_protocol.json +32785 -0
- webtap/cdp/schema/cdp_version.json +8 -0
- webtap/cdp/session.py +667 -0
- webtap/client.py +81 -0
- webtap/commands/DEVELOPER_GUIDE.md +401 -0
- webtap/commands/TIPS.md +269 -0
- webtap/commands/__init__.py +29 -0
- webtap/commands/_builders.py +331 -0
- webtap/commands/_code_generation.py +110 -0
- webtap/commands/_tips.py +147 -0
- webtap/commands/_utils.py +273 -0
- webtap/commands/connection.py +220 -0
- webtap/commands/console.py +87 -0
- webtap/commands/fetch.py +310 -0
- webtap/commands/filters.py +116 -0
- webtap/commands/javascript.py +73 -0
- webtap/commands/js_export.py +73 -0
- webtap/commands/launch.py +72 -0
- webtap/commands/navigation.py +197 -0
- webtap/commands/network.py +136 -0
- webtap/commands/quicktype.py +306 -0
- webtap/commands/request.py +93 -0
- webtap/commands/selections.py +138 -0
- webtap/commands/setup.py +219 -0
- webtap/commands/to_model.py +163 -0
- webtap/daemon.py +185 -0
- webtap/daemon_state.py +53 -0
- webtap/filters.py +219 -0
- webtap/rpc/__init__.py +14 -0
- webtap/rpc/errors.py +49 -0
- webtap/rpc/framework.py +223 -0
- webtap/rpc/handlers.py +625 -0
- webtap/rpc/machine.py +84 -0
- webtap/services/README.md +83 -0
- webtap/services/__init__.py +15 -0
- webtap/services/console.py +124 -0
- webtap/services/dom.py +547 -0
- webtap/services/fetch.py +415 -0
- webtap/services/main.py +392 -0
- webtap/services/network.py +401 -0
- webtap/services/setup/__init__.py +185 -0
- webtap/services/setup/chrome.py +233 -0
- webtap/services/setup/desktop.py +255 -0
- webtap/services/setup/extension.py +147 -0
- webtap/services/setup/platform.py +162 -0
- webtap/services/state_snapshot.py +86 -0
- webtap_tool-0.11.0.dist-info/METADATA +535 -0
- webtap_tool-0.11.0.dist-info/RECORD +64 -0
- webtap_tool-0.11.0.dist-info/WHEEL +4 -0
- webtap_tool-0.11.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Platform detection and path management using platformdirs.
|
|
2
|
+
|
|
3
|
+
PUBLIC API:
|
|
4
|
+
- get_platform_info: Get platform information dict with paths and Chrome location
|
|
5
|
+
- ensure_directories: Create required directories with proper permissions
|
|
6
|
+
- APP_NAME: Application name constant
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import platform
|
|
10
|
+
import shutil
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
import platformdirs
|
|
15
|
+
|
|
16
|
+
# Application constants
|
|
17
|
+
APP_NAME = "webtap"
|
|
18
|
+
APP_AUTHOR = "webtap"
|
|
19
|
+
|
|
20
|
+
# Directory names
|
|
21
|
+
BIN_DIR_NAME = ".local/bin"
|
|
22
|
+
WRAPPER_NAME = "chrome-debug"
|
|
23
|
+
TMP_RUNTIME_DIR = "/tmp"
|
|
24
|
+
|
|
25
|
+
# Chrome executable names for Linux
|
|
26
|
+
CHROME_NAMES_LINUX = [
|
|
27
|
+
"google-chrome",
|
|
28
|
+
"google-chrome-stable",
|
|
29
|
+
"chromium",
|
|
30
|
+
"chromium-browser",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
# Chrome paths for macOS
|
|
34
|
+
CHROME_PATHS_MACOS = [
|
|
35
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
36
|
+
"Applications/Google Chrome.app/Contents/MacOS/Google Chrome", # Relative to home
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
# Chrome paths for Linux
|
|
40
|
+
CHROME_PATHS_LINUX = [
|
|
41
|
+
"/usr/bin/google-chrome",
|
|
42
|
+
"/usr/bin/google-chrome-stable",
|
|
43
|
+
"/usr/bin/chromium",
|
|
44
|
+
"/usr/bin/chromium-browser",
|
|
45
|
+
"/snap/bin/chromium",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
# Platform identifiers
|
|
49
|
+
PLATFORM_DARWIN = "Darwin"
|
|
50
|
+
PLATFORM_LINUX = "Linux"
|
|
51
|
+
|
|
52
|
+
# Application directories
|
|
53
|
+
MACOS_APPLICATIONS_DIR = "Applications"
|
|
54
|
+
LINUX_APPLICATIONS_DIR = ".local/share/applications"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_platform_paths() -> dict[str, Path]:
|
|
58
|
+
"""Get platform-appropriate paths using platformdirs.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Dictionary of paths for config, data, cache, runtime, and state directories.
|
|
62
|
+
"""
|
|
63
|
+
dirs = platformdirs.PlatformDirs(APP_NAME, APP_AUTHOR)
|
|
64
|
+
|
|
65
|
+
paths = {
|
|
66
|
+
"config_dir": Path(dirs.user_config_dir), # ~/.config/webtap or ~/Library/Application Support/webtap
|
|
67
|
+
"data_dir": Path(dirs.user_data_dir), # ~/.local/share/webtap or ~/Library/Application Support/webtap
|
|
68
|
+
"cache_dir": Path(dirs.user_cache_dir), # ~/.cache/webtap or ~/Library/Caches/webtap
|
|
69
|
+
"state_dir": Path(dirs.user_state_dir), # ~/.local/state/webtap or ~/Library/Application Support/webtap
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Runtime dir (not available on all platforms)
|
|
73
|
+
try:
|
|
74
|
+
paths["runtime_dir"] = Path(dirs.user_runtime_dir)
|
|
75
|
+
except AttributeError:
|
|
76
|
+
# Fallback for platforms without runtime dir
|
|
77
|
+
paths["runtime_dir"] = Path(TMP_RUNTIME_DIR) / APP_NAME
|
|
78
|
+
|
|
79
|
+
return paths
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_chrome_path() -> Optional[Path]:
|
|
83
|
+
"""Find Chrome executable path for current platform.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Path to Chrome executable or None if not found.
|
|
87
|
+
"""
|
|
88
|
+
system = platform.system()
|
|
89
|
+
|
|
90
|
+
if system == PLATFORM_DARWIN:
|
|
91
|
+
# macOS standard locations
|
|
92
|
+
candidates = [
|
|
93
|
+
Path(CHROME_PATHS_MACOS[0]),
|
|
94
|
+
Path.home() / CHROME_PATHS_MACOS[1],
|
|
95
|
+
]
|
|
96
|
+
elif system == PLATFORM_LINUX:
|
|
97
|
+
# Linux standard locations
|
|
98
|
+
candidates = [Path(p) for p in CHROME_PATHS_LINUX]
|
|
99
|
+
else:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
for path in candidates:
|
|
103
|
+
if path.exists():
|
|
104
|
+
return path
|
|
105
|
+
|
|
106
|
+
# Try to find in PATH
|
|
107
|
+
for name in CHROME_NAMES_LINUX:
|
|
108
|
+
if found := shutil.which(name):
|
|
109
|
+
return Path(found)
|
|
110
|
+
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_platform_info() -> dict:
|
|
115
|
+
"""Get comprehensive platform information.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Dictionary with system info, paths, and capabilities.
|
|
119
|
+
"""
|
|
120
|
+
system = platform.system()
|
|
121
|
+
paths = get_platform_paths()
|
|
122
|
+
|
|
123
|
+
# Unified paths for both platforms
|
|
124
|
+
paths["bin_dir"] = Path.home() / BIN_DIR_NAME # User space, no sudo needed
|
|
125
|
+
|
|
126
|
+
# Platform-specific launcher locations
|
|
127
|
+
if system == PLATFORM_DARWIN:
|
|
128
|
+
paths["applications_dir"] = Path.home() / MACOS_APPLICATIONS_DIR
|
|
129
|
+
else: # Linux
|
|
130
|
+
paths["applications_dir"] = Path.home() / LINUX_APPLICATIONS_DIR
|
|
131
|
+
|
|
132
|
+
chrome_path = get_chrome_path()
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
"system": system.lower(),
|
|
136
|
+
"is_macos": system == PLATFORM_DARWIN,
|
|
137
|
+
"is_linux": system == PLATFORM_LINUX,
|
|
138
|
+
"paths": paths,
|
|
139
|
+
"chrome": {
|
|
140
|
+
"path": chrome_path,
|
|
141
|
+
"found": chrome_path is not None,
|
|
142
|
+
"wrapper_name": WRAPPER_NAME,
|
|
143
|
+
},
|
|
144
|
+
"capabilities": {
|
|
145
|
+
"desktop_files": system == PLATFORM_LINUX,
|
|
146
|
+
"app_bundles": system == PLATFORM_DARWIN,
|
|
147
|
+
"bindfs": system == PLATFORM_LINUX and shutil.which("bindfs") is not None,
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def ensure_directories() -> None:
|
|
153
|
+
"""Ensure all required directories exist with proper permissions."""
|
|
154
|
+
paths = get_platform_paths()
|
|
155
|
+
|
|
156
|
+
for name, path in paths.items():
|
|
157
|
+
if name != "runtime_dir": # Runtime dir is often system-managed
|
|
158
|
+
path.mkdir(parents=True, exist_ok=True, mode=0o755)
|
|
159
|
+
|
|
160
|
+
# Ensure bin directory exists
|
|
161
|
+
info = get_platform_info()
|
|
162
|
+
info["paths"]["bin_dir"].mkdir(parents=True, exist_ok=True, mode=0o755)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Immutable state snapshots for thread-safe SSE broadcasting."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class StateSnapshot:
|
|
9
|
+
"""Immutable snapshot of WebTap state.
|
|
10
|
+
|
|
11
|
+
Frozen dataclass provides inherent thread safety - multiple threads can
|
|
12
|
+
read simultaneously without locks. Updated atomically when state changes.
|
|
13
|
+
|
|
14
|
+
Used by SSE broadcast to avoid lock contention between asyncio event loop
|
|
15
|
+
and background threads (WebSocket, disconnect handlers).
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
connected: Whether connected to Chrome page
|
|
19
|
+
page_id: Stable page identifier (empty if not connected)
|
|
20
|
+
page_title: Page title (empty if not connected)
|
|
21
|
+
page_url: Page URL (empty if not connected)
|
|
22
|
+
event_count: Total CDP events stored
|
|
23
|
+
fetch_enabled: Whether fetch interception is active
|
|
24
|
+
paused_count: Number of paused requests (if fetch enabled)
|
|
25
|
+
enabled_filters: Tuple of enabled filter category names
|
|
26
|
+
disabled_filters: Tuple of disabled filter category names
|
|
27
|
+
inspect_active: Whether element inspection mode is active
|
|
28
|
+
selections: Dict of selected elements (id -> element data)
|
|
29
|
+
prompt: Browser prompt text (unused, reserved)
|
|
30
|
+
pending_count: Number of pending element selections being processed
|
|
31
|
+
error_message: Current error message or None
|
|
32
|
+
error_timestamp: Error timestamp or None
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
# Connection state
|
|
36
|
+
connected: bool
|
|
37
|
+
page_id: str
|
|
38
|
+
page_title: str
|
|
39
|
+
page_url: str
|
|
40
|
+
|
|
41
|
+
# Event state
|
|
42
|
+
event_count: int
|
|
43
|
+
|
|
44
|
+
# Fetch interception state
|
|
45
|
+
fetch_enabled: bool
|
|
46
|
+
response_stage: bool
|
|
47
|
+
paused_count: int
|
|
48
|
+
|
|
49
|
+
# Filter state (immutable tuples)
|
|
50
|
+
enabled_filters: tuple[str, ...]
|
|
51
|
+
disabled_filters: tuple[str, ...]
|
|
52
|
+
|
|
53
|
+
# Browser/DOM state
|
|
54
|
+
inspect_active: bool
|
|
55
|
+
selections: dict[str, Any] # Dict is mutable but replaced atomically
|
|
56
|
+
prompt: str
|
|
57
|
+
pending_count: int
|
|
58
|
+
|
|
59
|
+
# Error state
|
|
60
|
+
error_message: str | None
|
|
61
|
+
error_timestamp: float | None
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def create_empty(cls) -> "StateSnapshot":
|
|
65
|
+
"""Create empty snapshot for disconnected state."""
|
|
66
|
+
return cls(
|
|
67
|
+
connected=False,
|
|
68
|
+
page_id="",
|
|
69
|
+
page_title="",
|
|
70
|
+
page_url="",
|
|
71
|
+
event_count=0,
|
|
72
|
+
fetch_enabled=False,
|
|
73
|
+
response_stage=False,
|
|
74
|
+
paused_count=0,
|
|
75
|
+
enabled_filters=(),
|
|
76
|
+
disabled_filters=(),
|
|
77
|
+
inspect_active=False,
|
|
78
|
+
selections={},
|
|
79
|
+
prompt="",
|
|
80
|
+
pending_count=0,
|
|
81
|
+
error_message=None,
|
|
82
|
+
error_timestamp=None,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
__all__ = ["StateSnapshot"]
|