cmdop 0.1.22__py3-none-any.whl → 0.1.23__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.
- cmdop/__init__.py +1 -1
- cmdop/_generated/rpc_messages/browser_pb2.py +135 -85
- cmdop/_generated/rpc_messages/browser_pb2.pyi +270 -2
- cmdop/_generated/rpc_messages_pb2.pyi +25 -0
- cmdop/_generated/service_pb2.py +2 -2
- cmdop/_generated/service_pb2_grpc.py +345 -0
- cmdop/services/browser/capabilities/__init__.py +2 -0
- cmdop/services/browser/capabilities/fetch.py +1 -2
- cmdop/services/browser/capabilities/network.py +245 -0
- cmdop/services/browser/models.py +103 -0
- cmdop/services/browser/service/sync.py +204 -4
- cmdop/services/browser/session.py +32 -4
- {cmdop-0.1.22.dist-info → cmdop-0.1.23.dist-info}/METADATA +43 -4
- {cmdop-0.1.22.dist-info → cmdop-0.1.23.dist-info}/RECORD +16 -15
- {cmdop-0.1.22.dist-info → cmdop-0.1.23.dist-info}/WHEEL +0 -0
- {cmdop-0.1.22.dist-info → cmdop-0.1.23.dist-info}/licenses/LICENSE +0 -0
cmdop/services/browser/models.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from enum import Enum
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
7
8
|
from pydantic import BaseModel
|
|
@@ -14,6 +15,25 @@ from cmdop.exceptions import (
|
|
|
14
15
|
)
|
|
15
16
|
|
|
16
17
|
|
|
18
|
+
class WaitUntil(str, Enum):
|
|
19
|
+
"""Wait strategy for navigation.
|
|
20
|
+
|
|
21
|
+
Determines when navigation is considered complete.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
LOAD = "load"
|
|
25
|
+
"""Wait for load event (default, not recommended for SPA)."""
|
|
26
|
+
|
|
27
|
+
DOMCONTENTLOADED = "domcontentloaded"
|
|
28
|
+
"""Wait for DOMContentLoaded event."""
|
|
29
|
+
|
|
30
|
+
NETWORKIDLE = "networkidle"
|
|
31
|
+
"""Wait until no network activity for 500ms (best for SPA)."""
|
|
32
|
+
|
|
33
|
+
COMMIT = "commit"
|
|
34
|
+
"""Return immediately after navigation commits (fastest)."""
|
|
35
|
+
|
|
36
|
+
|
|
17
37
|
def raise_browser_error(error: str, operation: str, **context: Any) -> None:
|
|
18
38
|
"""Raise appropriate browser exception based on error message."""
|
|
19
39
|
error_lower = error.lower()
|
|
@@ -115,3 +135,86 @@ class InfiniteScrollResult(BaseModel):
|
|
|
115
135
|
at_bottom: bool = False
|
|
116
136
|
total_seen: int = 0
|
|
117
137
|
error: str | None = None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# =============================================================================
|
|
141
|
+
# Network Capture Models (v2.19.0)
|
|
142
|
+
# =============================================================================
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class NetworkRequest(BaseModel):
|
|
146
|
+
"""Captured HTTP request."""
|
|
147
|
+
|
|
148
|
+
url: str = ""
|
|
149
|
+
method: str = "GET"
|
|
150
|
+
headers: dict[str, str] = {}
|
|
151
|
+
body: bytes = b""
|
|
152
|
+
content_type: str = ""
|
|
153
|
+
resource_type: str = "" # document, script, xhr, fetch, image, etc.
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class NetworkResponse(BaseModel):
|
|
157
|
+
"""Captured HTTP response."""
|
|
158
|
+
|
|
159
|
+
status: int = 0
|
|
160
|
+
status_text: str = ""
|
|
161
|
+
headers: dict[str, str] = {}
|
|
162
|
+
body: bytes = b""
|
|
163
|
+
content_type: str = ""
|
|
164
|
+
size: int = 0
|
|
165
|
+
from_cache: bool = False
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class NetworkTiming(BaseModel):
|
|
169
|
+
"""Request timing data."""
|
|
170
|
+
|
|
171
|
+
started_at_ms: int = 0
|
|
172
|
+
ended_at_ms: int = 0
|
|
173
|
+
duration_ms: int = 0
|
|
174
|
+
wait_time_ms: int = 0 # Time to first byte
|
|
175
|
+
receive_time_ms: int = 0 # Time to download body
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class NetworkExchange(BaseModel):
|
|
179
|
+
"""Complete request/response exchange."""
|
|
180
|
+
|
|
181
|
+
id: str = ""
|
|
182
|
+
request: NetworkRequest = NetworkRequest()
|
|
183
|
+
response: NetworkResponse | None = None
|
|
184
|
+
timing: NetworkTiming = NetworkTiming()
|
|
185
|
+
error: str = ""
|
|
186
|
+
frame_id: str = ""
|
|
187
|
+
initiator: str = "" # URL or script that initiated
|
|
188
|
+
|
|
189
|
+
def json_body(self) -> Any:
|
|
190
|
+
"""Parse response body as JSON."""
|
|
191
|
+
import json
|
|
192
|
+
if self.response and self.response.body:
|
|
193
|
+
return json.loads(self.response.body)
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
def text_body(self) -> str:
|
|
197
|
+
"""Get response body as text."""
|
|
198
|
+
if self.response and self.response.body:
|
|
199
|
+
return self.response.body.decode("utf-8", errors="replace")
|
|
200
|
+
return ""
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class NetworkStats(BaseModel):
|
|
204
|
+
"""Network capture statistics."""
|
|
205
|
+
|
|
206
|
+
enabled: bool = False
|
|
207
|
+
total_captured: int = 0
|
|
208
|
+
total_errors: int = 0
|
|
209
|
+
total_bytes: int = 0
|
|
210
|
+
average_duration_ms: int = 0
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class NetworkFilter(BaseModel):
|
|
214
|
+
"""Filter for querying captured exchanges."""
|
|
215
|
+
|
|
216
|
+
url_pattern: str = ""
|
|
217
|
+
methods: list[str] = []
|
|
218
|
+
status_codes: list[int] = []
|
|
219
|
+
resource_types: list[str] = []
|
|
220
|
+
limit: int = 0
|
|
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
7
7
|
|
|
8
8
|
from cmdop.services.base import BaseService
|
|
9
9
|
from cmdop.services.browser.service._helpers import BaseServiceMixin, cookie_to_pb, pb_to_cookie
|
|
10
|
-
from cmdop.services.browser.models import BrowserCookie, BrowserState, PageInfo, raise_browser_error
|
|
10
|
+
from cmdop.services.browser.models import BrowserCookie, BrowserState, PageInfo, WaitUntil, raise_browser_error
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
13
|
from cmdop.transport.base import BaseTransport
|
|
@@ -75,11 +75,31 @@ class BrowserService(BaseService, BaseServiceMixin):
|
|
|
75
75
|
|
|
76
76
|
# === Navigation & Interaction ===
|
|
77
77
|
|
|
78
|
-
def navigate(
|
|
79
|
-
|
|
78
|
+
def navigate(
|
|
79
|
+
self,
|
|
80
|
+
session_id: str,
|
|
81
|
+
url: str,
|
|
82
|
+
timeout_ms: int = 30000,
|
|
83
|
+
wait_until: WaitUntil = WaitUntil.LOAD,
|
|
84
|
+
) -> str:
|
|
85
|
+
from cmdop._generated.rpc_messages.browser_pb2 import (
|
|
86
|
+
BrowserNavigateRequest,
|
|
87
|
+
WaitUntil as PbWaitUntil,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Convert Python enum to proto enum
|
|
91
|
+
pb_wait_until = {
|
|
92
|
+
WaitUntil.LOAD: PbWaitUntil.WAIT_LOAD,
|
|
93
|
+
WaitUntil.DOMCONTENTLOADED: PbWaitUntil.WAIT_DOMCONTENTLOADED,
|
|
94
|
+
WaitUntil.NETWORKIDLE: PbWaitUntil.WAIT_NETWORKIDLE,
|
|
95
|
+
WaitUntil.COMMIT: PbWaitUntil.WAIT_COMMIT,
|
|
96
|
+
}.get(wait_until, PbWaitUntil.WAIT_LOAD)
|
|
80
97
|
|
|
81
98
|
request = BrowserNavigateRequest(
|
|
82
|
-
browser_session_id=session_id,
|
|
99
|
+
browser_session_id=session_id,
|
|
100
|
+
url=url,
|
|
101
|
+
timeout_ms=timeout_ms,
|
|
102
|
+
wait_until=pb_wait_until,
|
|
83
103
|
)
|
|
84
104
|
response = self._call_sync(self._get_stub.BrowserNavigate, request)
|
|
85
105
|
|
|
@@ -434,3 +454,183 @@ class BrowserService(BaseService, BaseServiceMixin):
|
|
|
434
454
|
"items": json.loads(response.items_json) if response.items_json else [],
|
|
435
455
|
"count": response.count,
|
|
436
456
|
}
|
|
457
|
+
|
|
458
|
+
# === Network Capture (v2.19.0) ===
|
|
459
|
+
|
|
460
|
+
def network_enable(
|
|
461
|
+
self, session_id: str, max_exchanges: int = 1000, max_response_size: int = 10_000_000
|
|
462
|
+
) -> None:
|
|
463
|
+
"""Enable network capture."""
|
|
464
|
+
from cmdop._generated.rpc_messages.browser_pb2 import BrowserNetworkEnableRequest
|
|
465
|
+
|
|
466
|
+
request = BrowserNetworkEnableRequest(
|
|
467
|
+
browser_session_id=session_id,
|
|
468
|
+
max_exchanges=max_exchanges,
|
|
469
|
+
max_response_size=max_response_size,
|
|
470
|
+
)
|
|
471
|
+
response = self._call_sync(self._get_stub.BrowserNetworkEnable, request)
|
|
472
|
+
|
|
473
|
+
if not response.success:
|
|
474
|
+
raise RuntimeError(f"NetworkEnable failed: {response.error}")
|
|
475
|
+
|
|
476
|
+
def network_disable(self, session_id: str) -> None:
|
|
477
|
+
"""Disable network capture."""
|
|
478
|
+
from cmdop._generated.rpc_messages.browser_pb2 import BrowserNetworkDisableRequest
|
|
479
|
+
|
|
480
|
+
request = BrowserNetworkDisableRequest(browser_session_id=session_id)
|
|
481
|
+
response = self._call_sync(self._get_stub.BrowserNetworkDisable, request)
|
|
482
|
+
|
|
483
|
+
if not response.success:
|
|
484
|
+
raise RuntimeError(f"NetworkDisable failed: {response.error}")
|
|
485
|
+
|
|
486
|
+
def network_get_exchanges(
|
|
487
|
+
self,
|
|
488
|
+
session_id: str,
|
|
489
|
+
url_pattern: str = "",
|
|
490
|
+
methods: list[str] | None = None,
|
|
491
|
+
status_codes: list[int] | None = None,
|
|
492
|
+
resource_types: list[str] | None = None,
|
|
493
|
+
limit: int = 0,
|
|
494
|
+
) -> dict:
|
|
495
|
+
"""Get captured exchanges."""
|
|
496
|
+
from cmdop._generated.rpc_messages.browser_pb2 import BrowserNetworkGetExchangesRequest
|
|
497
|
+
|
|
498
|
+
request = BrowserNetworkGetExchangesRequest(
|
|
499
|
+
browser_session_id=session_id,
|
|
500
|
+
url_pattern=url_pattern,
|
|
501
|
+
methods=methods or [],
|
|
502
|
+
status_codes=status_codes or [],
|
|
503
|
+
resource_types=resource_types or [],
|
|
504
|
+
limit=limit,
|
|
505
|
+
)
|
|
506
|
+
response = self._call_sync(self._get_stub.BrowserNetworkGetExchanges, request)
|
|
507
|
+
|
|
508
|
+
if not response.success:
|
|
509
|
+
raise RuntimeError(f"NetworkGetExchanges failed: {response.error}")
|
|
510
|
+
|
|
511
|
+
return {
|
|
512
|
+
"exchanges": [self._pb_to_exchange(e) for e in response.exchanges],
|
|
513
|
+
"count": response.count,
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
def network_get_exchange(self, session_id: str, exchange_id: str) -> dict:
|
|
517
|
+
"""Get specific exchange by ID."""
|
|
518
|
+
from cmdop._generated.rpc_messages.browser_pb2 import BrowserNetworkGetExchangeRequest
|
|
519
|
+
|
|
520
|
+
request = BrowserNetworkGetExchangeRequest(
|
|
521
|
+
browser_session_id=session_id, exchange_id=exchange_id
|
|
522
|
+
)
|
|
523
|
+
response = self._call_sync(self._get_stub.BrowserNetworkGetExchange, request)
|
|
524
|
+
|
|
525
|
+
if not response.success:
|
|
526
|
+
raise RuntimeError(f"NetworkGetExchange failed: {response.error}")
|
|
527
|
+
|
|
528
|
+
return {"exchange": self._pb_to_exchange(response.exchange) if response.exchange else None}
|
|
529
|
+
|
|
530
|
+
def network_get_last(self, session_id: str, url_pattern: str = "") -> dict:
|
|
531
|
+
"""Get most recent exchange matching URL pattern."""
|
|
532
|
+
from cmdop._generated.rpc_messages.browser_pb2 import BrowserNetworkGetLastRequest
|
|
533
|
+
|
|
534
|
+
request = BrowserNetworkGetLastRequest(
|
|
535
|
+
browser_session_id=session_id, url_pattern=url_pattern
|
|
536
|
+
)
|
|
537
|
+
response = self._call_sync(self._get_stub.BrowserNetworkGetLast, request)
|
|
538
|
+
|
|
539
|
+
if not response.success:
|
|
540
|
+
raise RuntimeError(f"NetworkGetLast failed: {response.error}")
|
|
541
|
+
|
|
542
|
+
return {"exchange": self._pb_to_exchange(response.exchange) if response.exchange else None}
|
|
543
|
+
|
|
544
|
+
def network_clear(self, session_id: str) -> None:
|
|
545
|
+
"""Clear all captured exchanges."""
|
|
546
|
+
from cmdop._generated.rpc_messages.browser_pb2 import BrowserNetworkClearRequest
|
|
547
|
+
|
|
548
|
+
request = BrowserNetworkClearRequest(browser_session_id=session_id)
|
|
549
|
+
response = self._call_sync(self._get_stub.BrowserNetworkClear, request)
|
|
550
|
+
|
|
551
|
+
if not response.success:
|
|
552
|
+
raise RuntimeError(f"NetworkClear failed: {response.error}")
|
|
553
|
+
|
|
554
|
+
def network_stats(self, session_id: str) -> dict:
|
|
555
|
+
"""Get capture statistics."""
|
|
556
|
+
from cmdop._generated.rpc_messages.browser_pb2 import BrowserNetworkStatsRequest
|
|
557
|
+
|
|
558
|
+
request = BrowserNetworkStatsRequest(browser_session_id=session_id)
|
|
559
|
+
response = self._call_sync(self._get_stub.BrowserNetworkStats, request)
|
|
560
|
+
|
|
561
|
+
if not response.success:
|
|
562
|
+
raise RuntimeError(f"NetworkStats failed: {response.error}")
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
"enabled": response.enabled,
|
|
566
|
+
"total_captured": response.total_captured,
|
|
567
|
+
"total_errors": response.total_errors,
|
|
568
|
+
"total_bytes": response.total_bytes,
|
|
569
|
+
"average_duration_ms": response.average_duration_ms,
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
def network_export_har(
|
|
573
|
+
self,
|
|
574
|
+
session_id: str,
|
|
575
|
+
url_pattern: str = "",
|
|
576
|
+
methods: list[str] | None = None,
|
|
577
|
+
status_codes: list[int] | None = None,
|
|
578
|
+
resource_types: list[str] | None = None,
|
|
579
|
+
) -> dict:
|
|
580
|
+
"""Export captured exchanges to HAR format."""
|
|
581
|
+
from cmdop._generated.rpc_messages.browser_pb2 import BrowserNetworkExportHARRequest
|
|
582
|
+
|
|
583
|
+
request = BrowserNetworkExportHARRequest(
|
|
584
|
+
browser_session_id=session_id,
|
|
585
|
+
url_pattern=url_pattern,
|
|
586
|
+
methods=methods or [],
|
|
587
|
+
status_codes=status_codes or [],
|
|
588
|
+
resource_types=resource_types or [],
|
|
589
|
+
)
|
|
590
|
+
response = self._call_sync(self._get_stub.BrowserNetworkExportHAR, request)
|
|
591
|
+
|
|
592
|
+
if not response.success:
|
|
593
|
+
raise RuntimeError(f"NetworkExportHAR failed: {response.error}")
|
|
594
|
+
|
|
595
|
+
return {"har_data": response.har_data}
|
|
596
|
+
|
|
597
|
+
def _pb_to_exchange(self, pb: Any) -> dict:
|
|
598
|
+
"""Convert protobuf exchange to dict."""
|
|
599
|
+
result: dict[str, Any] = {
|
|
600
|
+
"id": pb.id,
|
|
601
|
+
"error": pb.error,
|
|
602
|
+
"frame_id": pb.frame_id,
|
|
603
|
+
"initiator": pb.initiator,
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if pb.request:
|
|
607
|
+
result["request"] = {
|
|
608
|
+
"url": pb.request.url,
|
|
609
|
+
"method": pb.request.method,
|
|
610
|
+
"headers": dict(pb.request.headers),
|
|
611
|
+
"body": pb.request.body,
|
|
612
|
+
"content_type": pb.request.content_type,
|
|
613
|
+
"resource_type": pb.request.resource_type,
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if pb.response:
|
|
617
|
+
result["response"] = {
|
|
618
|
+
"status": pb.response.status,
|
|
619
|
+
"status_text": pb.response.status_text,
|
|
620
|
+
"headers": dict(pb.response.headers),
|
|
621
|
+
"body": pb.response.body,
|
|
622
|
+
"content_type": pb.response.content_type,
|
|
623
|
+
"size": pb.response.size,
|
|
624
|
+
"from_cache": pb.response.from_cache,
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if pb.timing:
|
|
628
|
+
result["timing"] = {
|
|
629
|
+
"started_at_ms": pb.timing.started_at_ms,
|
|
630
|
+
"ended_at_ms": pb.timing.ended_at_ms,
|
|
631
|
+
"duration_ms": pb.timing.duration_ms,
|
|
632
|
+
"wait_time_ms": pb.timing.wait_time_ms,
|
|
633
|
+
"receive_time_ms": pb.timing.receive_time_ms,
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return result
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
from typing import TYPE_CHECKING, Any
|
|
5
5
|
|
|
6
|
-
from cmdop.services.browser.models import BrowserCookie, BrowserState, PageInfo
|
|
6
|
+
from cmdop.services.browser.models import BrowserCookie, BrowserState, PageInfo, WaitUntil
|
|
7
7
|
|
|
8
8
|
from .capabilities import (
|
|
9
9
|
ScrollCapability,
|
|
@@ -11,6 +11,7 @@ from .capabilities import (
|
|
|
11
11
|
TimingCapability,
|
|
12
12
|
DOMCapability,
|
|
13
13
|
FetchCapability,
|
|
14
|
+
NetworkCapability,
|
|
14
15
|
)
|
|
15
16
|
|
|
16
17
|
if TYPE_CHECKING:
|
|
@@ -51,6 +52,7 @@ class BrowserSession:
|
|
|
51
52
|
"_timing",
|
|
52
53
|
"_dom",
|
|
53
54
|
"_fetch",
|
|
55
|
+
"_network",
|
|
54
56
|
)
|
|
55
57
|
|
|
56
58
|
def __init__(self, service: "BrowserService", session_id: str) -> None:
|
|
@@ -61,6 +63,7 @@ class BrowserSession:
|
|
|
61
63
|
self._timing: TimingCapability | None = None
|
|
62
64
|
self._dom: DOMCapability | None = None
|
|
63
65
|
self._fetch: FetchCapability | None = None
|
|
66
|
+
self._network: NetworkCapability | None = None
|
|
64
67
|
|
|
65
68
|
@property
|
|
66
69
|
def session_id(self) -> str:
|
|
@@ -103,11 +106,36 @@ class BrowserSession:
|
|
|
103
106
|
self._fetch = FetchCapability(self)
|
|
104
107
|
return self._fetch
|
|
105
108
|
|
|
109
|
+
@property
|
|
110
|
+
def network(self) -> NetworkCapability:
|
|
111
|
+
"""Network: enable(), disable(), get_all(), filter(), last(), clear(), stats()"""
|
|
112
|
+
if self._network is None:
|
|
113
|
+
self._network = NetworkCapability(self)
|
|
114
|
+
return self._network
|
|
115
|
+
|
|
106
116
|
# === Core Methods ===
|
|
107
117
|
|
|
108
|
-
def navigate(
|
|
109
|
-
|
|
110
|
-
|
|
118
|
+
def navigate(
|
|
119
|
+
self,
|
|
120
|
+
url: str,
|
|
121
|
+
timeout_ms: int = 30000,
|
|
122
|
+
wait_until: WaitUntil = WaitUntil.LOAD,
|
|
123
|
+
) -> str:
|
|
124
|
+
"""Navigate to URL with specified wait strategy.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
url: URL to navigate to.
|
|
128
|
+
timeout_ms: Timeout in milliseconds.
|
|
129
|
+
wait_until: When navigation is considered complete:
|
|
130
|
+
- WaitUntil.LOAD: Wait for load event (default, slow for SPA)
|
|
131
|
+
- WaitUntil.DOMCONTENTLOADED: Wait for DOMContentLoaded event
|
|
132
|
+
- WaitUntil.NETWORKIDLE: Wait until network is idle (best for SPA)
|
|
133
|
+
- WaitUntil.COMMIT: Return immediately (fastest)
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Final URL after navigation (may differ due to redirects).
|
|
137
|
+
"""
|
|
138
|
+
return self._service.navigate(self._session_id, url, timeout_ms, wait_until)
|
|
111
139
|
|
|
112
140
|
def click(self, selector: str, timeout_ms: int = 5000, move_cursor: bool = False) -> None:
|
|
113
141
|
"""Click element by CSS selector."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cmdop
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.23
|
|
4
4
|
Summary: Python SDK for CMDOP agent interaction
|
|
5
5
|
Project-URL: Homepage, https://cmdop.com
|
|
6
6
|
Project-URL: Documentation, https://cmdop.com
|
|
@@ -148,8 +148,10 @@ health: Health = result.output # Typed!
|
|
|
148
148
|
Capability-based API for browser automation.
|
|
149
149
|
|
|
150
150
|
```python
|
|
151
|
+
from cmdop.services.browser.models import WaitUntil
|
|
152
|
+
|
|
151
153
|
with client.browser.create_session() as s:
|
|
152
|
-
s.navigate("https://shop.com/products")
|
|
154
|
+
s.navigate("https://shop.com/products", wait_until=WaitUntil.NETWORKIDLE)
|
|
153
155
|
s.dom.close_modal() # Close popups
|
|
154
156
|
|
|
155
157
|
# BeautifulSoup parsing
|
|
@@ -182,7 +184,7 @@ with client.browser.create_session() as s:
|
|
|
182
184
|
|
|
183
185
|
| Method | Description |
|
|
184
186
|
|--------|-------------|
|
|
185
|
-
| `navigate(url)` | Go to URL |
|
|
187
|
+
| `navigate(url, wait_until)` | Go to URL (wait_until: LOAD, DOMCONTENTLOADED, NETWORKIDLE, COMMIT) |
|
|
186
188
|
| `click(selector, move_cursor)` | Click element |
|
|
187
189
|
| `type(selector, text)` | Type text |
|
|
188
190
|
| `wait_for(selector)` | Wait for element |
|
|
@@ -238,7 +240,44 @@ with client.browser.create_session() as s:
|
|
|
238
240
|
|--------|-------------|
|
|
239
241
|
| `json(url)` | Fetch JSON |
|
|
240
242
|
| `all(requests)` | Parallel fetch |
|
|
241
|
-
| `execute(
|
|
243
|
+
| `execute(js_code)` | Custom JS fetch code |
|
|
244
|
+
|
|
245
|
+
**`session.network`** - Network capture (v2.19.0)
|
|
246
|
+
| Method | Description |
|
|
247
|
+
|--------|-------------|
|
|
248
|
+
| `enable(max_exchanges)` | Start capturing HTTP traffic |
|
|
249
|
+
| `disable()` | Stop capturing |
|
|
250
|
+
| `get_all()` | Get all captured exchanges |
|
|
251
|
+
| `filter(url_pattern, methods, status_codes)` | Filter exchanges |
|
|
252
|
+
| `last(url_pattern)` | Get most recent matching exchange |
|
|
253
|
+
| `api_calls(url_pattern)` | Get XHR/Fetch calls matching pattern |
|
|
254
|
+
| `last_json(url_pattern)` | Get JSON body from last matching response |
|
|
255
|
+
| `wait_for(url_pattern, timeout_ms)` | Wait for matching request |
|
|
256
|
+
| `stats()` | Capture statistics |
|
|
257
|
+
| `export_har()` | Export to HAR format |
|
|
258
|
+
| `clear()` | Clear captured data |
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
# Example: Intercept API responses
|
|
262
|
+
from cmdop.services.browser.models import WaitUntil
|
|
263
|
+
|
|
264
|
+
with client.browser.create_session() as s:
|
|
265
|
+
s.network.enable()
|
|
266
|
+
s.navigate("https://app.example.com", wait_until=WaitUntil.NETWORKIDLE)
|
|
267
|
+
|
|
268
|
+
# Get last API response
|
|
269
|
+
api = s.network.last("/api/data")
|
|
270
|
+
data = api.json_body()
|
|
271
|
+
|
|
272
|
+
# Filter by criteria
|
|
273
|
+
posts = s.network.filter(
|
|
274
|
+
url_pattern="/api/posts",
|
|
275
|
+
methods=["GET"],
|
|
276
|
+
status_codes=[200],
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
s.network.disable()
|
|
280
|
+
```
|
|
242
281
|
|
|
243
282
|
## SDKBaseModel
|
|
244
283
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
cmdop/__init__.py,sha256=
|
|
1
|
+
cmdop/__init__.py,sha256=eqEIR3w99NYHsoGF6FioESxTgQhB2yWiARNYWbmNdkk,5200
|
|
2
2
|
cmdop/client.py,sha256=nTotStZPBfYN3TrHH-OlEJMSVAXskYMQRkocsFmyaBY,14601
|
|
3
3
|
cmdop/config.py,sha256=vpw1aGCyS4NKlZyzVur81Lt06QmN3FnscZji0bypUi0,4398
|
|
4
4
|
cmdop/discovery.py,sha256=HNxSOa5tSuG7ppfFs21XdviW5ucjpRswVPguhX5j8Dg,7479
|
|
@@ -24,11 +24,11 @@ cmdop/_generated/file_rpc_pb2_grpc.py,sha256=HddeBZYoBOUkx4yPq2qdpwv2QmQouSbwCkf
|
|
|
24
24
|
cmdop/_generated/message_pool.py,sha256=ncIr4Z2KeinOVGyzmR-Drc76DZnDTwWl4-bQYcIl8uM,97
|
|
25
25
|
cmdop/_generated/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
26
|
cmdop/_generated/rpc_messages_pb2.py,sha256=X3iog1k2vUDna-hBpEXy0E9-D05mkDWlmBSy0L_yhmQ,2734
|
|
27
|
-
cmdop/_generated/rpc_messages_pb2.pyi,sha256=
|
|
27
|
+
cmdop/_generated/rpc_messages_pb2.pyi,sha256=ddIPYj1axgsGwSc0KO1lTnCknJexEqeRz3VEwsLzG0E,10507
|
|
28
28
|
cmdop/_generated/rpc_messages_pb2_grpc.py,sha256=WHRqOkW1r_9wP5Sd1Bc20jDVErtDN_xHvhsv8r8xyss,892
|
|
29
|
-
cmdop/_generated/service_pb2.py,sha256=
|
|
29
|
+
cmdop/_generated/service_pb2.py,sha256=WRgcxz-wh7RWXTXcBTgw85V5uAgsc68UGaD0gAGwAxE,13315
|
|
30
30
|
cmdop/_generated/service_pb2.pyi,sha256=-q-RU8TbkwpnKuBQGIhEsjGXj8ltD87sdzgC3B6zKYY,1649
|
|
31
|
-
cmdop/_generated/service_pb2_grpc.py,sha256=
|
|
31
|
+
cmdop/_generated/service_pb2_grpc.py,sha256=ulLybECN-LLMMYlUG3XJw7Fmfz8PD0jRWwVHmhlYPjs,124703
|
|
32
32
|
cmdop/_generated/tunnel_pb2.py,sha256=oF5qOSw_9faeXtVFELfEw1dq3TtPOUWl1v6DzUqguGo,3928
|
|
33
33
|
cmdop/_generated/tunnel_pb2.pyi,sha256=ipcoNwnTLY16IfA95SbTP7REypE65ZVpf6akF7h4FCw,4925
|
|
34
34
|
cmdop/_generated/tunnel_pb2_grpc.py,sha256=yutTebujge5cHC3h84FKWSdvGBuPVYOkRDy0UGtHZ6c,886
|
|
@@ -80,8 +80,8 @@ cmdop/_generated/rpc_messages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
|
|
|
80
80
|
cmdop/_generated/rpc_messages/agent_pb2.py,sha256=1KCs4kwuuTwZm9Y8ql7t6Jjsutuepbq85E6DGVDWa84,3393
|
|
81
81
|
cmdop/_generated/rpc_messages/agent_pb2.pyi,sha256=F7gy0PQlJ4cRQCli7U4v5A87-Lsw2_x0nV4hFXG1qj0,3866
|
|
82
82
|
cmdop/_generated/rpc_messages/agent_pb2_grpc.py,sha256=ABbaG1eMcaWPZKdPoom2F-IjjVqjYNMDOjMp8X6QLHE,898
|
|
83
|
-
cmdop/_generated/rpc_messages/browser_pb2.py,sha256=
|
|
84
|
-
cmdop/_generated/rpc_messages/browser_pb2.pyi,sha256=
|
|
83
|
+
cmdop/_generated/rpc_messages/browser_pb2.py,sha256=2yuoDnRYtAq34NOg4xFPVo0jz0Tr1ulatT8Qe6Ibty8,24851
|
|
84
|
+
cmdop/_generated/rpc_messages/browser_pb2.pyi,sha256=HXQxtegycSDNrHXaxCnMKqukb2Na4dJTCUAg413c31k,37988
|
|
85
85
|
cmdop/_generated/rpc_messages/browser_pb2_grpc.py,sha256=P8F8QYLT_kedGq7AXj37Us2hFQOaevdfYMHl0tTdY48,900
|
|
86
86
|
cmdop/_generated/rpc_messages/device_pb2.py,sha256=nKj7axhR-FVSQoQUd4ENXpc5PU_UCjSlExajLIoeZY8,2435
|
|
87
87
|
cmdop/_generated/rpc_messages/device_pb2.pyi,sha256=u5oLfTHuOSpYwMiVe2tw0vVQLLFvYgK3lxw-Vky1ZkA,1974
|
|
@@ -167,15 +167,16 @@ cmdop/services/extract.py,sha256=zwzikEKH4T4OnrprmJg3wqBWQJr-DYsSZgJGK_2yIHU,111
|
|
|
167
167
|
cmdop/services/files.py,sha256=RGhfo7tW6diuUWC_EQQ-Y9zO1btm6mBTji0SWWa0fdo,12548
|
|
168
168
|
cmdop/services/terminal.py,sha256=9SSWBexe2rWgMd-hGBEs9mcax3l7x_U84VHZpMC4xK8,17512
|
|
169
169
|
cmdop/services/browser/__init__.py,sha256=31Ofu9RCYTAedPKLvnor8J7oGDgTjbqJ58OkxxHYwdk,1270
|
|
170
|
-
cmdop/services/browser/models.py,sha256=
|
|
170
|
+
cmdop/services/browser/models.py,sha256=9MpNFgSgZDIznmTmsCUByEN31t_iQ6kAza1BsPSsuJs,5320
|
|
171
171
|
cmdop/services/browser/parsing.py,sha256=0hQAy-0ZwJqtmhEqHO3EEdVB3iYmyhXRdouN_dCbig8,3820
|
|
172
|
-
cmdop/services/browser/session.py,sha256=
|
|
173
|
-
cmdop/services/browser/capabilities/__init__.py,sha256=
|
|
172
|
+
cmdop/services/browser/session.py,sha256=atSDDrTdkVJ6OrTiz8OaifegedZvbkUZUpfxGKUrDw8,6403
|
|
173
|
+
cmdop/services/browser/capabilities/__init__.py,sha256=UFmqG3XB2xm0MI2jRmsSXyvFyREPbfJnXLzgaragpbU,398
|
|
174
174
|
cmdop/services/browser/capabilities/_base.py,sha256=mW0jKa2CyvK-8cjenv5JYvuCKiO3rpt5F7WtWFXBitA,749
|
|
175
175
|
cmdop/services/browser/capabilities/_helpers.py,sha256=jXqYbeDocAHec2GwF2_BNnJ78vTyUnHteQoS-RSG00k,488
|
|
176
176
|
cmdop/services/browser/capabilities/dom.py,sha256=DuXfildga23wGBNJWtNzx-t2Cq553HC48o9KAWAlyC0,2612
|
|
177
|
-
cmdop/services/browser/capabilities/fetch.py,sha256=
|
|
177
|
+
cmdop/services/browser/capabilities/fetch.py,sha256=IpUrcUAOEanrN4TrB-JVtn3TtJ8GdkyN5qbTbpBWvLc,1310
|
|
178
178
|
cmdop/services/browser/capabilities/input.py,sha256=uYmWGqturMDent44Us80oT_nk4kFNGyPJ0P5I35ulew,1749
|
|
179
|
+
cmdop/services/browser/capabilities/network.py,sha256=tZV4Oh_J5zUjEe9GBLQBDXEVh9EVTecPbyjE7lIUrd0,7775
|
|
179
180
|
cmdop/services/browser/capabilities/scroll.py,sha256=sh0VuOPOv81BZg80-n8TABOj5RpshJT12qJwm4F_OY0,4808
|
|
180
181
|
cmdop/services/browser/capabilities/timing.py,sha256=NH34G_4Kfukh6JCdhLRGoouA-uNTbx9ly7ybP9Kh558,1868
|
|
181
182
|
cmdop/services/browser/js/__init__.py,sha256=gTiZguikKfztDtggZTux2FqhT8YTjyHCzQR4TEnT7z4,1177
|
|
@@ -186,7 +187,7 @@ cmdop/services/browser/js/scroll.py,sha256=yiOMAaR8ac8jCiWgNCVIt1pUGxdvktMUtzv-A
|
|
|
186
187
|
cmdop/services/browser/service/__init__.py,sha256=AZH_r2FsxLfJGCVBaaAPw3dTGaUlgIFdlYd_RR8KxSg,100
|
|
187
188
|
cmdop/services/browser/service/_helpers.py,sha256=w8foDUGZcD4HyF5eyLZUFxbx_fctAFsYRovvsksi3l4,1584
|
|
188
189
|
cmdop/services/browser/service/aio.py,sha256=0E2D4igQb3YzAakdXsRMt8PEd0rFxI2gXfimwq_6nzk,767
|
|
189
|
-
cmdop/services/browser/service/sync.py,sha256=
|
|
190
|
+
cmdop/services/browser/service/sync.py,sha256=40CIqdUAR-ZVFfQJVgUWVgGq3j7mgcpGs1SRe8zXjmI,23096
|
|
190
191
|
cmdop/streaming/__init__.py,sha256=kG9UlJRqv8ndcwKMzWUddPlZT61pFO_Uf_c08A_8TxA,877
|
|
191
192
|
cmdop/streaming/base.py,sha256=r7Q2QlRxgULzs9vlSGcOC_fwAQ_cF3Z3M7WsPQtxG5I,2990
|
|
192
193
|
cmdop/streaming/handlers.py,sha256=FDEhADmCEFRbifvr9dU1X3C-K_96noz89Bl3tuDa_rQ,2616
|
|
@@ -197,7 +198,7 @@ cmdop/transport/base.py,sha256=2pkV8i9epgp_21dyReCfX47abRUrnALm0W5BXb-Fuz0,5571
|
|
|
197
198
|
cmdop/transport/discovery.py,sha256=rcGAuVrR1l6jwcP0dqZxVhX1NsFK7sRHygFMCLmmUbA,10673
|
|
198
199
|
cmdop/transport/local.py,sha256=ob6tWVxSdKwblHSMK8CkgjyuSdQoAeWgy5OAUd5ZNuE,7411
|
|
199
200
|
cmdop/transport/remote.py,sha256=FNVqus9wOv7LlxKarXjLmSyvJiHwhvPbNDOPv1IQkmE,4329
|
|
200
|
-
cmdop-0.1.
|
|
201
|
-
cmdop-0.1.
|
|
202
|
-
cmdop-0.1.
|
|
203
|
-
cmdop-0.1.
|
|
201
|
+
cmdop-0.1.23.dist-info/METADATA,sha256=3BVnbTuAgOy-U2nU1g7gpiE1n1E81AZjepAvHM1N0hA,8929
|
|
202
|
+
cmdop-0.1.23.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
203
|
+
cmdop-0.1.23.dist-info/licenses/LICENSE,sha256=6hyzbI1QVXW6B-XT7PaQ6UG9lns11Y_nnap8uUKGUqo,1062
|
|
204
|
+
cmdop-0.1.23.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|