webtap-tool 0.7.0__py3-none-any.whl → 0.8.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.
Potentially problematic release.
This version of webtap-tool might be problematic. Click here for more details.
- webtap/__init__.py +4 -0
- webtap/api.py +50 -57
- webtap/app.py +5 -0
- webtap/cdp/session.py +166 -27
- webtap/commands/TIPS.md +125 -22
- webtap/commands/_builders.py +7 -1
- webtap/commands/_code_generation.py +110 -0
- webtap/commands/body.py +9 -5
- webtap/commands/connection.py +21 -0
- webtap/commands/javascript.py +13 -25
- webtap/commands/navigation.py +5 -0
- webtap/commands/quicktype.py +268 -0
- webtap/commands/to_model.py +23 -75
- webtap/services/body.py +209 -24
- webtap/services/dom.py +19 -12
- webtap/services/fetch.py +19 -0
- webtap/services/main.py +194 -2
- webtap/services/state_snapshot.py +88 -0
- {webtap_tool-0.7.0.dist-info → webtap_tool-0.8.0.dist-info}/METADATA +1 -1
- {webtap_tool-0.7.0.dist-info → webtap_tool-0.8.0.dist-info}/RECORD +22 -19
- {webtap_tool-0.7.0.dist-info → webtap_tool-0.8.0.dist-info}/WHEEL +0 -0
- {webtap_tool-0.7.0.dist-info → webtap_tool-0.8.0.dist-info}/entry_points.txt +0 -0
webtap/services/dom.py
CHANGED
|
@@ -46,7 +46,7 @@ class DOMService:
|
|
|
46
46
|
self.state = state
|
|
47
47
|
self._inspection_active = False
|
|
48
48
|
self._next_id = 1
|
|
49
|
-
self.
|
|
49
|
+
self._broadcast_callback: "Any | None" = None # Callback to service._trigger_broadcast()
|
|
50
50
|
self._state_lock = threading.Lock() # Protect state mutations
|
|
51
51
|
self._pending_selections = 0 # Track in-flight selection processing
|
|
52
52
|
self._executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="dom-worker")
|
|
@@ -59,13 +59,13 @@ class DOMService:
|
|
|
59
59
|
"""Set state after initialization."""
|
|
60
60
|
self.state = state
|
|
61
61
|
|
|
62
|
-
def
|
|
63
|
-
"""Set
|
|
62
|
+
def set_broadcast_callback(self, callback: "Any") -> None:
|
|
63
|
+
"""Set callback for broadcasting state changes.
|
|
64
64
|
|
|
65
65
|
Args:
|
|
66
|
-
|
|
66
|
+
callback: Function to call when state changes (service._trigger_broadcast)
|
|
67
67
|
"""
|
|
68
|
-
self.
|
|
68
|
+
self._broadcast_callback = callback
|
|
69
69
|
|
|
70
70
|
def start_inspect(self) -> dict[str, Any]:
|
|
71
71
|
"""Enable CDP element inspection mode.
|
|
@@ -116,6 +116,7 @@ class DOMService:
|
|
|
116
116
|
self._inspection_active = True
|
|
117
117
|
logger.info("Element inspection mode enabled")
|
|
118
118
|
|
|
119
|
+
self._trigger_broadcast()
|
|
119
120
|
return {"success": True, "inspect_active": True}
|
|
120
121
|
|
|
121
122
|
except Exception as e:
|
|
@@ -143,6 +144,7 @@ class DOMService:
|
|
|
143
144
|
self._inspection_active = False
|
|
144
145
|
logger.info("Element inspection mode disabled")
|
|
145
146
|
|
|
147
|
+
self._trigger_broadcast()
|
|
146
148
|
return {"success": True, "inspect_active": False}
|
|
147
149
|
|
|
148
150
|
except Exception as e:
|
|
@@ -244,12 +246,12 @@ class DOMService:
|
|
|
244
246
|
self._trigger_broadcast()
|
|
245
247
|
|
|
246
248
|
def _trigger_broadcast(self) -> None:
|
|
247
|
-
"""Trigger SSE broadcast via
|
|
248
|
-
if self.
|
|
249
|
+
"""Trigger SSE broadcast via service callback (ensures snapshot update)."""
|
|
250
|
+
if self._broadcast_callback:
|
|
249
251
|
try:
|
|
250
|
-
self.
|
|
252
|
+
self._broadcast_callback()
|
|
251
253
|
except Exception as e:
|
|
252
|
-
logger.debug(f"Failed to
|
|
254
|
+
logger.debug(f"Failed to trigger broadcast: {e}")
|
|
253
255
|
|
|
254
256
|
def _extract_node_data(self, backend_node_id: int) -> dict[str, Any]:
|
|
255
257
|
"""Extract complete element data via CDP.
|
|
@@ -486,14 +488,16 @@ class DOMService:
|
|
|
486
488
|
self.state.browser_data["selections"] = {}
|
|
487
489
|
self._next_id = 1
|
|
488
490
|
logger.info("Selections cleared")
|
|
491
|
+
self._trigger_broadcast()
|
|
489
492
|
|
|
490
493
|
def cleanup(self) -> None:
|
|
491
494
|
"""Cleanup resources (executor, callbacks).
|
|
492
495
|
|
|
493
496
|
Call this before disconnect or app exit.
|
|
497
|
+
Safe to call multiple times.
|
|
494
498
|
"""
|
|
495
499
|
# Shutdown executor - wait=False to avoid blocking on stuck tasks
|
|
496
|
-
# cancel_futures=True prevents hanging on incomplete selections
|
|
500
|
+
# cancel_futures=True prevents hanging on incomplete selections
|
|
497
501
|
if hasattr(self, "_executor"):
|
|
498
502
|
try:
|
|
499
503
|
self._executor.shutdown(wait=False, cancel_futures=True)
|
|
@@ -501,12 +505,15 @@ class DOMService:
|
|
|
501
505
|
except Exception as e:
|
|
502
506
|
logger.debug(f"Executor shutdown error (non-fatal): {e}")
|
|
503
507
|
|
|
504
|
-
# Clear inspection state
|
|
505
|
-
if self._inspection_active:
|
|
508
|
+
# Clear inspection state (only if connected)
|
|
509
|
+
if self._inspection_active and self.cdp and self.cdp.is_connected:
|
|
506
510
|
try:
|
|
507
511
|
self.stop_inspect()
|
|
508
512
|
except Exception as e:
|
|
509
513
|
logger.debug(f"Failed to stop inspect on cleanup: {e}")
|
|
510
514
|
|
|
515
|
+
# Force clear inspection flag even if CDP call failed
|
|
516
|
+
self._inspection_active = False
|
|
517
|
+
|
|
511
518
|
|
|
512
519
|
__all__ = ["DOMService"]
|
webtap/services/fetch.py
CHANGED
|
@@ -21,6 +21,23 @@ class FetchService:
|
|
|
21
21
|
self.enable_response_stage = False # Config option for future
|
|
22
22
|
self.cdp: CDPSession | None = None
|
|
23
23
|
self.body_service: BodyService | None = None
|
|
24
|
+
self._broadcast_callback: "Any | None" = None # Callback to service._trigger_broadcast()
|
|
25
|
+
|
|
26
|
+
def set_broadcast_callback(self, callback: "Any") -> None:
|
|
27
|
+
"""Set callback for broadcasting state changes.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
callback: Function to call when state changes (service._trigger_broadcast)
|
|
31
|
+
"""
|
|
32
|
+
self._broadcast_callback = callback
|
|
33
|
+
|
|
34
|
+
def _trigger_broadcast(self) -> None:
|
|
35
|
+
"""Trigger SSE broadcast via service callback (ensures snapshot update)."""
|
|
36
|
+
if self._broadcast_callback:
|
|
37
|
+
try:
|
|
38
|
+
self._broadcast_callback()
|
|
39
|
+
except Exception as e:
|
|
40
|
+
logger.debug(f"Failed to trigger broadcast: {e}")
|
|
24
41
|
|
|
25
42
|
# ============= Core State Queries =============
|
|
26
43
|
|
|
@@ -147,6 +164,7 @@ class FetchService:
|
|
|
147
164
|
stage_msg = "Request and Response stages" if response_stage else "Request stage only"
|
|
148
165
|
logger.info(f"Fetch interception enabled ({stage_msg})")
|
|
149
166
|
|
|
167
|
+
self._trigger_broadcast() # Update snapshot
|
|
150
168
|
return {"enabled": True, "stages": stage_msg, "paused": self.paused_count}
|
|
151
169
|
|
|
152
170
|
except Exception as e:
|
|
@@ -174,6 +192,7 @@ class FetchService:
|
|
|
174
192
|
self.body_service.clear_cache()
|
|
175
193
|
|
|
176
194
|
logger.info("Fetch interception disabled")
|
|
195
|
+
self._trigger_broadcast() # Update snapshot
|
|
177
196
|
return {"enabled": False}
|
|
178
197
|
|
|
179
198
|
except Exception as e:
|
webtap/services/main.py
CHANGED
|
@@ -12,6 +12,7 @@ from webtap.services.network import NetworkService
|
|
|
12
12
|
from webtap.services.console import ConsoleService
|
|
13
13
|
from webtap.services.body import BodyService
|
|
14
14
|
from webtap.services.dom import DOMService
|
|
15
|
+
from webtap.services.state_snapshot import StateSnapshot
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
REQUIRED_DOMAINS = [
|
|
@@ -47,8 +48,11 @@ class WebTapService:
|
|
|
47
48
|
Args:
|
|
48
49
|
state: WebTapState instance from app.py
|
|
49
50
|
"""
|
|
51
|
+
import threading
|
|
52
|
+
|
|
50
53
|
self.state = state
|
|
51
54
|
self.cdp = state.cdp
|
|
55
|
+
self._state_lock = threading.RLock() # Reentrant lock - safe to acquire multiple times by same thread
|
|
52
56
|
|
|
53
57
|
self.enabled_domains: set[str] = set()
|
|
54
58
|
self.filters = FilterManager()
|
|
@@ -65,8 +69,10 @@ class WebTapService:
|
|
|
65
69
|
self.body.cdp = self.cdp
|
|
66
70
|
self.dom.set_cdp(self.cdp)
|
|
67
71
|
self.dom.set_state(self.state)
|
|
72
|
+
self.dom.set_broadcast_callback(self._trigger_broadcast) # DOM calls back for snapshot updates
|
|
68
73
|
|
|
69
74
|
self.fetch.body_service = self.body
|
|
75
|
+
self.fetch.set_broadcast_callback(self._trigger_broadcast) # Fetch calls back for snapshot updates
|
|
70
76
|
|
|
71
77
|
# Legacy wiring for CDP event handler
|
|
72
78
|
self.cdp.fetch_service = self.fetch
|
|
@@ -75,14 +81,135 @@ class WebTapService:
|
|
|
75
81
|
self.cdp.register_event_callback("Overlay.inspectNodeRequested", self.dom.handle_inspect_node_requested)
|
|
76
82
|
self.cdp.register_event_callback("Page.frameNavigated", self.dom.handle_frame_navigated)
|
|
77
83
|
|
|
84
|
+
# Register disconnect callback for unexpected disconnects
|
|
85
|
+
self.cdp.set_disconnect_callback(self._handle_unexpected_disconnect)
|
|
86
|
+
|
|
87
|
+
# Broadcast queue for SSE state updates (set by API server)
|
|
88
|
+
self._broadcast_queue: "Any | None" = None
|
|
89
|
+
|
|
90
|
+
# Immutable state snapshot for thread-safe SSE reads
|
|
91
|
+
# Updated atomically on every state change, read without locks
|
|
92
|
+
self._state_snapshot: StateSnapshot = StateSnapshot.create_empty()
|
|
93
|
+
|
|
94
|
+
def set_broadcast_queue(self, queue: "Any") -> None:
|
|
95
|
+
"""Set queue for broadcasting state changes.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
queue: asyncio.Queue for thread-safe signaling
|
|
99
|
+
"""
|
|
100
|
+
self._broadcast_queue = queue
|
|
101
|
+
|
|
102
|
+
def _create_snapshot(self) -> StateSnapshot:
|
|
103
|
+
"""Create immutable state snapshot from current state.
|
|
104
|
+
|
|
105
|
+
MUST be called with self._state_lock held to ensure atomic read.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Frozen StateSnapshot with current state
|
|
109
|
+
"""
|
|
110
|
+
# Connection state (read page_info first to avoid race with disconnect)
|
|
111
|
+
page_info = self.cdp.page_info
|
|
112
|
+
connected = self.cdp.is_connected and page_info is not None
|
|
113
|
+
page_id = page_info.get("id", "") if page_info else ""
|
|
114
|
+
page_title = page_info.get("title", "") if page_info else ""
|
|
115
|
+
page_url = page_info.get("url", "") if page_info else ""
|
|
116
|
+
|
|
117
|
+
# Event count
|
|
118
|
+
event_count = self.event_count
|
|
119
|
+
|
|
120
|
+
# Fetch state
|
|
121
|
+
fetch_enabled = self.fetch.enabled
|
|
122
|
+
paused_count = self.fetch.paused_count if fetch_enabled else 0
|
|
123
|
+
|
|
124
|
+
# Filter state (convert to immutable tuples)
|
|
125
|
+
fm = self.filters
|
|
126
|
+
filter_categories = list(fm.filters.keys())
|
|
127
|
+
enabled_filters = tuple(fm.enabled_categories)
|
|
128
|
+
disabled_filters = tuple(cat for cat in filter_categories if cat not in enabled_filters)
|
|
129
|
+
|
|
130
|
+
# Browser/DOM state (get_state() is already thread-safe internally)
|
|
131
|
+
browser_state = self.dom.get_state()
|
|
132
|
+
|
|
133
|
+
# Error state
|
|
134
|
+
error = self.state.error_state
|
|
135
|
+
error_message = error.get("message") if error else None
|
|
136
|
+
error_timestamp = error.get("timestamp") if error else None
|
|
137
|
+
|
|
138
|
+
# Deep copy selections to ensure true immutability
|
|
139
|
+
import copy
|
|
140
|
+
|
|
141
|
+
selections = copy.deepcopy(browser_state["selections"])
|
|
142
|
+
|
|
143
|
+
return StateSnapshot(
|
|
144
|
+
connected=connected,
|
|
145
|
+
page_id=page_id,
|
|
146
|
+
page_title=page_title,
|
|
147
|
+
page_url=page_url,
|
|
148
|
+
event_count=event_count,
|
|
149
|
+
fetch_enabled=fetch_enabled,
|
|
150
|
+
paused_count=paused_count,
|
|
151
|
+
enabled_filters=enabled_filters,
|
|
152
|
+
disabled_filters=disabled_filters,
|
|
153
|
+
inspect_active=browser_state["inspect_active"],
|
|
154
|
+
selections=selections, # Deep copy ensures nested dicts are immutable
|
|
155
|
+
prompt=browser_state["prompt"],
|
|
156
|
+
pending_count=browser_state["pending_count"],
|
|
157
|
+
error_message=error_message,
|
|
158
|
+
error_timestamp=error_timestamp,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def _trigger_broadcast(self) -> None:
|
|
162
|
+
"""Trigger SSE broadcast with updated state snapshot (thread-safe).
|
|
163
|
+
|
|
164
|
+
Called after service mutations to:
|
|
165
|
+
1. Create fresh immutable snapshot (atomic replacement)
|
|
166
|
+
2. Signal SSE clients to broadcast
|
|
167
|
+
|
|
168
|
+
Uses RLock so same thread can call multiple times safely.
|
|
169
|
+
asyncio.Queue.put_nowait() is thread-safe for cross-thread communication.
|
|
170
|
+
"""
|
|
171
|
+
import logging
|
|
172
|
+
|
|
173
|
+
logger = logging.getLogger(__name__)
|
|
174
|
+
|
|
175
|
+
# Update snapshot atomically
|
|
176
|
+
# RLock allows same thread to acquire multiple times, blocks other threads
|
|
177
|
+
try:
|
|
178
|
+
with self._state_lock:
|
|
179
|
+
self._state_snapshot = self._create_snapshot()
|
|
180
|
+
except (TypeError, AttributeError) as e:
|
|
181
|
+
# Programming errors should propagate for debugging
|
|
182
|
+
logger.error(f"Programming error in snapshot creation: {e}")
|
|
183
|
+
raise
|
|
184
|
+
except Exception as e:
|
|
185
|
+
# Unexpected errors logged but don't crash the app
|
|
186
|
+
logger.error(f"Failed to create state snapshot: {e}", exc_info=True)
|
|
187
|
+
return # Don't signal broadcast if snapshot creation failed
|
|
188
|
+
|
|
189
|
+
# Signal broadcast (store reference to avoid TOCTOU race)
|
|
190
|
+
queue = self._broadcast_queue
|
|
191
|
+
if queue:
|
|
192
|
+
try:
|
|
193
|
+
queue.put_nowait({"type": "state_change"})
|
|
194
|
+
except Exception as e:
|
|
195
|
+
logger.warning(f"Failed to queue broadcast: {e}")
|
|
196
|
+
|
|
197
|
+
def get_state_snapshot(self) -> StateSnapshot:
|
|
198
|
+
"""Get current immutable state snapshot (thread-safe, no locks).
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Current StateSnapshot - immutable, safe to read from any thread
|
|
202
|
+
"""
|
|
203
|
+
return self._state_snapshot
|
|
204
|
+
|
|
78
205
|
@property
|
|
79
206
|
def event_count(self) -> int:
|
|
80
207
|
"""Total count of all CDP events stored."""
|
|
81
208
|
if not self.cdp or not self.cdp.is_connected:
|
|
82
209
|
return 0
|
|
83
210
|
try:
|
|
84
|
-
result = self.cdp.
|
|
85
|
-
return result[0] if result else 0
|
|
211
|
+
result = self.cdp.query("SELECT COUNT(*) FROM events")
|
|
212
|
+
return result[0][0] if result else 0
|
|
86
213
|
except Exception:
|
|
87
214
|
return 0
|
|
88
215
|
|
|
@@ -105,6 +232,7 @@ class WebTapService:
|
|
|
105
232
|
self.filters.load()
|
|
106
233
|
|
|
107
234
|
page_info = self.cdp.page_info or {}
|
|
235
|
+
self._trigger_broadcast()
|
|
108
236
|
return {"connected": True, "title": page_info.get("title", "Untitled"), "url": page_info.get("url", "")}
|
|
109
237
|
except Exception as e:
|
|
110
238
|
return {"error": str(e)}
|
|
@@ -118,6 +246,7 @@ class WebTapService:
|
|
|
118
246
|
|
|
119
247
|
self.body.clear_cache()
|
|
120
248
|
self.dom.clear_selections()
|
|
249
|
+
self.dom.cleanup() # Shutdown executor properly
|
|
121
250
|
|
|
122
251
|
# Clear error state on disconnect
|
|
123
252
|
if self.state.error_state:
|
|
@@ -126,6 +255,7 @@ class WebTapService:
|
|
|
126
255
|
self.cdp.disconnect()
|
|
127
256
|
self.enabled_domains.clear()
|
|
128
257
|
|
|
258
|
+
self._trigger_broadcast()
|
|
129
259
|
return {"disconnected": True, "was_connected": was_connected}
|
|
130
260
|
|
|
131
261
|
def enable_domains(self, domains: list[str]) -> dict[str, str]:
|
|
@@ -175,6 +305,7 @@ class WebTapService:
|
|
|
175
305
|
def clear_events(self) -> dict[str, Any]:
|
|
176
306
|
"""Clear all stored CDP events."""
|
|
177
307
|
self.cdp.clear_events()
|
|
308
|
+
self._trigger_broadcast()
|
|
178
309
|
return {"cleared": True, "events": 0}
|
|
179
310
|
|
|
180
311
|
def list_pages(self) -> dict[str, Any]:
|
|
@@ -187,3 +318,64 @@ class WebTapService:
|
|
|
187
318
|
return {"pages": pages}
|
|
188
319
|
except Exception as e:
|
|
189
320
|
return {"error": str(e), "pages": []}
|
|
321
|
+
|
|
322
|
+
def _handle_unexpected_disconnect(self, code: int, reason: str) -> None:
|
|
323
|
+
"""Handle unexpected WebSocket disconnect (tab closed, crashed, etc).
|
|
324
|
+
|
|
325
|
+
Called from background thread by CDPSession._on_close.
|
|
326
|
+
Performs service-level cleanup and notifies SSE clients.
|
|
327
|
+
Events are preserved for debugging.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
code: WebSocket close code (e.g., 1006 = abnormal closure)
|
|
331
|
+
reason: Human-readable close reason
|
|
332
|
+
"""
|
|
333
|
+
import logging
|
|
334
|
+
import time
|
|
335
|
+
|
|
336
|
+
logger = logging.getLogger(__name__)
|
|
337
|
+
|
|
338
|
+
# Map WebSocket close codes to user-friendly messages
|
|
339
|
+
reason_map = {
|
|
340
|
+
1000: "Page closed normally",
|
|
341
|
+
1001: "Browser tab closed",
|
|
342
|
+
1006: "Connection lost (tab crashed or browser closed)",
|
|
343
|
+
1011: "Chrome internal error",
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
# Handle None code (abnormal closure with no code)
|
|
347
|
+
if code is None:
|
|
348
|
+
user_reason = "Connection lost (page closed or crashed)"
|
|
349
|
+
else:
|
|
350
|
+
user_reason = reason_map.get(code, f"Connection closed unexpectedly (code {code})")
|
|
351
|
+
|
|
352
|
+
logger.warning(f"Unexpected disconnect: {user_reason}")
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
# Thread-safe state cleanup (called from background thread)
|
|
356
|
+
with self._state_lock:
|
|
357
|
+
# Clean up service state (no CDP calls - connection already gone)
|
|
358
|
+
if self.fetch.enabled:
|
|
359
|
+
self.fetch.enabled = False # Direct state update, no CDP disable
|
|
360
|
+
|
|
361
|
+
self.body.clear_cache()
|
|
362
|
+
self.dom.clear_selections()
|
|
363
|
+
|
|
364
|
+
# Events preserved for debugging - use Clear button to remove explicitly
|
|
365
|
+
# DB thread and field_paths persist for reconnection
|
|
366
|
+
|
|
367
|
+
# Set error state with disconnect info
|
|
368
|
+
self.state.error_state = {"message": user_reason, "timestamp": time.time()}
|
|
369
|
+
|
|
370
|
+
self.enabled_domains.clear()
|
|
371
|
+
|
|
372
|
+
# Cleanup outside lock (safe to call multiple times, has internal protection)
|
|
373
|
+
self.dom.cleanup() # Shutdown executor
|
|
374
|
+
|
|
375
|
+
# Notify SSE clients
|
|
376
|
+
self._trigger_broadcast()
|
|
377
|
+
|
|
378
|
+
logger.info("Unexpected disconnect cleanup completed")
|
|
379
|
+
|
|
380
|
+
except Exception as e:
|
|
381
|
+
logger.error(f"Error during unexpected disconnect cleanup: {e}")
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Immutable state snapshots for thread-safe SSE broadcasting.
|
|
2
|
+
|
|
3
|
+
PUBLIC API:
|
|
4
|
+
- StateSnapshot: Frozen dataclass for zero-lock state reads
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class StateSnapshot:
|
|
13
|
+
"""Immutable snapshot of WebTap state.
|
|
14
|
+
|
|
15
|
+
Frozen dataclass provides inherent thread safety - multiple threads can
|
|
16
|
+
read simultaneously without locks. Updated atomically when state changes.
|
|
17
|
+
|
|
18
|
+
Used by SSE broadcast to avoid lock contention between asyncio event loop
|
|
19
|
+
and background threads (WebSocket, disconnect handlers).
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
connected: Whether connected to Chrome page
|
|
23
|
+
page_id: Stable page identifier (empty if not connected)
|
|
24
|
+
page_title: Page title (empty if not connected)
|
|
25
|
+
page_url: Page URL (empty if not connected)
|
|
26
|
+
event_count: Total CDP events stored
|
|
27
|
+
fetch_enabled: Whether fetch interception is active
|
|
28
|
+
paused_count: Number of paused requests (if fetch enabled)
|
|
29
|
+
enabled_filters: Tuple of enabled filter category names
|
|
30
|
+
disabled_filters: Tuple of disabled filter category names
|
|
31
|
+
inspect_active: Whether element inspection mode is active
|
|
32
|
+
selections: Dict of selected elements (id -> element data)
|
|
33
|
+
prompt: Browser prompt text (unused, reserved)
|
|
34
|
+
pending_count: Number of pending element selections being processed
|
|
35
|
+
error_message: Current error message or None
|
|
36
|
+
error_timestamp: Error timestamp or None
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# Connection state
|
|
40
|
+
connected: bool
|
|
41
|
+
page_id: str
|
|
42
|
+
page_title: str
|
|
43
|
+
page_url: str
|
|
44
|
+
|
|
45
|
+
# Event state
|
|
46
|
+
event_count: int
|
|
47
|
+
|
|
48
|
+
# Fetch interception state
|
|
49
|
+
fetch_enabled: bool
|
|
50
|
+
paused_count: int
|
|
51
|
+
|
|
52
|
+
# Filter state (immutable tuples)
|
|
53
|
+
enabled_filters: tuple[str, ...]
|
|
54
|
+
disabled_filters: tuple[str, ...]
|
|
55
|
+
|
|
56
|
+
# Browser/DOM state
|
|
57
|
+
inspect_active: bool
|
|
58
|
+
selections: dict[str, Any] # Dict is mutable but replaced atomically
|
|
59
|
+
prompt: str
|
|
60
|
+
pending_count: int
|
|
61
|
+
|
|
62
|
+
# Error state
|
|
63
|
+
error_message: str | None
|
|
64
|
+
error_timestamp: float | None
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def create_empty(cls) -> "StateSnapshot":
|
|
68
|
+
"""Create empty snapshot for disconnected state."""
|
|
69
|
+
return cls(
|
|
70
|
+
connected=False,
|
|
71
|
+
page_id="",
|
|
72
|
+
page_title="",
|
|
73
|
+
page_url="",
|
|
74
|
+
event_count=0,
|
|
75
|
+
fetch_enabled=False,
|
|
76
|
+
paused_count=0,
|
|
77
|
+
enabled_filters=(),
|
|
78
|
+
disabled_filters=(),
|
|
79
|
+
inspect_active=False,
|
|
80
|
+
selections={},
|
|
81
|
+
prompt="",
|
|
82
|
+
pending_count=0,
|
|
83
|
+
error_message=None,
|
|
84
|
+
error_timestamp=None,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
__all__ = ["StateSnapshot"]
|
|
@@ -1,51 +1,54 @@
|
|
|
1
1
|
webtap/VISION.md,sha256=kfoJfEPVc4chOrD9tNMDmYBY9rX9KB-286oZj70ALCE,7681
|
|
2
|
-
webtap/__init__.py,sha256=
|
|
3
|
-
webtap/api.py,sha256=
|
|
4
|
-
webtap/app.py,sha256=
|
|
2
|
+
webtap/__init__.py,sha256=gQPBOt8Q9Y9znQidBvIM5DbecNBTQe4XuoLK5WjFIfU,1250
|
|
3
|
+
webtap/api.py,sha256=w-7WGyR54fv980GF873w4T5TPErYcgx9PfRBNiddk64,18251
|
|
4
|
+
webtap/app.py,sha256=DG40lLVod4iSJP1TnrzxrEkkP4_sQ6G5euGubQqfEGQ,4073
|
|
5
5
|
webtap/filters.py,sha256=kRCicGMSV3R_zSvwzqZqksnry6jxJNdXRcgWvpoBLfc,13323
|
|
6
6
|
webtap/cdp/README.md,sha256=0TS0V_dRgRAzBqhddpXWD4S0YVi5wI4JgFJSll_KUBE,5660
|
|
7
7
|
webtap/cdp/__init__.py,sha256=c6NFG0XJnAa5GTe9MLr9mDZcLZqoTQN7A1cvvOfLcgY,453
|
|
8
8
|
webtap/cdp/query.py,sha256=x2Cy7KMolYkTelpROGezOfMFgYnbSlCvHkvvW1v_gLI,4229
|
|
9
|
-
webtap/cdp/session.py,sha256=
|
|
9
|
+
webtap/cdp/session.py,sha256=nm4fpHKiGskvHkhxvlmZULEVaY4PpLb7g7lmuwdEEG4,21573
|
|
10
10
|
webtap/cdp/schema/README.md,sha256=hnWCzbXYcYtWaZb_SgjVaFBiG81S9b9Y3x-euQFwQDo,1222
|
|
11
11
|
webtap/cdp/schema/cdp_protocol.json,sha256=dp9_OLYLuVsQb1oV5r6MZfMzURscBLyAXUckdaPWyv4,1488452
|
|
12
12
|
webtap/cdp/schema/cdp_version.json,sha256=OhGy1qpfQjSe3Z7OqL6KynBFlDFBXxKGPZCY-ZN_lVU,399
|
|
13
13
|
webtap/commands/DEVELOPER_GUIDE.md,sha256=LYOhycZ3k5EHx5nREfkjvLz7vOs8pXCRLlcDm-keWao,11973
|
|
14
|
-
webtap/commands/TIPS.md,sha256=
|
|
14
|
+
webtap/commands/TIPS.md,sha256=ssTcJr-nzoAOtTQOvuziDVoWGFk7pLa7RomwX79_kaA,14228
|
|
15
15
|
webtap/commands/__init__.py,sha256=rr3xM_bY0BgxkDOjsnsI8UBhjlz7nqiYlgJ8fjiJ1jQ,270
|
|
16
|
-
webtap/commands/_builders.py,sha256=
|
|
16
|
+
webtap/commands/_builders.py,sha256=VUHiObHfOveduILYEX711wnA-tfx6GL_8t_zuQ-CPQw,8146
|
|
17
|
+
webtap/commands/_code_generation.py,sha256=uBpusbIGCWqm6HwaWSuhFnRu_J5jm4rJ7qmx-esogf4,3288
|
|
17
18
|
webtap/commands/_tips.py,sha256=SleMpwdghrHNqdzR60Cu8T0NZqJfWfcfrgIcyWI6GIQ,4793
|
|
18
19
|
webtap/commands/_utils.py,sha256=VLXDhhhJITrQjwEyeLRTU2ey0QcLzY-_OxTjtPJlhYM,6816
|
|
19
|
-
webtap/commands/body.py,sha256=
|
|
20
|
-
webtap/commands/connection.py,sha256=
|
|
20
|
+
webtap/commands/body.py,sha256=wEBD1hCEopmPSnq7M_f5mU0Rb4EPGak9OVED4jo0ZW4,7278
|
|
21
|
+
webtap/commands/connection.py,sha256=o_Ny6dbQewNsPZ8QfdGZKY4mgqeAhqbRIqq56ksniFU,6264
|
|
21
22
|
webtap/commands/console.py,sha256=BBaxSiLsVBChBY3Xi_nXwWjFlfc5KW9FQTPp5PzMUoE,2145
|
|
22
23
|
webtap/commands/events.py,sha256=dsS6xd8GfkZ4VOnAQSCMxyEvwdha9J0Kh9oeK0CaU5Y,4058
|
|
23
24
|
webtap/commands/fetch.py,sha256=8J6TPBWhStbkN5c5Q4KmK6nB5WiIgnAk0BkPFbh9ggg,7737
|
|
24
25
|
webtap/commands/filters.py,sha256=jDZ8JcYIZv_K6aupwuAo9uqAi85e3EIKbf38BXz5nnI,10316
|
|
25
26
|
webtap/commands/inspect.py,sha256=QonZigFYnfEVWYQY__r0n1aVvTqFBukFV-AWzc5KmfA,5711
|
|
26
|
-
webtap/commands/javascript.py,sha256=
|
|
27
|
+
webtap/commands/javascript.py,sha256=d1aCs6VthNUkxGXRIWHxCzFTwrr0j81NVtGknsz75GU,4274
|
|
27
28
|
webtap/commands/launch.py,sha256=iZDLundKlxKRLKf3Vz5at42-tp2f-Uj5wZf7fbhBfA0,2202
|
|
28
|
-
webtap/commands/navigation.py,sha256=
|
|
29
|
+
webtap/commands/navigation.py,sha256=OBhCUnRvzF4aZ7c3YZUznr-yU4uuLBGTndDtkb1EJHg,6167
|
|
29
30
|
webtap/commands/network.py,sha256=gEOg_u7VF9A5aKv5myzLCuvfAUkF1OPxsuj4UAgbS44,3111
|
|
31
|
+
webtap/commands/quicktype.py,sha256=t4hAVxkAbwKcf_gZlsxWIUPIABoDrboeZMS937usXHg,9002
|
|
30
32
|
webtap/commands/selections.py,sha256=M001d_Gc51aSTuVeXGa19LDh2ZGR_qBJEjVGKpcGGFM,4895
|
|
31
33
|
webtap/commands/server.py,sha256=DOcIgYuKp0ydwrK9EA3hGwqOwfwM9DABhdPu3hk_jjo,6948
|
|
32
34
|
webtap/commands/setup.py,sha256=dov1LaN50nAEMNIuBLSK7mcnwhfn9rtqdTopBm1-PhA,9648
|
|
33
|
-
webtap/commands/to_model.py,sha256=
|
|
35
|
+
webtap/commands/to_model.py,sha256=M_X_Y6Eb6tU-Ar4UP43q5AHBfBlCXV0chEuwRJ-poOE,3214
|
|
34
36
|
webtap/services/README.md,sha256=rala_jtnNgSiQ1lFLM7x_UQ4SJZDceAm7dpkQMRTYaI,2346
|
|
35
37
|
webtap/services/__init__.py,sha256=IjFqu0Ak6D-r18aokcQMtenDV3fbelvfjTCejGv6CZ0,570
|
|
36
|
-
webtap/services/body.py,sha256=
|
|
38
|
+
webtap/services/body.py,sha256=5c1YO3xKHU4qHFLeczag0m91PoSzp9-tBE30SENJEzg,11586
|
|
37
39
|
webtap/services/console.py,sha256=XVfSKTvEHyyOdujsg85S3wtj1CdZhzKtWwlx25MvSv8,3768
|
|
38
|
-
webtap/services/dom.py,sha256=
|
|
39
|
-
webtap/services/fetch.py,sha256
|
|
40
|
-
webtap/services/main.py,sha256=
|
|
40
|
+
webtap/services/dom.py,sha256=JNYHFrgPpJnMZJ3s267R5m6xFWJwKA8Fwn3XGln6wxs,19399
|
|
41
|
+
webtap/services/fetch.py,sha256=-ZsfEfF3iFge3fiyLniflU0v0In-SxJMb7VtIe7_sjU,14417
|
|
42
|
+
webtap/services/main.py,sha256=kHKHtLnwR3rnpbtJXZZtAdNmD_dg0ejvd-TvrkXTqLg,13948
|
|
41
43
|
webtap/services/network.py,sha256=0o_--F6YvmXqqFqrcjL1gc6Vr9V1Ytb_U7r_DSUWupA,3444
|
|
44
|
+
webtap/services/state_snapshot.py,sha256=xy7q-QaDhrHrEKVctJNH0t14zzI4DNmlt_-4C5ipr2M,2701
|
|
42
45
|
webtap/services/setup/__init__.py,sha256=lfoKCAroc-JoE_r7L-KZkF85ZWiB41MBIgrR7ZISSoE,7157
|
|
43
46
|
webtap/services/setup/chrome.py,sha256=zfPWeb6zm_xjIfiS2S_O9lR2BjGKaPXXo06pN_B9lAU,7187
|
|
44
47
|
webtap/services/setup/desktop.py,sha256=fXwQa201W-s2mengm_dJZ9BigJopVrO9YFUQcW_TSFQ,8022
|
|
45
48
|
webtap/services/setup/extension.py,sha256=iJY43JlQO6Vicgd9Mz6Mw0LQfbBNUGhnwI8n-LnvHBY,3602
|
|
46
49
|
webtap/services/setup/filters.py,sha256=lAPSLMH_KZQO-7bRkmURwzforx7C3SDrKEw2ZogN-Lo,3220
|
|
47
50
|
webtap/services/setup/platform.py,sha256=7yn-7LQFffgerWzWRtOG-yNEsR36ICThYUAu_N2FAso,4532
|
|
48
|
-
webtap_tool-0.
|
|
49
|
-
webtap_tool-0.
|
|
50
|
-
webtap_tool-0.
|
|
51
|
-
webtap_tool-0.
|
|
51
|
+
webtap_tool-0.8.0.dist-info/METADATA,sha256=6EJihElxEAWvWgBttK2EXsdNPJc95JKY96FzZ50Z4Hs,17636
|
|
52
|
+
webtap_tool-0.8.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
53
|
+
webtap_tool-0.8.0.dist-info/entry_points.txt,sha256=iFe575I0CIb1MbfPt0oX2VYyY5gSU_dA551PKVR83TU,39
|
|
54
|
+
webtap_tool-0.8.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|