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.
- cmdop/__init__.py +1 -1
- cmdop/_generated/rpc_messages/browser_pb2.py +81 -81
- cmdop/_generated/rpc_messages/browser_pb2.pyi +4 -2
- cmdop/client.py +2 -8
- cmdop/services/browser/__init__.py +44 -31
- cmdop/services/browser/capabilities/__init__.py +15 -0
- cmdop/services/browser/capabilities/_base.py +28 -0
- cmdop/services/browser/capabilities/_helpers.py +16 -0
- cmdop/services/browser/capabilities/dom.py +76 -0
- cmdop/services/browser/capabilities/fetch.py +46 -0
- cmdop/services/browser/capabilities/input.py +49 -0
- cmdop/services/browser/capabilities/scroll.py +147 -0
- cmdop/services/browser/capabilities/timing.py +66 -0
- cmdop/services/browser/js/__init__.py +6 -4
- cmdop/services/browser/js/interaction.py +34 -0
- cmdop/services/browser/service/__init__.py +5 -0
- cmdop/services/browser/service/aio.py +30 -0
- cmdop/services/browser/{sync/service.py → service/sync.py} +9 -4
- cmdop/services/browser/session.py +166 -0
- {cmdop-0.1.20.dist-info → cmdop-0.1.22.dist-info}/METADATA +70 -41
- {cmdop-0.1.20.dist-info → cmdop-0.1.22.dist-info}/RECORD +24 -20
- cmdop/services/browser/aio/__init__.py +0 -6
- cmdop/services/browser/aio/service.py +0 -415
- cmdop/services/browser/aio/session.py +0 -358
- cmdop/services/browser/base/__init__.py +0 -6
- cmdop/services/browser/base/session.py +0 -124
- cmdop/services/browser/sync/__init__.py +0 -6
- cmdop/services/browser/sync/session.py +0 -580
- /cmdop/services/browser/{base/service.py → service/_helpers.py} +0 -0
- {cmdop-0.1.20.dist-info → cmdop-0.1.22.dist-info}/WHEEL +0 -0
- {cmdop-0.1.20.dist-info → cmdop-0.1.22.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Scroll capability."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any, Callable
|
|
5
|
+
|
|
6
|
+
from cmdop.services.browser.js import (
|
|
7
|
+
build_scroll_js,
|
|
8
|
+
build_scroll_to_bottom_js,
|
|
9
|
+
build_get_scroll_info_js,
|
|
10
|
+
build_infinite_scroll_js,
|
|
11
|
+
)
|
|
12
|
+
from cmdop.services.browser.models import ScrollResult, ScrollInfo, InfiniteScrollResult
|
|
13
|
+
|
|
14
|
+
from ._base import BaseCapability
|
|
15
|
+
from ._helpers import to_dict
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ScrollCapability(BaseCapability):
|
|
19
|
+
"""Scroll operations.
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
session.scroll.js("down", 500)
|
|
23
|
+
session.scroll.to_bottom()
|
|
24
|
+
info = session.scroll.info()
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def js(
|
|
28
|
+
self,
|
|
29
|
+
direction: str = "down",
|
|
30
|
+
amount: int = 500,
|
|
31
|
+
selector: str | None = None,
|
|
32
|
+
smooth: bool = True,
|
|
33
|
+
human_like: bool = False,
|
|
34
|
+
container: str | None = None,
|
|
35
|
+
) -> ScrollResult:
|
|
36
|
+
"""Scroll using JavaScript. Use when native scroll doesn't work."""
|
|
37
|
+
js = build_scroll_js(direction, amount, selector, smooth, human_like, container)
|
|
38
|
+
data = to_dict(self._js(js))
|
|
39
|
+
return ScrollResult(
|
|
40
|
+
success=data.get("success", False),
|
|
41
|
+
scroll_y=int(data.get("scrollY", 0)),
|
|
42
|
+
scrolled_by=int(data.get("scrolledBy", 0)),
|
|
43
|
+
at_bottom=data.get("atBottom", False),
|
|
44
|
+
error=data.get("error"),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def to_bottom(self) -> ScrollResult:
|
|
48
|
+
"""Scroll to page bottom."""
|
|
49
|
+
data = to_dict(self._js(build_scroll_to_bottom_js()))
|
|
50
|
+
return ScrollResult(
|
|
51
|
+
success=data.get("success", False),
|
|
52
|
+
scroll_y=int(data.get("scrollY", 0)),
|
|
53
|
+
scrolled_by=int(data.get("scrolledBy", 0)),
|
|
54
|
+
at_bottom=True,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def to_element(self, selector: str) -> ScrollResult:
|
|
58
|
+
"""Scroll element into view."""
|
|
59
|
+
return self.js(selector=selector)
|
|
60
|
+
|
|
61
|
+
def info(self) -> ScrollInfo:
|
|
62
|
+
"""Get scroll position and page dimensions."""
|
|
63
|
+
data = to_dict(self._js(build_get_scroll_info_js()))
|
|
64
|
+
return ScrollInfo(
|
|
65
|
+
scroll_x=int(data.get("scrollX", 0)),
|
|
66
|
+
scroll_y=int(data.get("scrollY", 0)),
|
|
67
|
+
page_height=int(data.get("pageHeight", 0)),
|
|
68
|
+
page_width=int(data.get("pageWidth", 0)),
|
|
69
|
+
viewport_height=int(data.get("viewportHeight", 0)),
|
|
70
|
+
viewport_width=int(data.get("viewportWidth", 0)),
|
|
71
|
+
at_bottom=data.get("atBottom", False),
|
|
72
|
+
at_top=data.get("atTop", True),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def native(
|
|
76
|
+
self,
|
|
77
|
+
direction: str = "down",
|
|
78
|
+
amount: int = 500,
|
|
79
|
+
selector: str | None = None,
|
|
80
|
+
smooth: bool = True,
|
|
81
|
+
) -> ScrollResult:
|
|
82
|
+
"""Scroll using native browser API."""
|
|
83
|
+
data = self._call("scroll", direction, amount, selector, smooth)
|
|
84
|
+
return ScrollResult(
|
|
85
|
+
success=True,
|
|
86
|
+
scroll_y=data.get("scroll_y", 0),
|
|
87
|
+
scrolled_by=data.get("scrolled_by", 0),
|
|
88
|
+
at_bottom=data.get("at_bottom", False),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def collect(
|
|
92
|
+
self,
|
|
93
|
+
seen_keys: set[str],
|
|
94
|
+
key_selector: str = "a[href]",
|
|
95
|
+
key_attr: str = "href",
|
|
96
|
+
container_selector: str = "body",
|
|
97
|
+
) -> InfiniteScrollResult:
|
|
98
|
+
"""Extract new keys for infinite scroll patterns. Updates seen_keys in-place."""
|
|
99
|
+
js = build_infinite_scroll_js(list(seen_keys), key_selector, key_attr, container_selector)
|
|
100
|
+
data = to_dict(self._js(js))
|
|
101
|
+
new_keys = data.get("new_keys", [])
|
|
102
|
+
seen_keys.update(new_keys)
|
|
103
|
+
return InfiniteScrollResult(
|
|
104
|
+
new_keys=new_keys,
|
|
105
|
+
at_bottom=data.get("at_bottom", False),
|
|
106
|
+
total_seen=data.get("total_seen", len(seen_keys)),
|
|
107
|
+
error=data.get("error"),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def infinite(
|
|
111
|
+
self,
|
|
112
|
+
extract_fn: Callable[[], list[Any]],
|
|
113
|
+
limit: int = 100,
|
|
114
|
+
max_scrolls: int = 50,
|
|
115
|
+
max_no_new: int = 3,
|
|
116
|
+
scroll_amount: int = 800,
|
|
117
|
+
delay: float = 1.0,
|
|
118
|
+
) -> list[Any]:
|
|
119
|
+
"""Smart infinite scroll with extraction.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
extract_fn: Returns new items each call (dedup is caller's job)
|
|
123
|
+
limit: Stop after this many items
|
|
124
|
+
max_scrolls: Max scroll attempts
|
|
125
|
+
max_no_new: Stop after N scrolls with no new items
|
|
126
|
+
scroll_amount: Pixels per scroll
|
|
127
|
+
delay: Seconds between scrolls
|
|
128
|
+
"""
|
|
129
|
+
items: list[Any] = []
|
|
130
|
+
no_new = 0
|
|
131
|
+
|
|
132
|
+
for _ in range(max_scrolls):
|
|
133
|
+
new = extract_fn()
|
|
134
|
+
if new:
|
|
135
|
+
items.extend(new)
|
|
136
|
+
no_new = 0
|
|
137
|
+
if len(items) >= limit:
|
|
138
|
+
break
|
|
139
|
+
else:
|
|
140
|
+
no_new += 1
|
|
141
|
+
if no_new >= max_no_new:
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
self.js("down", scroll_amount)
|
|
145
|
+
time.sleep(delay)
|
|
146
|
+
|
|
147
|
+
return items[:limit]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Timing capability."""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
import time
|
|
5
|
+
import threading
|
|
6
|
+
from typing import Callable, TypeVar
|
|
7
|
+
|
|
8
|
+
from ._base import BaseCapability
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TimingCapability(BaseCapability):
|
|
14
|
+
"""Timing operations: wait, delays, timeouts.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
session.timing.wait(1000)
|
|
18
|
+
session.timing.random(0.5, 2.0)
|
|
19
|
+
result, ok = session.timing.timeout(lambda: slow_fn(), 30)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def wait(self, ms: int, jitter: float = 0.1) -> None:
|
|
23
|
+
"""Wait milliseconds with jitter (±10% by default)."""
|
|
24
|
+
actual = (ms / 1000) * (1 + random.uniform(-jitter, jitter))
|
|
25
|
+
time.sleep(actual)
|
|
26
|
+
|
|
27
|
+
def seconds(self, sec: float, jitter: float = 0.1) -> None:
|
|
28
|
+
"""Wait seconds with jitter."""
|
|
29
|
+
self.wait(int(sec * 1000), jitter)
|
|
30
|
+
|
|
31
|
+
def random(self, min_sec: float = 0.5, max_sec: float = 1.5) -> None:
|
|
32
|
+
"""Wait random time between min and max seconds."""
|
|
33
|
+
time.sleep(min_sec + random.random() * (max_sec - min_sec))
|
|
34
|
+
|
|
35
|
+
def timeout(
|
|
36
|
+
self,
|
|
37
|
+
fn: Callable[[], T],
|
|
38
|
+
seconds: float = 60.0,
|
|
39
|
+
on_timeout: Callable[[], None] | None = None,
|
|
40
|
+
) -> tuple[T | None, bool]:
|
|
41
|
+
"""Run function with timeout. Returns (result, success)."""
|
|
42
|
+
result: list[T | None] = [None]
|
|
43
|
+
error: list[Exception | None] = [None]
|
|
44
|
+
done = threading.Event()
|
|
45
|
+
|
|
46
|
+
def run():
|
|
47
|
+
try:
|
|
48
|
+
result[0] = fn()
|
|
49
|
+
except Exception as e:
|
|
50
|
+
error[0] = e
|
|
51
|
+
finally:
|
|
52
|
+
done.set()
|
|
53
|
+
|
|
54
|
+
threading.Thread(target=run, daemon=True).start()
|
|
55
|
+
|
|
56
|
+
if done.wait(timeout=seconds):
|
|
57
|
+
if error[0]:
|
|
58
|
+
raise error[0]
|
|
59
|
+
return result[0], True
|
|
60
|
+
|
|
61
|
+
if on_timeout:
|
|
62
|
+
try:
|
|
63
|
+
on_timeout()
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
return None, False
|
|
@@ -7,29 +7,30 @@ This module provides JavaScript code generators for common browser operations:
|
|
|
7
7
|
- Interaction: Hover, select, modals
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
from
|
|
10
|
+
from .core import (
|
|
11
11
|
parse_json_result,
|
|
12
12
|
build_async_js,
|
|
13
13
|
)
|
|
14
14
|
|
|
15
|
-
from
|
|
15
|
+
from .fetch import (
|
|
16
16
|
build_fetch_js,
|
|
17
17
|
build_fetch_all_js,
|
|
18
18
|
)
|
|
19
19
|
|
|
20
|
-
from
|
|
20
|
+
from .scroll import (
|
|
21
21
|
build_scroll_js,
|
|
22
22
|
build_scroll_to_bottom_js,
|
|
23
23
|
build_infinite_scroll_js,
|
|
24
24
|
build_get_scroll_info_js,
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
-
from
|
|
27
|
+
from .interaction import (
|
|
28
28
|
build_hover_js,
|
|
29
29
|
build_select_js,
|
|
30
30
|
build_close_modal_js,
|
|
31
31
|
build_click_all_by_text_js,
|
|
32
32
|
build_press_key_js,
|
|
33
|
+
build_click_js,
|
|
33
34
|
)
|
|
34
35
|
|
|
35
36
|
__all__ = [
|
|
@@ -50,4 +51,5 @@ __all__ = [
|
|
|
50
51
|
"build_close_modal_js",
|
|
51
52
|
"build_click_all_by_text_js",
|
|
52
53
|
"build_press_key_js",
|
|
54
|
+
"build_click_js",
|
|
53
55
|
]
|
|
@@ -178,3 +178,37 @@ def build_click_all_by_text_js(text: str, role: str = "button") -> str:
|
|
|
178
178
|
return JSON.stringify({{ clicked: clicked }});
|
|
179
179
|
}})()
|
|
180
180
|
"""
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def build_click_js(selector: str, scroll_into_view: bool = True) -> str:
|
|
184
|
+
"""
|
|
185
|
+
Build JS to click element via JavaScript (more reliable than CDP click).
|
|
186
|
+
|
|
187
|
+
This is useful when native CDP click hangs or doesn't work properly.
|
|
188
|
+
Uses document.querySelector to find element and calls .click() directly.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
selector: CSS selector for the element to click
|
|
192
|
+
scroll_into_view: If True, scroll element into view before clicking (default: True)
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
JS code that returns { success: true/false, error?: string }
|
|
196
|
+
"""
|
|
197
|
+
scroll_code = 'el.scrollIntoView({block: "center", behavior: "instant"});' if scroll_into_view else ''
|
|
198
|
+
selector_escaped = json.dumps(selector)
|
|
199
|
+
|
|
200
|
+
return f"""
|
|
201
|
+
(function() {{
|
|
202
|
+
const el = document.querySelector({selector_escaped});
|
|
203
|
+
if (!el) {{
|
|
204
|
+
return JSON.stringify({{ success: false, error: 'Element not found' }});
|
|
205
|
+
}}
|
|
206
|
+
try {{
|
|
207
|
+
{scroll_code}
|
|
208
|
+
el.click();
|
|
209
|
+
return JSON.stringify({{ success: true }});
|
|
210
|
+
}} catch (e) {{
|
|
211
|
+
return JSON.stringify({{ success: false, error: e.message }});
|
|
212
|
+
}}
|
|
213
|
+
}})()
|
|
214
|
+
"""
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Async browser service stub.
|
|
2
|
+
|
|
3
|
+
Async browser is not implemented yet. Use sync BrowserService instead.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING, NoReturn
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from cmdop.transport.base import BaseTransport
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AsyncBrowserService:
|
|
15
|
+
"""
|
|
16
|
+
Async browser service stub.
|
|
17
|
+
|
|
18
|
+
Not implemented yet. Use sync CMDOPClient.browser instead.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, transport: BaseTransport) -> None:
|
|
22
|
+
self._transport = transport
|
|
23
|
+
|
|
24
|
+
def _not_implemented(self) -> NoReturn:
|
|
25
|
+
raise NotImplementedError(
|
|
26
|
+
"Async browser is not implemented. Use sync CMDOPClient.browser instead."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
async def create_session(self, *args, **kwargs) -> NoReturn:
|
|
30
|
+
self._not_implemented()
|
|
@@ -6,7 +6,7 @@ import json
|
|
|
6
6
|
from typing import TYPE_CHECKING, Any
|
|
7
7
|
|
|
8
8
|
from cmdop.services.base import BaseService
|
|
9
|
-
from cmdop.services.browser.
|
|
9
|
+
from cmdop.services.browser.service._helpers import BaseServiceMixin, cookie_to_pb, pb_to_cookie
|
|
10
10
|
from cmdop.services.browser.models import BrowserCookie, BrowserState, PageInfo, raise_browser_error
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
@@ -47,7 +47,7 @@ class BrowserService(BaseService, BaseServiceMixin):
|
|
|
47
47
|
height: int = 800,
|
|
48
48
|
) -> "BrowserSession":
|
|
49
49
|
from cmdop._generated.rpc_messages.browser_pb2 import BrowserCreateSessionRequest
|
|
50
|
-
from cmdop.services.browser.
|
|
50
|
+
from cmdop.services.browser.session import BrowserSession
|
|
51
51
|
|
|
52
52
|
request = BrowserCreateSessionRequest(
|
|
53
53
|
provider=provider,
|
|
@@ -88,11 +88,16 @@ class BrowserService(BaseService, BaseServiceMixin):
|
|
|
88
88
|
|
|
89
89
|
return response.final_url
|
|
90
90
|
|
|
91
|
-
def click(
|
|
91
|
+
def click(
|
|
92
|
+
self, session_id: str, selector: str, timeout_ms: int = 5000, move_cursor: bool = False
|
|
93
|
+
) -> None:
|
|
92
94
|
from cmdop._generated.rpc_messages.browser_pb2 import BrowserClickRequest
|
|
93
95
|
|
|
94
96
|
request = BrowserClickRequest(
|
|
95
|
-
browser_session_id=session_id,
|
|
97
|
+
browser_session_id=session_id,
|
|
98
|
+
selector=selector,
|
|
99
|
+
timeout_ms=timeout_ms,
|
|
100
|
+
move_cursor=move_cursor,
|
|
96
101
|
)
|
|
97
102
|
response = self._call_sync(self._get_stub.BrowserClick, request)
|
|
98
103
|
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Browser session with capability-based API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from cmdop.services.browser.models import BrowserCookie, BrowserState, PageInfo
|
|
7
|
+
|
|
8
|
+
from .capabilities import (
|
|
9
|
+
ScrollCapability,
|
|
10
|
+
InputCapability,
|
|
11
|
+
TimingCapability,
|
|
12
|
+
DOMCapability,
|
|
13
|
+
FetchCapability,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from cmdop.services.browser.service.sync import BrowserService
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BrowserSession:
|
|
21
|
+
"""Browser session with grouped capabilities.
|
|
22
|
+
|
|
23
|
+
Core methods (on session directly):
|
|
24
|
+
session.navigate(url)
|
|
25
|
+
session.click(selector)
|
|
26
|
+
session.type(selector, text)
|
|
27
|
+
session.wait_for(selector)
|
|
28
|
+
session.execute_script(js)
|
|
29
|
+
|
|
30
|
+
Capabilities (grouped by function):
|
|
31
|
+
session.scroll.js("down", 500)
|
|
32
|
+
session.scroll.to_bottom()
|
|
33
|
+
session.input.click_js(selector)
|
|
34
|
+
session.input.key("Escape")
|
|
35
|
+
session.timing.wait(1000)
|
|
36
|
+
session.dom.soup()
|
|
37
|
+
session.fetch.json("/api/data")
|
|
38
|
+
|
|
39
|
+
Usage:
|
|
40
|
+
with service.create_session() as session:
|
|
41
|
+
session.navigate("https://example.com")
|
|
42
|
+
session.scroll.js("down", 500)
|
|
43
|
+
session.input.click_js(".button")
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
__slots__ = (
|
|
47
|
+
"_service",
|
|
48
|
+
"_session_id",
|
|
49
|
+
"_scroll",
|
|
50
|
+
"_input",
|
|
51
|
+
"_timing",
|
|
52
|
+
"_dom",
|
|
53
|
+
"_fetch",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def __init__(self, service: "BrowserService", session_id: str) -> None:
|
|
57
|
+
self._service = service
|
|
58
|
+
self._session_id = session_id
|
|
59
|
+
self._scroll: ScrollCapability | None = None
|
|
60
|
+
self._input: InputCapability | None = None
|
|
61
|
+
self._timing: TimingCapability | None = None
|
|
62
|
+
self._dom: DOMCapability | None = None
|
|
63
|
+
self._fetch: FetchCapability | None = None
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def session_id(self) -> str:
|
|
67
|
+
return self._session_id
|
|
68
|
+
|
|
69
|
+
# === Capabilities (lazy init) ===
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def scroll(self) -> ScrollCapability:
|
|
73
|
+
"""Scroll: js(), to_bottom(), to_element(), info(), native(), collect()"""
|
|
74
|
+
if self._scroll is None:
|
|
75
|
+
self._scroll = ScrollCapability(self)
|
|
76
|
+
return self._scroll
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def input(self) -> InputCapability:
|
|
80
|
+
"""Input: click_js(), key(), click_all(), hover(), hover_js()"""
|
|
81
|
+
if self._input is None:
|
|
82
|
+
self._input = InputCapability(self)
|
|
83
|
+
return self._input
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def timing(self) -> TimingCapability:
|
|
87
|
+
"""Timing: wait(), seconds(), random(), timeout()"""
|
|
88
|
+
if self._timing is None:
|
|
89
|
+
self._timing = TimingCapability(self)
|
|
90
|
+
return self._timing
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def dom(self) -> DOMCapability:
|
|
94
|
+
"""DOM: html(), text(), soup(), parse(), select(), close_modal(), extract()"""
|
|
95
|
+
if self._dom is None:
|
|
96
|
+
self._dom = DOMCapability(self)
|
|
97
|
+
return self._dom
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def fetch(self) -> FetchCapability:
|
|
101
|
+
"""Fetch: json(), all(), execute()"""
|
|
102
|
+
if self._fetch is None:
|
|
103
|
+
self._fetch = FetchCapability(self)
|
|
104
|
+
return self._fetch
|
|
105
|
+
|
|
106
|
+
# === Core Methods ===
|
|
107
|
+
|
|
108
|
+
def navigate(self, url: str, timeout_ms: int = 30000) -> str:
|
|
109
|
+
"""Navigate to URL. Returns final URL."""
|
|
110
|
+
return self._service.navigate(self._session_id, url, timeout_ms)
|
|
111
|
+
|
|
112
|
+
def click(self, selector: str, timeout_ms: int = 5000, move_cursor: bool = False) -> None:
|
|
113
|
+
"""Click element by CSS selector."""
|
|
114
|
+
self._service.click(self._session_id, selector, timeout_ms, move_cursor)
|
|
115
|
+
|
|
116
|
+
def type(self, selector: str, text: str, human_like: bool = False, clear_first: bool = True) -> None:
|
|
117
|
+
"""Type text into element."""
|
|
118
|
+
self._service.type(self._session_id, selector, text, human_like, clear_first)
|
|
119
|
+
|
|
120
|
+
def wait_for(self, selector: str, timeout_ms: int = 30000) -> bool:
|
|
121
|
+
"""Wait for element to appear."""
|
|
122
|
+
return self._service.wait_for(self._session_id, selector, timeout_ms)
|
|
123
|
+
|
|
124
|
+
def execute_script(self, script: str) -> str:
|
|
125
|
+
"""Execute raw JavaScript."""
|
|
126
|
+
return self._service.execute_script(self._session_id, script)
|
|
127
|
+
|
|
128
|
+
# === State ===
|
|
129
|
+
|
|
130
|
+
def screenshot(self, full_page: bool = False) -> bytes:
|
|
131
|
+
"""Take screenshot."""
|
|
132
|
+
return self._service.screenshot(self._session_id, full_page)
|
|
133
|
+
|
|
134
|
+
def get_state(self) -> BrowserState:
|
|
135
|
+
"""Get browser state."""
|
|
136
|
+
return self._service.get_state(self._session_id)
|
|
137
|
+
|
|
138
|
+
def get_cookies(self, domain: str = "") -> list[BrowserCookie]:
|
|
139
|
+
"""Get cookies."""
|
|
140
|
+
return self._service.get_cookies(self._session_id, domain)
|
|
141
|
+
|
|
142
|
+
def set_cookies(self, cookies: list[BrowserCookie | dict]) -> None:
|
|
143
|
+
"""Set cookies."""
|
|
144
|
+
self._service.set_cookies(self._session_id, cookies)
|
|
145
|
+
|
|
146
|
+
def get_page_info(self) -> PageInfo:
|
|
147
|
+
"""Get page info."""
|
|
148
|
+
return self._service.get_page_info(self._session_id)
|
|
149
|
+
|
|
150
|
+
# === Internal ===
|
|
151
|
+
|
|
152
|
+
def _call_service(self, method: str, *args: Any, **kwargs: Any) -> Any:
|
|
153
|
+
"""Call service method (used by capabilities)."""
|
|
154
|
+
return getattr(self._service, method)(self._session_id, *args, **kwargs)
|
|
155
|
+
|
|
156
|
+
# === Context Manager ===
|
|
157
|
+
|
|
158
|
+
def close(self) -> None:
|
|
159
|
+
"""Close session."""
|
|
160
|
+
self._service.close_session(self._session_id)
|
|
161
|
+
|
|
162
|
+
def __enter__(self) -> "BrowserSession":
|
|
163
|
+
return self
|
|
164
|
+
|
|
165
|
+
def __exit__(self, *args: Any) -> None:
|
|
166
|
+
self.close()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cmdop
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.22
|
|
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
|
|
@@ -145,71 +145,100 @@ health: Health = result.output # Typed!
|
|
|
145
145
|
|
|
146
146
|
## Browser
|
|
147
147
|
|
|
148
|
+
Capability-based API for browser automation.
|
|
149
|
+
|
|
148
150
|
```python
|
|
149
|
-
with client.browser.create_session() as
|
|
150
|
-
|
|
151
|
-
|
|
151
|
+
with client.browser.create_session() as s:
|
|
152
|
+
s.navigate("https://shop.com/products")
|
|
153
|
+
s.dom.close_modal() # Close popups
|
|
152
154
|
|
|
153
155
|
# BeautifulSoup parsing
|
|
154
|
-
soup =
|
|
156
|
+
soup = s.dom.soup() # SoupWrapper with chainable API
|
|
155
157
|
for item in soup.select(".product"):
|
|
156
158
|
title = item.select_one("h2").text()
|
|
157
159
|
price = item.attr("data-price")
|
|
158
160
|
|
|
159
161
|
# Scrolling with random delays
|
|
160
162
|
for _ in range(10):
|
|
161
|
-
soup =
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
b.wait_random(0.8, 1.5) # Random delay
|
|
163
|
+
soup = s.dom.soup(".listings")
|
|
164
|
+
s.scroll.js("down", 700)
|
|
165
|
+
s.timing.random(0.8, 1.5)
|
|
165
166
|
|
|
166
|
-
# Click with cursor movement
|
|
167
|
-
|
|
167
|
+
# Click with cursor movement
|
|
168
|
+
s.click("button.buy", move_cursor=True)
|
|
168
169
|
|
|
169
170
|
# Click all "See more" buttons
|
|
170
|
-
|
|
171
|
+
s.input.click_all("See more")
|
|
171
172
|
|
|
172
|
-
#
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
# Mouse operations
|
|
174
|
+
s.input.mouse_move(500, 300)
|
|
175
|
+
s.input.hover(".tooltip-trigger")
|
|
175
176
|
|
|
176
177
|
# JS fetch (bypass CORS, inherit cookies)
|
|
177
|
-
data =
|
|
178
|
+
data = s.fetch.json("/api/items")
|
|
178
179
|
```
|
|
179
180
|
|
|
181
|
+
### Core Methods (on session)
|
|
182
|
+
|
|
180
183
|
| Method | Description |
|
|
181
184
|
|--------|-------------|
|
|
182
185
|
| `navigate(url)` | Go to URL |
|
|
183
|
-
| `click(selector, move_cursor)` | Click element
|
|
184
|
-
| `click_all_by_text(text, role)` | Click all matching elements |
|
|
186
|
+
| `click(selector, move_cursor)` | Click element |
|
|
185
187
|
| `type(selector, text)` | Type text |
|
|
186
|
-
| `wait_for(selector
|
|
187
|
-
| `
|
|
188
|
-
| `wait_random(min, max)` | Random sleep |
|
|
189
|
-
| `extract(selector, attr)` | Get text/attr |
|
|
190
|
-
| `get_html(selector)` | Get HTML |
|
|
191
|
-
| `soup(selector)` | → SoupWrapper |
|
|
192
|
-
| `parse_html(html)` | → BeautifulSoup |
|
|
193
|
-
| `fetch_json(url)` | JS fetch → dict |
|
|
194
|
-
| `fetch_all(urls)` | Parallel fetch |
|
|
195
|
-
| `execute_js(code)` | Run async JS |
|
|
188
|
+
| `wait_for(selector)` | Wait for element |
|
|
189
|
+
| `execute_script(js)` | Run JavaScript |
|
|
196
190
|
| `screenshot()` | PNG bytes |
|
|
197
|
-
| `
|
|
198
|
-
| `
|
|
199
|
-
| `
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
191
|
+
| `get_state()` | URL + title |
|
|
192
|
+
| `get_page_info()` | Full page info |
|
|
193
|
+
| `get/set_cookies()` | Cookie management |
|
|
194
|
+
|
|
195
|
+
### Capabilities
|
|
196
|
+
|
|
197
|
+
**`session.scroll`** - Scrolling
|
|
198
|
+
| Method | Description |
|
|
199
|
+
|--------|-------------|
|
|
200
|
+
| `js(dir, amount)` | JS scroll (works on complex sites) |
|
|
201
|
+
| `native(dir, amount)` | Browser API scroll |
|
|
202
|
+
| `to_bottom()` | Scroll to page bottom |
|
|
203
|
+
| `to_element(selector)` | Scroll element into view |
|
|
204
|
+
| `info()` | Get scroll position |
|
|
205
|
+
| `infinite(extract_fn)` | Smart infinite scroll with extraction |
|
|
206
|
+
|
|
207
|
+
**`session.input`** - Input operations
|
|
208
|
+
| Method | Description |
|
|
209
|
+
|--------|-------------|
|
|
210
|
+
| `click_js(selector)` | JS click (reliable) |
|
|
211
|
+
| `click_all(text, role)` | Click all matching elements |
|
|
212
|
+
| `key(key, selector)` | Press keyboard key |
|
|
213
|
+
| `hover(selector)` | Hover over element (native) |
|
|
214
|
+
| `hover_js(selector)` | Hover via JS |
|
|
215
|
+
| `mouse_move(x, y)` | Move cursor to coordinates |
|
|
216
|
+
|
|
217
|
+
**`session.timing`** - Delays
|
|
218
|
+
| Method | Description |
|
|
219
|
+
|--------|-------------|
|
|
220
|
+
| `wait(ms)` | Wait milliseconds |
|
|
221
|
+
| `seconds(n)` | Wait seconds |
|
|
222
|
+
| `random(min, max)` | Random delay |
|
|
223
|
+
| `timeout(fn, sec, cleanup)` | Run with timeout |
|
|
224
|
+
|
|
225
|
+
**`session.dom`** - DOM operations
|
|
226
|
+
| Method | Description |
|
|
227
|
+
|--------|-------------|
|
|
228
|
+
| `html(selector)` | Get HTML |
|
|
229
|
+
| `text(selector)` | Get text content |
|
|
230
|
+
| `soup(selector)` | → SoupWrapper |
|
|
231
|
+
| `parse(html)` | → BeautifulSoup |
|
|
232
|
+
| `extract(selector, attr)` | Get text/attr list |
|
|
203
233
|
| `select(selector, value)` | Dropdown select |
|
|
204
234
|
| `close_modal()` | Close dialogs |
|
|
205
|
-
| `press_key(key, selector)` | Press keyboard key |
|
|
206
|
-
| `get/set_cookies()` | Cookie management |
|
|
207
235
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
236
|
+
**`session.fetch`** - HTTP from browser context
|
|
237
|
+
| Method | Description |
|
|
238
|
+
|--------|-------------|
|
|
239
|
+
| `json(url)` | Fetch JSON |
|
|
240
|
+
| `all(requests)` | Parallel fetch |
|
|
241
|
+
| `execute(method, url, ...)` | Custom request |
|
|
213
242
|
|
|
214
243
|
## SDKBaseModel
|
|
215
244
|
|