webtap-tool 0.3.0__py3-none-any.whl → 0.5.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/services/dom.py ADDED
@@ -0,0 +1,512 @@
1
+ """DOM inspection service using Chrome DevTools Protocol.
2
+
3
+ PUBLIC API:
4
+ - DOMService: Manages element inspection and selection via CDP Overlay domain
5
+ """
6
+
7
+ import logging
8
+ import re
9
+ import threading
10
+ from concurrent.futures import ThreadPoolExecutor
11
+ from typing import Any, TYPE_CHECKING
12
+
13
+ if TYPE_CHECKING:
14
+ from webtap.cdp.session import CDPSession
15
+ from webtap.app import WebTapState
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class DOMService:
21
+ """Manages element inspection and selection via CDP Overlay domain.
22
+
23
+ Uses CDP's native inspect mode (Overlay.setInspectMode) which provides:
24
+ - Native Chrome highlight on hover (no custom overlay needed)
25
+ - Click events via Overlay.inspectNodeRequested
26
+ - Accurate element data via DOM.describeNode, CSS.getComputedStyleForNode
27
+
28
+ Selections are stored in state.browser_data (not DuckDB) as they are
29
+ ephemeral session data cleared after prompt submission.
30
+
31
+ Attributes:
32
+ cdp: CDP session for executing commands
33
+ state: WebTap state for storing selections
34
+ _inspection_active: Whether inspect mode is currently active
35
+ _next_id: Counter for assigning selection IDs
36
+ """
37
+
38
+ def __init__(self, cdp: "CDPSession | None" = None, state: "WebTapState | None" = None):
39
+ """Initialize DOM service.
40
+
41
+ Args:
42
+ cdp: CDPSession instance. Can be None initially, set via set_cdp().
43
+ state: WebTapState instance. Can be None initially, set via set_state().
44
+ """
45
+ self.cdp = cdp
46
+ self.state = state
47
+ self._inspection_active = False
48
+ self._next_id = 1
49
+ self._broadcast_queue: "Any | None" = None # asyncio.Queue for thread-safe broadcasts
50
+ self._state_lock = threading.Lock() # Protect state mutations
51
+ self._pending_selections = 0 # Track in-flight selection processing
52
+ self._executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="dom-worker")
53
+
54
+ def set_cdp(self, cdp: "CDPSession") -> None:
55
+ """Set CDP session after initialization."""
56
+ self.cdp = cdp
57
+
58
+ def set_state(self, state: "WebTapState") -> None:
59
+ """Set state after initialization."""
60
+ self.state = state
61
+
62
+ def set_broadcast_queue(self, queue: "Any") -> None:
63
+ """Set queue for broadcasting state changes.
64
+
65
+ Args:
66
+ queue: asyncio.Queue for thread-safe signaling
67
+ """
68
+ self._broadcast_queue = queue
69
+
70
+ def start_inspect(self) -> dict[str, Any]:
71
+ """Enable CDP element inspection mode.
72
+
73
+ Enables Overlay.setInspectMode with searchForNode mode, which:
74
+ - Shows native Chrome highlight on hover
75
+ - Fires Overlay.inspectNodeRequested on click
76
+
77
+ Returns:
78
+ Success status dictionary.
79
+ """
80
+ if not self.cdp or not self.cdp.ws_app:
81
+ return {"error": "Not connected to page"}
82
+
83
+ if self._inspection_active:
84
+ return {"error": "Inspection already active"}
85
+
86
+ try:
87
+ # Enable DOM domain first (Overlay depends on it)
88
+ self.cdp.execute("DOM.enable")
89
+
90
+ # Request document to establish DOM tree context
91
+ # REQUIRED: BackendNodeIds only work after getDocument() is called
92
+ self.cdp.execute("DOM.getDocument", {"depth": -1})
93
+
94
+ # Enable CSS domain (needed for computed styles)
95
+ self.cdp.execute("CSS.enable")
96
+
97
+ # Enable Overlay domain
98
+ self.cdp.execute("Overlay.enable")
99
+
100
+ # Set inspect mode with native Chrome highlighting
101
+ self.cdp.execute(
102
+ "Overlay.setInspectMode",
103
+ {
104
+ "mode": "searchForNode",
105
+ "highlightConfig": {
106
+ "showInfo": True,
107
+ "showStyles": True,
108
+ "contentColor": {"r": 111, "g": 168, "b": 220, "a": 0.66},
109
+ "paddingColor": {"r": 147, "g": 196, "b": 125, "a": 0.55},
110
+ "borderColor": {"r": 255, "g": 229, "b": 153, "a": 0.66},
111
+ "marginColor": {"r": 246, "g": 178, "b": 107, "a": 0.66},
112
+ },
113
+ },
114
+ )
115
+
116
+ self._inspection_active = True
117
+ logger.info("Element inspection mode enabled")
118
+
119
+ return {"success": True, "inspect_active": True}
120
+
121
+ except Exception as e:
122
+ logger.error(f"Failed to enable inspection mode: {e}")
123
+ return {"error": str(e)}
124
+
125
+ def stop_inspect(self) -> dict[str, Any]:
126
+ """Disable CDP element inspection mode.
127
+
128
+ Returns:
129
+ Success status dictionary.
130
+ """
131
+ if not self.cdp or not self.cdp.ws_app:
132
+ return {"error": "Not connected to page"}
133
+
134
+ if not self._inspection_active:
135
+ return {"success": True, "inspect_active": False}
136
+
137
+ try:
138
+ # Disable inspect mode
139
+ # NOTE: highlightConfig required even for mode=none, otherwise CDP throws:
140
+ # "Internal error: highlight configuration parameter is missing"
141
+ self.cdp.execute("Overlay.setInspectMode", {"mode": "none", "highlightConfig": {}})
142
+
143
+ self._inspection_active = False
144
+ logger.info("Element inspection mode disabled")
145
+
146
+ return {"success": True, "inspect_active": False}
147
+
148
+ except Exception as e:
149
+ logger.error(f"Failed to disable inspection mode: {e}")
150
+ return {"error": str(e)}
151
+
152
+ def handle_inspect_node_requested(self, event: dict) -> None:
153
+ """Handle Overlay.inspectNodeRequested event (user clicked element).
154
+
155
+ CRITICAL: Called from WebSocket thread - MUST NOT make blocking CDP calls!
156
+ Offload to background thread to avoid deadlock.
157
+
158
+ Args:
159
+ event: CDP event with method and params
160
+ """
161
+ if not self.cdp or not self.state:
162
+ logger.error("DOMService not properly initialized (missing cdp or state)")
163
+ return
164
+
165
+ params = event.get("params", {})
166
+ backend_node_id = params.get("backendNodeId")
167
+ if not backend_node_id:
168
+ logger.warning("inspectNodeRequested event missing backendNodeId")
169
+ return
170
+
171
+ # Increment pending counter (thread-safe)
172
+ with self._state_lock:
173
+ self._pending_selections += 1
174
+ self._trigger_broadcast()
175
+
176
+ # Submit to background thread - returns immediately, no blocking
177
+ self._executor.submit(self._process_node_selection, backend_node_id)
178
+
179
+ def handle_frame_navigated(self, event: dict) -> None:
180
+ """Handle Page.frameNavigated event (page navigation).
181
+
182
+ Clears selections when main frame navigates to keep state in sync with page.
183
+ Called from WebSocket thread - must be non-blocking.
184
+
185
+ Args:
186
+ event: CDP event with method and params
187
+ """
188
+ params = event.get("params", {})
189
+ frame = params.get("frame", {})
190
+
191
+ # Only clear on main frame navigation (not iframes)
192
+ if frame.get("parentId"):
193
+ return
194
+
195
+ logger.info("Main frame navigated - clearing selections")
196
+ self.clear_selections()
197
+ self._trigger_broadcast()
198
+
199
+ def _process_node_selection(self, backend_node_id: int) -> None:
200
+ """Process node selection in background thread.
201
+
202
+ Safe to make blocking CDP calls here - we're not in WebSocket thread.
203
+
204
+ Args:
205
+ backend_node_id: CDP backend node ID from inspectNodeRequested event
206
+ """
207
+ try:
208
+ # Make blocking CDP calls (OK in background thread)
209
+ data = self._extract_node_data(backend_node_id)
210
+
211
+ # Thread-safe state update
212
+ with self._state_lock:
213
+ if not self.state:
214
+ logger.error("DOMService state not initialized")
215
+ return
216
+
217
+ selection_id = str(self._next_id)
218
+ self._next_id += 1
219
+
220
+ if not self.state.browser_data:
221
+ self.state.browser_data = {"selections": {}, "prompt": ""}
222
+ if "selections" not in self.state.browser_data:
223
+ self.state.browser_data["selections"] = {}
224
+
225
+ self.state.browser_data["selections"][selection_id] = data
226
+
227
+ logger.info(f"Element selected: {selection_id} - {data.get('preview', {}).get('tag', 'unknown')}")
228
+
229
+ except Exception as e:
230
+ logger.error(f"Failed to process node selection: {e}")
231
+ # Set error state for UI display
232
+ if self.state:
233
+ import time
234
+
235
+ error_msg = str(e)
236
+ # Provide user-friendly message for common errors
237
+ if "timed out" in error_msg.lower() or isinstance(e, TimeoutError):
238
+ error_msg = "Element selection timed out - page may be unresponsive"
239
+ self.state.error_state = {"message": error_msg, "timestamp": time.time()}
240
+ finally:
241
+ # Decrement pending counter (thread-safe)
242
+ with self._state_lock:
243
+ self._pending_selections -= 1
244
+ self._trigger_broadcast()
245
+
246
+ def _trigger_broadcast(self) -> None:
247
+ """Trigger SSE broadcast via queue (thread-safe helper)."""
248
+ if self._broadcast_queue:
249
+ try:
250
+ self._broadcast_queue.put_nowait({"type": "dom_update"})
251
+ except Exception as e:
252
+ logger.debug(f"Failed to queue broadcast: {e}")
253
+
254
+ def _extract_node_data(self, backend_node_id: int) -> dict[str, Any]:
255
+ """Extract complete element data via CDP.
256
+
257
+ Args:
258
+ backend_node_id: CDP backend node ID from inspectNodeRequested event
259
+
260
+ Returns:
261
+ Dictionary with element data compatible with browser_data schema
262
+
263
+ Raises:
264
+ RuntimeError: If CDP is not connected or commands fail
265
+ TimeoutError: If CDP commands timeout (page busy, heavy load)
266
+ """
267
+ if not self.cdp:
268
+ raise RuntimeError("CDP session not initialized")
269
+
270
+ # Use 15s timeout for interactive operations (balanced between responsiveness and heavy pages)
271
+ # Still shorter than default 30s to provide faster failure feedback
272
+ timeout = 15.0
273
+
274
+ try:
275
+ # Describe node directly with backendNodeId (no need for resolveNode first!)
276
+ describe_result = self.cdp.execute("DOM.describeNode", {"backendNodeId": backend_node_id}, timeout=timeout)
277
+
278
+ if "node" not in describe_result:
279
+ raise RuntimeError(f"Failed to describe node {backend_node_id}")
280
+
281
+ node = describe_result["node"]
282
+ node_id = node["nodeId"]
283
+
284
+ # Get outer HTML
285
+ html_result = self.cdp.execute("DOM.getOuterHTML", {"nodeId": node_id}, timeout=timeout)
286
+ outer_html = html_result.get("outerHTML", "")
287
+
288
+ # Get computed styles
289
+ styles_result = self.cdp.execute("CSS.getComputedStyleForNode", {"nodeId": node_id}, timeout=timeout)
290
+
291
+ # Convert styles to dict
292
+ styles = {}
293
+ for prop in styles_result.get("computedStyle", []):
294
+ styles[prop["name"]] = prop["value"]
295
+
296
+ # Get box model for badge positioning
297
+ try:
298
+ box_result = self.cdp.execute("DOM.getBoxModel", {"nodeId": node_id}, timeout=timeout)
299
+ # Use top-left corner of content box
300
+ content_box = box_result["model"]["content"]
301
+ badge_x = int(content_box[0]) # Top-left x
302
+ badge_y = int(content_box[1]) # Top-left y
303
+ except Exception:
304
+ # Fallback if element has no box model (display: none, etc.)
305
+ badge_x = 0
306
+ badge_y = 0
307
+
308
+ except TimeoutError as e:
309
+ logger.warning(f"Timeout extracting node {backend_node_id}: {e}")
310
+ raise RuntimeError("Element selection timed out - page may be busy or unresponsive") from e
311
+
312
+ # Generate CSS selector
313
+ css_selector = self._generate_css_selector(node)
314
+
315
+ # Generate XPath
316
+ xpath = self._generate_xpath(node)
317
+
318
+ # Generate jsPath (for js() command integration)
319
+ js_path = f"document.querySelector('{css_selector}')"
320
+
321
+ # Build preview
322
+ tag = node.get("nodeName", "").lower()
323
+ node_attrs = node.get("attributes", [])
324
+ attrs_dict = {}
325
+ for i in range(0, len(node_attrs), 2):
326
+ if i + 1 < len(node_attrs):
327
+ attrs_dict[node_attrs[i]] = node_attrs[i + 1]
328
+
329
+ preview = {
330
+ "tag": tag,
331
+ "id": attrs_dict.get("id", ""),
332
+ "classes": attrs_dict.get("class", "").split() if attrs_dict.get("class") else [],
333
+ "text": self._get_node_text(outer_html)[:100], # First 100 chars
334
+ }
335
+
336
+ # Build complete data structure (compatible with existing schema)
337
+ return {
338
+ "outerHTML": outer_html,
339
+ "selector": css_selector,
340
+ "jsPath": js_path,
341
+ "styles": styles,
342
+ "xpath": xpath,
343
+ "fullXpath": xpath, # CDP doesn't distinguish, use same
344
+ "preview": preview,
345
+ "badge": {"x": badge_x, "y": badge_y},
346
+ "nodeId": node_id,
347
+ "backendNodeId": backend_node_id,
348
+ }
349
+
350
+ def _generate_css_selector(self, node: dict) -> str:
351
+ """Generate unique CSS selector for node.
352
+
353
+ Uses a combination of strategies to ensure uniqueness:
354
+ 1. ID if available (most unique)
355
+ 2. Tag + classes + nth-child for specificity
356
+ 3. Falls back to full path if needed
357
+
358
+ Args:
359
+ node: CDP node description
360
+
361
+ Returns:
362
+ CSS selector string
363
+ """
364
+ # Parse attributes
365
+ attrs_dict = self._parse_node_attributes(node)
366
+
367
+ # Strategy 1: ID selector (unique by definition)
368
+ if "id" in attrs_dict and attrs_dict["id"]:
369
+ return f"#{attrs_dict['id']}"
370
+
371
+ # Strategy 2: Build selector with tag + classes + nth-child
372
+ tag = node.get("nodeName", "").lower()
373
+ selector = tag
374
+
375
+ # Add first 2 classes for specificity without being too brittle
376
+ if "class" in attrs_dict and attrs_dict["class"]:
377
+ classes = attrs_dict["class"].split()[:2]
378
+ if classes:
379
+ selector += "." + ".".join(classes)
380
+
381
+ # Add nth-child for uniqueness within parent
382
+ # This is key to distinguishing elements with same tag/class
383
+ parent_id = node.get("parentId")
384
+ if parent_id and self.cdp:
385
+ try:
386
+ # Get parent node to count children
387
+ parent_result = self.cdp.execute("DOM.describeNode", {"nodeId": parent_id}, timeout=5.0)
388
+
389
+ if "node" in parent_result:
390
+ parent_node = parent_result["node"]
391
+ child_node_ids = parent_node.get("childNodeIds", [])
392
+
393
+ # Find our position among siblings
394
+ node_id = node.get("nodeId")
395
+ if node_id in child_node_ids:
396
+ nth = child_node_ids.index(node_id) + 1
397
+ selector += f":nth-child({nth})"
398
+
399
+ except Exception as e:
400
+ logger.debug(f"Could not add nth-child to selector: {e}")
401
+
402
+ return selector
403
+
404
+ def _parse_node_attributes(self, node: dict) -> dict:
405
+ """Parse CDP node attributes array into dictionary.
406
+
407
+ Args:
408
+ node: CDP node with attributes array [name1, value1, name2, value2, ...]
409
+
410
+ Returns:
411
+ Dictionary of {name: value}
412
+ """
413
+ attrs = node.get("attributes", [])
414
+ attrs_dict = {}
415
+ for i in range(0, len(attrs), 2):
416
+ if i + 1 < len(attrs):
417
+ attrs_dict[attrs[i]] = attrs[i + 1]
418
+ return attrs_dict
419
+
420
+ def _generate_xpath(self, node: dict) -> str:
421
+ """Generate XPath for node.
422
+
423
+ Args:
424
+ node: CDP node description
425
+
426
+ Returns:
427
+ XPath string
428
+ """
429
+ tag = node.get("nodeName", "").lower()
430
+ attrs_dict = self._parse_node_attributes(node)
431
+
432
+ # Prefer ID (unique)
433
+ if "id" in attrs_dict and attrs_dict["id"]:
434
+ return f"//{tag}[@id='{attrs_dict['id']}']"
435
+
436
+ # Use class attribute if available
437
+ if "class" in attrs_dict and attrs_dict["class"]:
438
+ # XPath class matching (contains all classes)
439
+ classes = attrs_dict["class"].split()
440
+ if classes:
441
+ return f"//{tag}[@class='{attrs_dict['class']}']"
442
+
443
+ # Fallback to tag only
444
+ return f"//{tag}"
445
+
446
+ def _get_node_text(self, html: str) -> str:
447
+ """Extract text content from HTML (simple implementation).
448
+
449
+ Args:
450
+ html: Outer HTML string
451
+
452
+ Returns:
453
+ Extracted text content
454
+ """
455
+ # Simple regex to strip tags
456
+ text = re.sub(r"<[^>]+>", "", html)
457
+ return text.strip()
458
+
459
+ def get_state(self) -> dict[str, Any]:
460
+ """Get current DOM service state (thread-safe).
461
+
462
+ Returns:
463
+ State dictionary with inspect_active, selections, and pending count
464
+ """
465
+ # Thread-safe read: protect against concurrent writes from WebSocket thread
466
+ with self._state_lock:
467
+ selections = {}
468
+ prompt = ""
469
+
470
+ if self.state is not None and self.state.browser_data:
471
+ # Deep copy to prevent mutations during SSE broadcast
472
+ selections = dict(self.state.browser_data.get("selections", {}))
473
+ prompt = self.state.browser_data.get("prompt", "")
474
+
475
+ return {
476
+ "inspect_active": self._inspection_active,
477
+ "selections": selections,
478
+ "prompt": prompt,
479
+ "pending_count": self._pending_selections, # For progress indicator
480
+ }
481
+
482
+ def clear_selections(self) -> None:
483
+ """Clear all selections (thread-safe)."""
484
+ with self._state_lock:
485
+ if self.state is not None and self.state.browser_data:
486
+ self.state.browser_data["selections"] = {}
487
+ self._next_id = 1
488
+ logger.info("Selections cleared")
489
+
490
+ def cleanup(self) -> None:
491
+ """Cleanup resources (executor, callbacks).
492
+
493
+ Call this before disconnect or app exit.
494
+ """
495
+ # Shutdown executor - wait=False to avoid blocking on stuck tasks
496
+ # cancel_futures=True prevents hanging on incomplete selections (Python 3.9+)
497
+ if hasattr(self, "_executor"):
498
+ try:
499
+ self._executor.shutdown(wait=False, cancel_futures=True)
500
+ logger.info("ThreadPoolExecutor shut down")
501
+ except Exception as e:
502
+ logger.debug(f"Executor shutdown error (non-fatal): {e}")
503
+
504
+ # Clear inspection state
505
+ if self._inspection_active:
506
+ try:
507
+ self.stop_inspect()
508
+ except Exception as e:
509
+ logger.debug(f"Failed to stop inspect on cleanup: {e}")
510
+
511
+
512
+ __all__ = ["DOMService"]
webtap/services/main.py CHANGED
@@ -11,6 +11,7 @@ from webtap.services.fetch import FetchService
11
11
  from webtap.services.network import NetworkService
