cmdop 0.1.20__py3-none-any.whl → 0.1.22__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.
Files changed (31) hide show
  1. cmdop/__init__.py +1 -1
  2. cmdop/_generated/rpc_messages/browser_pb2.py +81 -81
  3. cmdop/_generated/rpc_messages/browser_pb2.pyi +4 -2
  4. cmdop/client.py +2 -8
  5. cmdop/services/browser/__init__.py +44 -31
  6. cmdop/services/browser/capabilities/__init__.py +15 -0
  7. cmdop/services/browser/capabilities/_base.py +28 -0
  8. cmdop/services/browser/capabilities/_helpers.py +16 -0
  9. cmdop/services/browser/capabilities/dom.py +76 -0
  10. cmdop/services/browser/capabilities/fetch.py +46 -0
  11. cmdop/services/browser/capabilities/input.py +49 -0
  12. cmdop/services/browser/capabilities/scroll.py +147 -0
  13. cmdop/services/browser/capabilities/timing.py +66 -0
  14. cmdop/services/browser/js/__init__.py +6 -4
  15. cmdop/services/browser/js/interaction.py +34 -0
  16. cmdop/services/browser/service/__init__.py +5 -0
  17. cmdop/services/browser/service/aio.py +30 -0
  18. cmdop/services/browser/{sync/service.py → service/sync.py} +9 -4
  19. cmdop/services/browser/session.py +166 -0
  20. {cmdop-0.1.20.dist-info → cmdop-0.1.22.dist-info}/METADATA +70 -41
  21. {cmdop-0.1.20.dist-info → cmdop-0.1.22.dist-info}/RECORD +24 -20
  22. cmdop/services/browser/aio/__init__.py +0 -6
  23. cmdop/services/browser/aio/service.py +0 -415
  24. cmdop/services/browser/aio/session.py +0 -358
  25. cmdop/services/browser/base/__init__.py +0 -6
  26. cmdop/services/browser/base/session.py +0 -124
  27. cmdop/services/browser/sync/__init__.py +0 -6
  28. cmdop/services/browser/sync/session.py +0 -580
  29. /cmdop/services/browser/{base/service.py → service/_helpers.py} +0 -0
  30. {cmdop-0.1.20.dist-info → cmdop-0.1.22.dist-info}/WHEEL +0 -0
  31. {cmdop-0.1.20.dist-info → cmdop-0.1.22.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,5 @@
1
- cmdop/__init__.py,sha256=KXYlejW7fNSgFT8NJkPTT-tWtMNxNBqy-YWeahE7zxY,5200
2
- cmdop/client.py,sha256=NzCBMIAj2vnVzwU1OUttW_kpgJh5aRtmixOwa9HUUUg,14924
1
+ cmdop/__init__.py,sha256=te8zdIJesHnMNTru-7YrTOnoUObhAm972gTFbhoHW5g,5200
2
+ cmdop/client.py,sha256=nTotStZPBfYN3TrHH-OlEJMSVAXskYMQRkocsFmyaBY,14601
3
3
  cmdop/config.py,sha256=vpw1aGCyS4NKlZyzVur81Lt06QmN3FnscZji0bypUi0,4398
4
4
  cmdop/discovery.py,sha256=HNxSOa5tSuG7ppfFs21XdviW5ucjpRswVPguhX5j8Dg,7479
5
5
  cmdop/exceptions.py,sha256=-ubepNXXpasXDmlKhl3k0MhWRsI6Oj46Ic_jH4VUw-g,10314
@@ -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=nez7IJkPR5gsr2mHey-cE0UgeQB_H9PxAAfNB_gHALE,16823
84
- cmdop/_generated/rpc_messages/browser_pb2.pyi,sha256=aPKlDGOPxGZoFHSbMBziRvC4U--I6ZY0ZSFgtRTKm9A,25118
83
+ cmdop/_generated/rpc_messages/browser_pb2.py,sha256=VAmjY4WUDXkpYOkS-q6w5qplbpIQGLmqDiS6b3O6UYE,16875
84
+ cmdop/_generated/rpc_messages/browser_pb2.pyi,sha256=wA51sejGuiCfj9nEpAeymZwkvv1saQyIbAH5Ntlz0fc,25225
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
@@ -166,23 +166,27 @@ cmdop/services/base.py,sha256=BCYIUME5gfpDHAief30sQNVzrlAgHx6QGZ6AbLHQx5w,3087
166
166
  cmdop/services/extract.py,sha256=zwzikEKH4T4OnrprmJg3wqBWQJr-DYsSZgJGK_2yIHU,11119
167
167
  cmdop/services/files.py,sha256=RGhfo7tW6diuUWC_EQQ-Y9zO1btm6mBTji0SWWa0fdo,12548
168
168
  cmdop/services/terminal.py,sha256=9SSWBexe2rWgMd-hGBEs9mcax3l7x_U84VHZpMC4xK8,17512
169
- cmdop/services/browser/__init__.py,sha256=BFdrmtvWEiBztds-YITE-8aW4-7-B8jbsQaPsC1M60g,1475
169
+ cmdop/services/browser/__init__.py,sha256=31Ofu9RCYTAedPKLvnor8J7oGDgTjbqJ58OkxxHYwdk,1270
170
170
  cmdop/services/browser/models.py,sha256=qJ3da8hPvQn7Vtiqg3xg49G-GioidQGPPuLk2c_IBC4,2656
171
171
  cmdop/services/browser/parsing.py,sha256=0hQAy-0ZwJqtmhEqHO3EEdVB3iYmyhXRdouN_dCbig8,3820
172
- cmdop/services/browser/aio/__init__.py,sha256=7DI4Q0LZaAdu3fl8D1SaZ8E3LCGpTw97lqca1pOi5Kk,241
173
- cmdop/services/browser/aio/service.py,sha256=PTGBJtiS7xxU_4ApGqbyBQqGxqIPFlLcgJgU9GmCSk4,15071
174
- cmdop/services/browser/aio/session.py,sha256=2MuRXNM4wlc_6B-Xe8_DjyHFX8x7UaOmfic4T5jLHXI,12847
175
- cmdop/services/browser/base/__init__.py,sha256=mgG4y324zSDy-13uQiTg6rlFpaQFLc67ynbykRZuMms,227
176
- cmdop/services/browser/base/service.py,sha256=w8foDUGZcD4HyF5eyLZUFxbx_fctAFsYRovvsksi3l4,1584
177
- cmdop/services/browser/base/session.py,sha256=5-UiBxG3t5TtATih41JaPR9PdmIKfeICTM1RbhRdBok,3773
178
- cmdop/services/browser/js/__init__.py,sha256=lb86NItjLKT6GnePAIA7S9Du3G_Fb5aXe12PuKszYmY,1235
172
+ cmdop/services/browser/session.py,sha256=ZFZuzxEUEpwDenigBZdxr5aYePlo69LWre9YIK6LRSM,5363
173
+ cmdop/services/browser/capabilities/__init__.py,sha256=SvaRxAiB8pq671bWtTThVe5BgVg0G-4wAPcgOvu9ab4,334
174
+ cmdop/services/browser/capabilities/_base.py,sha256=mW0jKa2CyvK-8cjenv5JYvuCKiO3rpt5F7WtWFXBitA,749
175
+ cmdop/services/browser/capabilities/_helpers.py,sha256=jXqYbeDocAHec2GwF2_BNnJ78vTyUnHteQoS-RSG00k,488
176
+ cmdop/services/browser/capabilities/dom.py,sha256=DuXfildga23wGBNJWtNzx-t2Cq553HC48o9KAWAlyC0,2612
177
+ cmdop/services/browser/capabilities/fetch.py,sha256=GAjOjk9XkCrMgUzRUrdMmVgOmIClwBWPUnmfA_OqITY,1392
178
+ cmdop/services/browser/capabilities/input.py,sha256=uYmWGqturMDent44Us80oT_nk4kFNGyPJ0P5I35ulew,1749
179
+ cmdop/services/browser/capabilities/scroll.py,sha256=sh0VuOPOv81BZg80-n8TABOj5RpshJT12qJwm4F_OY0,4808
180
+ cmdop/services/browser/capabilities/timing.py,sha256=NH34G_4Kfukh6JCdhLRGoouA-uNTbx9ly7ybP9Kh558,1868
181
+ cmdop/services/browser/js/__init__.py,sha256=gTiZguikKfztDtggZTux2FqhT8YTjyHCzQR4TEnT7z4,1177
179
182
  cmdop/services/browser/js/core.py,sha256=QXCCX_al5tMgz7aCwMqhIs1aRe_IdG8teOJniaumA5Q,995
180
183
  cmdop/services/browser/js/fetch.py,sha256=WPy_H4LLkneSx06wpfnx4Sx_0Okf2ENXi6bveCd9ZCg,2188
181
- cmdop/services/browser/js/interaction.py,sha256=zdCy1l833k_a470qm3Q9QLKQrbSQ5tYgTUa4rVGailc,5444
184
+ cmdop/services/browser/js/interaction.py,sha256=qNcs9XwjXZPJfbg8BPeqw5hXya_xTQsHv1T7VWdQ7qY,6620
182
185
  cmdop/services/browser/js/scroll.py,sha256=yiOMAaR8ac8jCiWgNCVIt1pUGxdvktMUtzv-AVAE-2Q,7823
183
- cmdop/services/browser/sync/__init__.py,sha256=sFePvIsWqhaeCU9fw_ZyOITeeEHvZx4Gtcp8W3KBKG0,222
184
- cmdop/services/browser/sync/service.py,sha256=HGLjGdAmmiljyub2q5KSkNSD2zB8tsk4n92vJT18mCs,15364
185
- cmdop/services/browser/sync/session.py,sha256=CsINEF971Y91Zg9hFmEol7jTWp3TaiNKpSL2CXMZXAg,19684
186
+ cmdop/services/browser/service/__init__.py,sha256=AZH_r2FsxLfJGCVBaaAPw3dTGaUlgIFdlYd_RR8KxSg,100
187
+ cmdop/services/browser/service/_helpers.py,sha256=w8foDUGZcD4HyF5eyLZUFxbx_fctAFsYRovvsksi3l4,1584
188
+ cmdop/services/browser/service/aio.py,sha256=0E2D4igQb3YzAakdXsRMt8PEd0rFxI2gXfimwq_6nzk,767
189
+ cmdop/services/browser/service/sync.py,sha256=meSoRacS5WVBTABiXopWvTIItzvR7nUiyaxnDuUMoUQ,15466
186
190
  cmdop/streaming/__init__.py,sha256=kG9UlJRqv8ndcwKMzWUddPlZT61pFO_Uf_c08A_8TxA,877
187
191
  cmdop/streaming/base.py,sha256=r7Q2QlRxgULzs9vlSGcOC_fwAQ_cF3Z3M7WsPQtxG5I,2990
188
192
  cmdop/streaming/handlers.py,sha256=FDEhADmCEFRbifvr9dU1X3C-K_96noz89Bl3tuDa_rQ,2616
@@ -193,7 +197,7 @@ cmdop/transport/base.py,sha256=2pkV8i9epgp_21dyReCfX47abRUrnALm0W5BXb-Fuz0,5571
193
197
  cmdop/transport/discovery.py,sha256=rcGAuVrR1l6jwcP0dqZxVhX1NsFK7sRHygFMCLmmUbA,10673
194
198
  cmdop/transport/local.py,sha256=ob6tWVxSdKwblHSMK8CkgjyuSdQoAeWgy5OAUd5ZNuE,7411
195
199
  cmdop/transport/remote.py,sha256=FNVqus9wOv7LlxKarXjLmSyvJiHwhvPbNDOPv1IQkmE,4329
196
- cmdop-0.1.20.dist-info/METADATA,sha256=M2-UmMdH309qO_sQ5xBDohYrydjedT8YDUbX9JeZgfA,7019
197
- cmdop-0.1.20.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
198
- cmdop-0.1.20.dist-info/licenses/LICENSE,sha256=6hyzbI1QVXW6B-XT7PaQ6UG9lns11Y_nnap8uUKGUqo,1062
199
- cmdop-0.1.20.dist-info/RECORD,,
200
+ cmdop-0.1.22.dist-info/METADATA,sha256=OTt5H55-dBCDKBLpTJSiNtbsJAz5Wxy9jKTrIb7HTxE,7574
201
+ cmdop-0.1.22.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
202
+ cmdop-0.1.22.dist-info/licenses/LICENSE,sha256=6hyzbI1QVXW6B-XT7PaQ6UG9lns11Y_nnap8uUKGUqo,1062
203
+ cmdop-0.1.22.dist-info/RECORD,,
@@ -1,6 +0,0 @@
1
- """Asynchronous browser session and service."""
2
-
3
- from cmdop.services.browser.aio.session import AsyncBrowserSession
4
- from cmdop.services.browser.aio.service import AsyncBrowserService
5
-
6
- __all__ = ["AsyncBrowserSession", "AsyncBrowserService"]
@@ -1,415 +0,0 @@
1
- """Asynchronous browser service."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- from typing import TYPE_CHECKING, Any
7
-
8
- from cmdop.services.base import BaseService
9
- from cmdop.services.browser.base.service import BaseServiceMixin, cookie_to_pb, pb_to_cookie
10
- from cmdop.services.browser.models import BrowserCookie, BrowserState, PageInfo, ScrollResult, raise_browser_error
11
-
12
- if TYPE_CHECKING:
13
- from cmdop.transport.base import BaseTransport
14
-
15
-
16
- class AsyncBrowserService(BaseService, BaseServiceMixin):
17
- """
18
- Asynchronous browser service.
19
-
20
- Example:
21
- async with client.browser.create_session() as session:
22
- await session.navigate("https://google.com")
23
- await session.type("input[name='q']", "Python")
24
- results = await session.extract(".result-title")
25
- """
26
-
27
- def __init__(self, transport: BaseTransport) -> None:
28
- super().__init__(transport)
29
- self._stub: Any = None
30
-
31
- @property
32
- def _get_stub(self) -> Any:
33
- if self._stub is None:
34
- from cmdop._generated.service_pb2_grpc import TerminalStreamingServiceStub
35
- self._stub = TerminalStreamingServiceStub(self._async_channel)
36
- return self._stub
37
-
38
- # === Session Management ===
39
-
40
- async def create_session(
41
- self,
42
- start_url: str | None = None,
43
- provider: str = "camoufox",
44
- profile_id: str | None = None,
45
- headless: bool = False,
46
- width: int = 1280,
47
- height: int = 800,
48
- ) -> "AsyncBrowserSession":
49
- from cmdop._generated.rpc_messages.browser_pb2 import BrowserCreateSessionRequest
50
- from cmdop.services.browser.aio.session import AsyncBrowserSession
51
-
52
- request = BrowserCreateSessionRequest(
53
- provider=provider,
54
- profile_id=profile_id or "",
55
- start_url=start_url or "",
56
- headless=headless,
57
- width=width,
58
- height=height,
59
- )
60
- response = await self._call_async(self._get_stub.BrowserCreateSession, request)
61
-
62
- if not response.success:
63
- raise_browser_error(response.error, "create_session")
64
-
65
- return AsyncBrowserSession(self, response.browser_session_id)
66
-
67
- async def close_session(self, session_id: str) -> None:
68
- from cmdop._generated.rpc_messages.browser_pb2 import BrowserCloseSessionRequest
69
-
70
- request = BrowserCloseSessionRequest(browser_session_id=session_id)
71
- response = await self._call_async(self._get_stub.BrowserCloseSession, request)
72
-
73
- if not response.success:
74
- raise RuntimeError(f"Failed to close browser session: {response.error}")
75
-
76
- # === Navigation & Interaction ===
77
-
78
- async def navigate(self, session_id: str, url: str, timeout_ms: int = 30000) -> str:
79
- from cmdop._generated.rpc_messages.browser_pb2 import BrowserNavigateRequest
80
-
81
- request = BrowserNavigateRequest(
82
- browser_session_id=session_id, url=url, timeout_ms=timeout_ms
83
- )
84
- response = await self._call_async(self._get_stub.BrowserNavigate, request)
85
-
86
- if not response.success:
87
- raise_browser_error(response.error, "navigate", url=url)
88
-
89
- return response.final_url
90
-
91
- async def click(self, session_id: str, selector: str, timeout_ms: int = 5000) -> None:
92
- from cmdop._generated.rpc_messages.browser_pb2 import BrowserClickRequest
93
-
94
- request = BrowserClickRequest(
95
- browser_session_id=session_id, selector=selector, timeout_ms=timeout_ms
96
- )
97
- response = await self._call_async(self._get_stub.BrowserClick, request)
98
-
99
- if not response.success:
100
- raise_browser_error(response.error, "click", selector=selector)
101
-
102
- async def type(
103
- self,
104
- session_id: str,
105
- selector: str,
106
- text: str,
107
- human_like: bool = False,
108
- clear_first: bool = True,
109
- ) -> None:
110
- from cmdop._generated.rpc_messages.browser_pb2 import BrowserTypeRequest
111
-
112
- request = BrowserTypeRequest(
113
- browser_session_id=session_id,
114
- selector=selector,
115
- text=text,
116
- human_like=human_like,
117
- clear_first=clear_first,
118
- )
119
- response = await self._call_async(self._get_stub.BrowserType, request)
120
-
121
- if not response.success:
122
- raise_browser_error(response.error, "type", selector=selector)
123
-
124
- async def wait_for(
125
- self, session_id: str, selector: str, timeout_ms: int = 30000
126
- ) -> bool:
127
- from cmdop._generated.rpc_messages.browser_pb2 import BrowserWaitRequest
128
-
129
- request = BrowserWaitRequest(
130
- browser_session_id=session_id, selector=selector, timeout_ms=timeout_ms
131
- )
132
- response = await self._call_async(self._get_stub.BrowserWait, request)
133
-
134
- if not response.success:
135
- raise_browser_error(response.error, "wait_for", selector=selector)
136
-
137
- return response.found
138
-
139
- # === Extraction ===
140
-
141
- async def extract(
142
- self, session_id: str, selector: str, attr: str | None = None, limit: int = 100
143
- ) -> list[str]:
144
- from cmdop._generated.rpc_messages.browser_pb2 import BrowserExtractRequest
145
-
146
- request = BrowserExtractRequest(
147
- browser_session_id=session_id,
148
- selector=selector,
149
- attribute=attr or "",
150
- limit=limit,
151
- )
152
- response = await self._call_async(self._get_stub.BrowserExtract, request)
153
-
154
- if not response.success:
155
- raise RuntimeError(f"Extract failed: {response.error}")
156
-
157
- return list(response.values)
158
-
159
- async def extract_regex(
160
- self, session_id: str, pattern: str, from_html: bool = False, limit: int = 100
161
- ) -> list[str]:
162
- from cmdop._generated.rpc_messages.browser_pb2 import BrowserExtractRegexRequest
163
-
164
- request = BrowserExtractRegexRequest(
165
- browser_session_id=session_id,
166
- pattern=pattern,
167
- from_html=from_html,
168
- limit=limit,
169
- )
170
- response = await self._call_async(self._get_stub.BrowserExtractRegex, request)
171
-
172
- if not response.success:
173
- raise RuntimeError(f"ExtractRegex failed: {response.error}")
174
-
175
- return list(response.matches)
176
-
177
- async def get_html(self, session_id: str, selector: str | None = None) -> str:
178
- from cmdop._generated.rpc_messages.browser_pb2 import BrowserGetHTMLRequest
179
-
180
- request = BrowserGetHTMLRequest(
181
- browser_session_id=session_id, selector=selector or ""
182
- )
183
- response = await self._call_async(self._get_stub.BrowserGetHTML, request)
184
-
185
- if not response.success:
186
- raise RuntimeError(f"GetHTML failed: {response.error}")
187
-
188
- return response.html
189
-
190
- async def get_text(self, session_id: str, selector: str | None = None) -> str:
191
- from cmdop._generated.rpc_messages.browser_pb2 import BrowserGetTextRequest
192
-
193
- request = BrowserGetTextRequest(
194
- browser_session_id=session_id, selector=selector or ""
195
- )
196
- response = await self._call_async(self._get_stub.BrowserGetText, request)
197
-
198
- if not response.success:
199
- raise RuntimeError(f"GetText failed: {response.error}")
200
-
201
- return response.text
202
-
203
- # === JavaScript ===
204
-
205
- async def execute_script(self, session_id: str, script: str) -> str:
206
- from cmdop._generated.rpc_messages.browser_pb2 import BrowserExecuteScriptRequest
207
-
208
- request = BrowserExecuteScriptRequest(
209
- browser_session_id=session_id, script=script
210
- )
211
- response = await self._call_async(
212
- self._get_stub.BrowserExecuteScript, request
213
- )
214
-
215
- if not response.success:
216
- raise RuntimeError(f"ExecuteScript failed: {response.error}")
217
-
218
- return response.result
219
-
220
- # === State & Cookies ===
221
-
222
- async def screenshot(self, session_id: str, full_page: bool = False) -> bytes:
223
- from cmdop._generated.rpc_messages.browser_pb2 import BrowserScreenshotRequest
224
-
225
- request = BrowserScreenshotRequest(
226
- browser_session_id=session_id, full_page=full_page
227
- )
228
- response = await self._call_async(self._get_stub.BrowserScreenshot, request)
229
-
230
- if not response.success:
231
- raise RuntimeError(f"Screenshot failed: {response.error}")
232
-
233
- return response.data
234
-
235
- async def get_state(self, session_id: str) -> BrowserState:
236
- from cmdop._generated.rpc_messages.browser_pb2 import BrowserGetStateRequest
237
-
238
- request = BrowserGetStateRequest(browser_session_id=session_id)
239
- response = await self._call_async(self._get_stub.BrowserGetState, request)
240
-
241
- if not response.success:
242
- raise RuntimeError(f"GetState failed: {response.error}")
243
-
244
- return BrowserState(url=response.url, title=response.title)
245
-
246
- async def set_cookies(
247
- self, session_id: str, cookies: list[BrowserCookie | dict]
248
- ) -> None:
249
- from cmdop._generated.rpc_messages.browser_pb2 import (
250
- BrowserCookie as PbCookie,
251
- BrowserSetCookiesRequest,
252
- )
253
-
254
- pb_cookies = [cookie_to_pb(c, PbCookie) for c in cookies]
255
- request = BrowserSetCookiesRequest(
256
- browser_session_id=session_id, cookies=pb_cookies
257
- )
258
- response = await self._call_async(self._get_stub.BrowserSetCookies, request)
259
-
260
- if not response.success:
261
- raise RuntimeError(f"SetCookies failed: {response.error}")
262
-
263
- async def get_cookies(
264
- self, session_id: str, domain: str = ""
265
- ) -> list[BrowserCookie]:
266
- from cmdop._generated.rpc_messages.browser_pb2 import BrowserGetCookiesRequest
267
-
268
- request = BrowserGetCookiesRequest(
269
- browser_session_id=session_id, domain=domain
270
- )
271
- response = await self._call_async(self._get_stub.BrowserGetCookies, request)
272
-
273
- if not response.success:
274
- raise RuntimeError(f"GetCookies failed: {response.error}")
275
-
276
- return [pb_to_cookie(c) for c in response.cookies]
277
-
278
- # === Parser helpers ===
279
-
280
- async def validate_selectors(
281
- self, session_id: str, item: str, fields: dict[str, str]
282
- ) -> dict:
283
- from cmdop._generated.rpc_messages.browser_pb2 import BrowserValidateSelectorsRequest
284
-
285
- request = BrowserValidateSelectorsRequest(
286
- browser_session_id=session_id,
287
- item=item,
288
- fields=fields,
289
- )
290
- response = await self._call_async(
291
- self._get_stub.BrowserValidateSelectors, request
292
- )
293
-
294
- if not response.success:
295
- raise RuntimeError(f"ValidateSelectors failed: {response.error}")
296
-
297
- return {
298
- "valid": response.valid,
299
- "counts": dict(response.counts),
300
- "samples": dict(response.samples),
301
- "errors": list(response.errors),
302
- }
303
-
304
- async def extract_data(
305
- self, session_id: str, item: str, fields_json: str, limit: int = 100
306
- ) -> dict:
307
- from cmdop._generated.rpc_messages.browser_pb2 import BrowserExtractDataRequest
308
-
309
- request = BrowserExtractDataRequest(
310
- browser_session_id=session_id,
311
- item=item,
312
- fields_json=fields_json,
313
- limit=limit,
314
- )
315
- response = await self._call_async(self._get_stub.BrowserExtractData, request)
316
-
317
- if not response.success:
318
- raise RuntimeError(f"ExtractData failed: {response.error}")
319
-
320
- return {
321
- "items": json.loads(response.items_json) if response.items_json else [],
322
- "count": response.count,
323
- }
324
-
325
- # === Mouse & Scroll (v2.18.0) ===
326
-
327
- async def mouse_move(
328
- self, session_id: str, x: int, y: int, steps: int = 10
329
- ) -> None:
330
- """Move cursor to coordinates with smooth movement."""
331
- from cmdop._generated.rpc_messages.browser_pb2 import BrowserMouseMoveRequest
332
-
333
- request = BrowserMouseMoveRequest(
334
- browser_session_id=session_id, x=x, y=y, steps=steps
335
- )
336
- response = await self._call_async(self._get_stub.BrowserMouseMove, request)
337
-
338
- if not response.success:
339
- raise RuntimeError(f"MouseMove failed: {response.error}")
340
-
341
- async def hover(self, session_id: str, selector: str, timeout_ms: int = 5000) -> None:
342
- """Hover over element by selector."""
343
- from cmdop._generated.rpc_messages.browser_pb2 import BrowserHoverRequest
344
-
345
- request = BrowserHoverRequest(
346
- browser_session_id=session_id, selector=selector, timeout_ms=timeout_ms
347
- )
348
- response = await self._call_async(self._get_stub.BrowserHover, request)
349
-
350
- if not response.success:
351
- raise_browser_error(response.error, "hover", selector=selector)
352
-
353
- async def scroll(
354
- self,
355
- session_id: str,
356
- direction: str = "down",
357
- amount: int = 500,
358
- selector: str = "",
359
- smooth: bool = True,
360
- ) -> ScrollResult:
361
- """Scroll page or element into view."""
362
- from cmdop._generated.rpc_messages.browser_pb2 import BrowserScrollRequest
363
-
364
- request = BrowserScrollRequest(
365
- browser_session_id=session_id,
366
- direction=direction,
367
- amount=amount,
368
- selector=selector,
369
- smooth=smooth,
370
- )
371
- response = await self._call_async(self._get_stub.BrowserScroll, request)
372
-
373
- if not response.success:
374
- return ScrollResult(success=False, error=response.error)
375
-
376
- return ScrollResult(
377
- success=True,
378
- scroll_x=response.scroll_x,
379
- scroll_y=response.scroll_y,
380
- scrolled_by=response.scrolled_by,
381
- page_height=response.page_height,
382
- viewport_height=response.viewport_height,
383
- at_bottom=response.at_bottom,
384
- )
385
-
386
- async def get_page_info(self, session_id: str) -> PageInfo:
387
- """Get comprehensive page information."""
388
- from cmdop._generated.rpc_messages.browser_pb2 import BrowserGetPageInfoRequest
389
-
390
- request = BrowserGetPageInfoRequest(browser_session_id=session_id)
391
- response = await self._call_async(self._get_stub.BrowserGetPageInfo, request)
392
-
393
- if not response.success:
394
- raise RuntimeError(f"GetPageInfo failed: {response.error}")
395
-
396
- return PageInfo(
397
- url=response.url,
398
- title=response.title,
399
- page_height=response.page_height,
400
- viewport_height=response.viewport_height,
401
- viewport_width=response.viewport_width,
402
- scroll_x=response.scroll_x,
403
- scroll_y=response.scroll_y,
404
- at_top=response.at_top,
405
- at_bottom=response.at_bottom,
406
- load_time_ms=response.load_time_ms,
407
- cookies_count=response.cookies_count,
408
- is_https=response.is_https,
409
- has_iframes=response.has_iframes,
410
- dom_nodes_raw=response.dom_nodes_raw,
411
- dom_nodes_cleaned=response.dom_nodes_cleaned,
412
- tokens_estimate=response.tokens_estimate,
413
- cloudflare_detected=response.cloudflare_detected,
414
- captcha_detected=response.captcha_detected,
415
- )