12
12
  from webtap.services.console import ConsoleService
13
13
  from webtap.services.body import BodyService
14
+ from webtap.services.dom import DOMService
14
15
 
15
16
 
16
17
  REQUIRED_DOMAINS = [
@@ -37,6 +38,7 @@ class WebTapService:
37
38
  network: Network monitoring service.
38
39
  console: Console message service.
39
40
  body: Response body fetching service.
41
+ dom: DOM inspection and element selection service.
40
42
  """
41
43
 
42
44
  def __init__(self, state):
@@ -55,17 +57,24 @@ class WebTapService:
55
57
  self.network = NetworkService()
56
58
  self.console = ConsoleService()
57
59
  self.body = BodyService()
60
+ self.dom = DOMService()
58
61
 
59
62
  self.fetch.cdp = self.cdp
60
63
  self.network.cdp = self.cdp
61
64
  self.console.cdp = self.cdp
62
65
  self.body.cdp = self.cdp
66
+ self.dom.set_cdp(self.cdp)
67
+ self.dom.set_state(self.state)
63
68
 
64
69
  self.fetch.body_service = self.body
65
70
 
66
71
  # Legacy wiring for CDP event handler
67
72
  self.cdp.fetch_service = self.fetch
68
73
 
74
+ # Register DOM event callbacks
75
+ self.cdp.register_event_callback("Overlay.inspectNodeRequested", self.dom.handle_inspect_node_requested)
76
+ self.cdp.register_event_callback("Page.frameNavigated", self.dom.handle_frame_navigated)
77
+
69
78
  @property
70
79
  def event_count(self) -> int:
71
80
  """Total count of all CDP events stored."""
@@ -108,6 +117,11 @@ class WebTapService:
108
117
  self.fetch.disable()
109
118
 
110
119
  self.body.clear_cache()
120
+ self.dom.clear_selections()
121
+
122
+ # Clear error state on disconnect
123
+ if self.state.error_state:
124
+ self.state.error_state = None
111
125
 
112
126
  self.cdp.disconnect()
113
127
  self.enabled_domains.clear()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webtap-tool
3
- Version: 0.3.0
3
+ Version: 0.5.0
4
4
  Summary: Terminal-based web page inspector for AI debugging sessions
5
5
  Author-email: Fredrik Angelsen <fredrikangelsen@gmail.com>
6
6
  Classifier: Development Status :: 3 - Alpha
@@ -21,7 +21,7 @@ Requires-Dist: platformdirs>=4.4.0
21
21
  Requires-Dist: protobuf>=6.32.0
22
22
  Requires-Dist: pyjwt>=2.10.1
23
23
  Requires-Dist: pyyaml>=6.0.2
24
- Requires-Dist: replkit2[all]>=0.11.0
24
+ Requires-Dist: replkit2[all]>=0.12.0
25
25
  Requires-Dist: requests>=2.32.4
26
26
  Requires-Dist: uvicorn>=0.35.0
27
27
  Requires-Dist: websocket-client>=1.8.0
@@ -1,41 +1,42 @@
1
1
  webtap/VISION.md,sha256=kfoJfEPVc4chOrD9tNMDmYBY9rX9KB-286oZj70ALCE,7681
2
2
  webtap/__init__.py,sha256=DFWJGmqZfX8_h4csLA5pKPR4SkaHBMlUgU-WQIE96Gw,1092
3
- webtap/api.py,sha256=QLfwO_21uSyxBCsqei45c5Uyg7OVfaVopmBncx9ZRfw,8018
4
- webtap/app.py,sha256=OC8-767GtQ_hAOxUNqD6Yu3JYLNB3ZXdzKn-A1S_RJI,3305
5
- webtap/filters.py,sha256=nphF2bFRbFtS2ue-CbV1uzKpuK3IYBbwjLeLhMDdLEk,11034
3
+ webtap/api.py,sha256=twDO_aA861yjjvrKquZ0phm2SIL07Wq37DmFjtVbTw4,17990
4
+ webtap/app.py,sha256=UipzpRGS6riZxU7CgyW_eeWcB-6817P3egEO0pIdcp4,3790
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=BTajmDH6nel2dvia7IX9k6C-RinkN2NxY4rxtUJAJE0,12362
9
+ webtap/cdp/session.py,sha256=LkN676_gig06w9kcXhJXZQF0z0kDZG1TTvispiYw8fQ,16212
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
- webtap/commands/DEVELOPER_GUIDE.md,sha256=lii-bOUVNBiFjpZlYIipnFNNfUI9vI5VwAhGi2jvQAU,9870
14
- webtap/commands/TIPS.md,sha256=gE75m66QxU-7RG8NS9z5EchMoEN2OrQ0ZzHMETsAd_w,6854
13
+ webtap/commands/DEVELOPER_GUIDE.md,sha256=LYOhycZ3k5EHx5nREfkjvLz7vOs8pXCRLlcDm-keWao,11973
14
+ webtap/commands/TIPS.md,sha256=LnzNHb6fhoeqEyNIhx5IOKd05nY-xCiDLCrJ5zBOmNY,8131
15
15
  webtap/commands/__init__.py,sha256=rr3xM_bY0BgxkDOjsnsI8UBhjlz7nqiYlgJ8fjiJ1jQ,270
16
- webtap/commands/_builders.py,sha256=nUZLcMWXVWx6KmEV9KLajV-Gbxv2m2NB5lNZgR5Be1c,3842
17
- webtap/commands/_errors.py,sha256=W64bGcUDsvdncD3WiL4H7hVTVE58OiwWYC391SAk43s,3400
16
+ webtap/commands/_builders.py,sha256=SYacZmZTdkolQ7OOf3rFtFPCjkukY8z020WFA-i_O_A,7902
18
17
  webtap/commands/_tips.py,sha256=SleMpwdghrHNqdzR60Cu8T0NZqJfWfcfrgIcyWI6GIQ,4793
19
18
  webtap/commands/_utils.py,sha256=VLXDhhhJITrQjwEyeLRTU2ey0QcLzY-_OxTjtPJlhYM,6816
20
- webtap/commands/body.py,sha256=e67q_rT8vtRz5_ViQjl5a6BAolW4uwedRZkVp8TJ5Vo,7111
21
- webtap/commands/connection.py,sha256=ZYV2TmK1LRVFyMneNYswJmnaoi45rFTApQew5Gm-CC0,5465
22
- webtap/commands/console.py,sha256=moGLsZ-k5wtukjrPFkEXjBl-Jj_yj4bHEArPXVmZLUc,2180
23
- webtap/commands/events.py,sha256=yx3iJgTANKsoGXBMu1WfBOjEW_thmNKMmUTXtamqRtQ,4093
24
- webtap/commands/fetch.py,sha256=_TzOvJfVzPaw4ZmyI95Qb7rS3iKx2nmp_IL3jaQO_6g,7772
25
- webtap/commands/filters.py,sha256=trZvcbHTaF0FZC8dyMAmhmS2dlBoA82VXe5_DDS3eU8,8986
26
- webtap/commands/inspect.py,sha256=6PGN7iDT1oLzQJboNeYozLILrW2VsAzmtMpF3_XhD30,5746
27
- webtap/commands/javascript.py,sha256=QpQdqqoQwwTyz1lpibZ92XKOL89scu_ndgSjkhaYuDk,3195
19
+ webtap/commands/body.py,sha256=hJAoVU3iWBYCctPwBxVJM_xVe02TQnePB_OMTpsh1q4,7076
20
+ webtap/commands/connection.py,sha256=nvJ5k1KlyphTeVW9yJaATHo4N-hX7w62pf1-fAypSRk,5430
21
+ webtap/commands/console.py,sha256=BBaxSiLsVBChBY3Xi_nXwWjFlfc5KW9FQTPp5PzMUoE,2145
22
+ webtap/commands/events.py,sha256=dsS6xd8GfkZ4VOnAQSCMxyEvwdha9J0Kh9oeK0CaU5Y,4058
23
+ webtap/commands/fetch.py,sha256=8J6TPBWhStbkN5c5Q4KmK6nB5WiIgnAk0BkPFbh9ggg,7737
24
+ webtap/commands/filters.py,sha256=jDZ8JcYIZv_K6aupwuAo9uqAi85e3EIKbf38BXz5nnI,10316
25
+ webtap/commands/inspect.py,sha256=QonZigFYnfEVWYQY__r0n1aVvTqFBukFV-AWzc5KmfA,5711
26
+ webtap/commands/javascript.py,sha256=9n4gOA7cjFjCL_p1cePRS1j7WZRV771MwADZeC5g66s,4098
28
27
  webtap/commands/launch.py,sha256=iZDLundKlxKRLKf3Vz5at42-tp2f-Uj5wZf7fbhBfA0,2202
29
- webtap/commands/navigation.py,sha256=Mapawp2AZTJQaws2uwlTgMUhqz7HlVTLxiZ06n_MQc0,6071
30
- webtap/commands/network.py,sha256=hwZshGGdVsJ_9MFjOKJXT07I990JjZInw2LLnKXLQ5Y,2910
31
- webtap/commands/server.py,sha256=LSs3l3Pb_vwmWRnYH-sA9JUPxBTQtRedjFQ4KvDaZK0,6032
28
+ webtap/commands/navigation.py,sha256=aRVW-t2YJ0Haf6Wkg7uq8GstiRftklop9W1TicUk3oo,6036
29
+ webtap/commands/network.py,sha256=gEOg_u7VF9A5aKv5myzLCuvfAUkF1OPxsuj4UAgbS44,3111
30
+ webtap/commands/selections.py,sha256=uyEET9eaKcLWjBz0QUdMRuRU-HIBXGtARxK6k71zOMY,4910
31
+ webtap/commands/server.py,sha256=DOcIgYuKp0ydwrK9EA3hGwqOwfwM9DABhdPu3hk_jjo,6948
32
32
  webtap/commands/setup.py,sha256=dov1LaN50nAEMNIuBLSK7mcnwhfn9rtqdTopBm1-PhA,9648
33
33
  webtap/services/README.md,sha256=rala_jtnNgSiQ1lFLM7x_UQ4SJZDceAm7dpkQMRTYaI,2346
34
34
  webtap/services/__init__.py,sha256=IjFqu0Ak6D-r18aokcQMtenDV3fbelvfjTCejGv6CZ0,570
35
35
  webtap/services/body.py,sha256=XQPa19y5eUc3XJ2TuwVK6kffO1VQoKqNs33MBBz7hzU,3913
36
36
  webtap/services/console.py,sha256=XVfSKTvEHyyOdujsg85S3wtj1CdZhzKtWwlx25MvSv8,3768
37
+ webtap/services/dom.py,sha256=PC-mV56NMLvw37JNzI_jOnZrM7BiDPPn3kOQI9U81vI,19067
37
38
  webtap/services/fetch.py,sha256=nl6bpU2Vnf40kau4-mqAnIkhC-7Lx2vbTJKUglz9KnE,13602
38
- webtap/services/main.py,sha256=HcXdPuI7hzsxsNvfN0npGhj_M7HObc83Lr3fuy7BMeE,5673
39
+ webtap/services/main.py,sha256=KUJFXzXL0ORlYfDV6DNULv_nzNfmlBFz_mfOvscp2wY,6287
39
40
  webtap/services/network.py,sha256=0o_--F6YvmXqqFqrcjL1gc6Vr9V1Ytb_U7r_DSUWupA,3444
40
41
  webtap/services/setup/__init__.py,sha256=lfoKCAroc-JoE_r7L-KZkF85ZWiB41MBIgrR7ZISSoE,7157
41
42
  webtap/services/setup/chrome.py,sha256=zfPWeb6zm_xjIfiS2S_O9lR2BjGKaPXXo06pN_B9lAU,7187
@@ -43,7 +44,7 @@ webtap/services/setup/desktop.py,sha256=fXwQa201W-s2mengm_dJZ9BigJopVrO9YFUQcW_T
43
44
  webtap/services/setup/extension.py,sha256=OvTLuSi5u-kBAkqWAzfYt5lTNZrduXoCMZhFCuMisew,3318
44
45
  webtap/services/setup/filters.py,sha256=lAPSLMH_KZQO-7bRkmURwzforx7C3SDrKEw2ZogN-Lo,3220
45
46
  webtap/services/setup/platform.py,sha256=7yn-7LQFffgerWzWRtOG-yNEsR36ICThYUAu_N2FAso,4532
46
- webtap_tool-0.3.0.dist-info/METADATA,sha256=jm3l2rpT1TBdGU8AU7NJYV7RgwpfzITuPyjh4Zb1_pk,17588
47
- webtap_tool-0.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
48
- webtap_tool-0.3.0.dist-info/entry_points.txt,sha256=iFe575I0CIb1MbfPt0oX2VYyY5gSU_dA551PKVR83TU,39
49
- webtap_tool-0.3.0.dist-info/RECORD,,
47
+ webtap_tool-0.5.0.dist-info/METADATA,sha256=qRqF6_yp9fLVO_c57vgBkKpvC-wLGhVrpWaMMMWwJXU,17588
48
+ webtap_tool-0.5.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
49
+ webtap_tool-0.5.0.dist-info/entry_points.txt,sha256=iFe575I0CIb1MbfPt0oX2VYyY5gSU_dA551PKVR83TU,39
50
+ webtap_tool-0.5.0.dist-info/RECORD,,