open-browser-use-sdk 0.1.23__tar.gz
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.
- open_browser_use_sdk-0.1.23/PKG-INFO +59 -0
- open_browser_use_sdk-0.1.23/README.md +47 -0
- open_browser_use_sdk-0.1.23/open_browser_use/__init__.py +19 -0
- open_browser_use_sdk-0.1.23/open_browser_use/client.py +477 -0
- open_browser_use_sdk-0.1.23/open_browser_use_sdk.egg-info/PKG-INFO +59 -0
- open_browser_use_sdk-0.1.23/open_browser_use_sdk.egg-info/SOURCES.txt +8 -0
- open_browser_use_sdk-0.1.23/open_browser_use_sdk.egg-info/dependency_links.txt +1 -0
- open_browser_use_sdk-0.1.23/open_browser_use_sdk.egg-info/top_level.txt +1 -0
- open_browser_use_sdk-0.1.23/pyproject.toml +23 -0
- open_browser_use_sdk-0.1.23/setup.cfg +4 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: open-browser-use-sdk
|
|
3
|
+
Version: 0.1.23
|
|
4
|
+
Summary: Python SDK for Open Browser Use.
|
|
5
|
+
Author: iFurySt
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/iFurySt/open-codex-browser-use
|
|
8
|
+
Project-URL: Repository, https://github.com/iFurySt/open-codex-browser-use
|
|
9
|
+
Project-URL: Issues, https://github.com/iFurySt/open-codex-browser-use/issues
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# Open Browser Use Python SDK
|
|
14
|
+
|
|
15
|
+
Python client for controlling a real Chrome profile through Open Browser Use.
|
|
16
|
+
The package distribution is `open-browser-use-sdk`; the import module remains
|
|
17
|
+
`open_browser_use`.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
pip install open-browser-use-sdk
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The SDK expects the `open-browser-use` CLI and Chrome extension to already be
|
|
26
|
+
installed and connected:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
open-browser-use ping
|
|
30
|
+
open-browser-use info
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
import json
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
from open_browser_use import connect_open_browser_use
|
|
40
|
+
|
|
41
|
+
registry = json.loads(Path("/tmp/open-browser-use/active.json").read_text())
|
|
42
|
+
|
|
43
|
+
browser = connect_open_browser_use(
|
|
44
|
+
socket_path=registry["socketPath"],
|
|
45
|
+
session_id="python-sdk-example",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
browser.client.name_session("Python SDK example - OBU")
|
|
50
|
+
tab = browser.new_tab()
|
|
51
|
+
tab.goto("https://example.com", wait_until="domcontentloaded")
|
|
52
|
+
print(tab.title())
|
|
53
|
+
finally:
|
|
54
|
+
browser.client.finalize_tabs([])
|
|
55
|
+
browser.close()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Use `OpenBrowserUseClient` directly when you need raw Browser Use JSON-RPC or
|
|
59
|
+
CDP methods.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Open Browser Use Python SDK
|
|
2
|
+
|
|
3
|
+
Python client for controlling a real Chrome profile through Open Browser Use.
|
|
4
|
+
The package distribution is `open-browser-use-sdk`; the import module remains
|
|
5
|
+
`open_browser_use`.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pip install open-browser-use-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The SDK expects the `open-browser-use` CLI and Chrome extension to already be
|
|
14
|
+
installed and connected:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
open-browser-use ping
|
|
18
|
+
open-browser-use info
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```py
|
|
24
|
+
import json
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
from open_browser_use import connect_open_browser_use
|
|
28
|
+
|
|
29
|
+
registry = json.loads(Path("/tmp/open-browser-use/active.json").read_text())
|
|
30
|
+
|
|
31
|
+
browser = connect_open_browser_use(
|
|
32
|
+
socket_path=registry["socketPath"],
|
|
33
|
+
session_id="python-sdk-example",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
browser.client.name_session("Python SDK example - OBU")
|
|
38
|
+
tab = browser.new_tab()
|
|
39
|
+
tab.goto("https://example.com", wait_until="domcontentloaded")
|
|
40
|
+
print(tab.title())
|
|
41
|
+
finally:
|
|
42
|
+
browser.client.finalize_tabs([])
|
|
43
|
+
browser.close()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Use `OpenBrowserUseClient` directly when you need raw Browser Use JSON-RPC or
|
|
47
|
+
CDP methods.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .client import (
|
|
2
|
+
OpenBrowserUseBrowser,
|
|
3
|
+
OpenBrowserUseCdp,
|
|
4
|
+
OpenBrowserUseClient,
|
|
5
|
+
OpenBrowserUseLocator,
|
|
6
|
+
OpenBrowserUseTab,
|
|
7
|
+
OpenBrowserUseTabPlaywright,
|
|
8
|
+
connect_open_browser_use,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"OpenBrowserUseBrowser",
|
|
13
|
+
"OpenBrowserUseCdp",
|
|
14
|
+
"OpenBrowserUseClient",
|
|
15
|
+
"OpenBrowserUseLocator",
|
|
16
|
+
"OpenBrowserUseTab",
|
|
17
|
+
"OpenBrowserUseTabPlaywright",
|
|
18
|
+
"connect_open_browser_use",
|
|
19
|
+
]
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import socket
|
|
5
|
+
import struct
|
|
6
|
+
import time
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any, Callable, Literal
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
JsonObject = dict[str, Any]
|
|
12
|
+
LoadState = Literal["domcontentloaded", "load"]
|
|
13
|
+
NotificationHandler = Callable[[JsonObject], None]
|
|
14
|
+
DEFAULT_NAVIGATION_TIMEOUT = 10.0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class OpenBrowserUseClient:
|
|
19
|
+
socket_path: str
|
|
20
|
+
session_id: str = "open-browser-use-python"
|
|
21
|
+
turn_id: str | None = None
|
|
22
|
+
timeout: float = 10.0
|
|
23
|
+
|
|
24
|
+
def __post_init__(self) -> None:
|
|
25
|
+
if self.turn_id is None:
|
|
26
|
+
self.turn_id = f"turn-{time.time_ns()}"
|
|
27
|
+
self._next_id = 1
|
|
28
|
+
self._socket: socket.socket | None = None
|
|
29
|
+
self._notification_handlers: list[NotificationHandler] = []
|
|
30
|
+
|
|
31
|
+
def connect(self) -> "OpenBrowserUseClient":
|
|
32
|
+
if self._socket is None:
|
|
33
|
+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
34
|
+
sock.settimeout(self.timeout)
|
|
35
|
+
sock.connect(self.socket_path)
|
|
36
|
+
self._socket = sock
|
|
37
|
+
return self
|
|
38
|
+
|
|
39
|
+
def close(self) -> None:
|
|
40
|
+
if self._socket is not None:
|
|
41
|
+
self._socket.close()
|
|
42
|
+
self._socket = None
|
|
43
|
+
|
|
44
|
+
def on_notification(self, handler: NotificationHandler) -> Callable[[], None]:
|
|
45
|
+
self._notification_handlers.append(handler)
|
|
46
|
+
|
|
47
|
+
def remove() -> None:
|
|
48
|
+
if handler in self._notification_handlers:
|
|
49
|
+
self._notification_handlers.remove(handler)
|
|
50
|
+
|
|
51
|
+
return remove
|
|
52
|
+
|
|
53
|
+
def request(self, method: str, params: JsonObject | None = None) -> Any:
|
|
54
|
+
self.connect()
|
|
55
|
+
if self._socket is None:
|
|
56
|
+
raise RuntimeError("Open Browser Use socket is not connected")
|
|
57
|
+
request_id = self._next_id
|
|
58
|
+
self._next_id += 1
|
|
59
|
+
merged_params: JsonObject = {
|
|
60
|
+
"session_id": self.session_id,
|
|
61
|
+
"turn_id": self.turn_id,
|
|
62
|
+
}
|
|
63
|
+
if params:
|
|
64
|
+
merged_params.update(params)
|
|
65
|
+
request = {
|
|
66
|
+
"jsonrpc": "2.0",
|
|
67
|
+
"id": request_id,
|
|
68
|
+
"method": method,
|
|
69
|
+
"params": merged_params,
|
|
70
|
+
}
|
|
71
|
+
self._socket.sendall(encode_frame(request))
|
|
72
|
+
while True:
|
|
73
|
+
response = read_frame(self._socket)
|
|
74
|
+
if response.get("id") == request_id:
|
|
75
|
+
if "error" in response:
|
|
76
|
+
message = response["error"].get("message", "Open Browser Use request failed")
|
|
77
|
+
raise RuntimeError(message)
|
|
78
|
+
return response.get("result")
|
|
79
|
+
if "id" not in response and isinstance(response.get("method"), str):
|
|
80
|
+
self._dispatch_notification(response)
|
|
81
|
+
continue
|
|
82
|
+
raise RuntimeError(f"unexpected response id: {response.get('id')!r}")
|
|
83
|
+
|
|
84
|
+
def _dispatch_notification(self, notification: JsonObject) -> None:
|
|
85
|
+
for handler in list(self._notification_handlers):
|
|
86
|
+
handler(notification)
|
|
87
|
+
|
|
88
|
+
def get_info(self) -> Any:
|
|
89
|
+
return self.request("getInfo")
|
|
90
|
+
|
|
91
|
+
def create_tab(self) -> Any:
|
|
92
|
+
return self.request("createTab")
|
|
93
|
+
|
|
94
|
+
def get_tabs(self) -> Any:
|
|
95
|
+
return self.request("getTabs")
|
|
96
|
+
|
|
97
|
+
def get_user_tabs(self) -> Any:
|
|
98
|
+
return self.request("getUserTabs")
|
|
99
|
+
|
|
100
|
+
def get_user_history(self, **params: Any) -> Any:
|
|
101
|
+
return self.request("getUserHistory", params)
|
|
102
|
+
|
|
103
|
+
def claim_user_tab(self, tab_id: int) -> Any:
|
|
104
|
+
return self.request("claimUserTab", {"tabId": tab_id})
|
|
105
|
+
|
|
106
|
+
def finalize_tabs(self, keep: list[JsonObject]) -> Any:
|
|
107
|
+
return self.request("finalizeTabs", {"keep": keep})
|
|
108
|
+
|
|
109
|
+
def name_session(self, name: str) -> Any:
|
|
110
|
+
return self.request("nameSession", {"name": name})
|
|
111
|
+
|
|
112
|
+
def attach(self, tab_id: int) -> Any:
|
|
113
|
+
return self.request("attach", {"tabId": tab_id})
|
|
114
|
+
|
|
115
|
+
def detach(self, tab_id: int) -> Any:
|
|
116
|
+
return self.request("detach", {"tabId": tab_id})
|
|
117
|
+
|
|
118
|
+
def execute_cdp(self, tab_id: int, method: str, command_params: JsonObject | None = None) -> Any:
|
|
119
|
+
return self.request(
|
|
120
|
+
"executeCdp",
|
|
121
|
+
{
|
|
122
|
+
"target": {"tabId": tab_id},
|
|
123
|
+
"method": method,
|
|
124
|
+
"commandParams": command_params or {},
|
|
125
|
+
},
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def move_mouse(self, tab_id: int, x: float, y: float, wait_for_arrival: bool = True) -> Any:
|
|
129
|
+
return self.request(
|
|
130
|
+
"moveMouse",
|
|
131
|
+
{
|
|
132
|
+
"tabId": tab_id,
|
|
133
|
+
"x": x,
|
|
134
|
+
"y": y,
|
|
135
|
+
"waitForArrival": wait_for_arrival,
|
|
136
|
+
},
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def wait_for_file_chooser(self, tab_id: int, timeout_ms: int | None = None) -> Any:
|
|
140
|
+
params: JsonObject = {"tabId": tab_id}
|
|
141
|
+
if timeout_ms is not None:
|
|
142
|
+
params["timeoutMs"] = timeout_ms
|
|
143
|
+
return self.request("waitForFileChooser", params)
|
|
144
|
+
|
|
145
|
+
def set_file_chooser_files(self, file_chooser_id: str, files: list[str]) -> Any:
|
|
146
|
+
return self.request(
|
|
147
|
+
"setFileChooserFiles",
|
|
148
|
+
{
|
|
149
|
+
"fileChooserId": file_chooser_id,
|
|
150
|
+
"files": files,
|
|
151
|
+
},
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def wait_for_download(self, tab_id: int, timeout_ms: int | None = None) -> Any:
|
|
155
|
+
params: JsonObject = {"tabId": tab_id}
|
|
156
|
+
if timeout_ms is not None:
|
|
157
|
+
params["timeoutMs"] = timeout_ms
|
|
158
|
+
return self.request("waitForDownload", params)
|
|
159
|
+
|
|
160
|
+
def download_path(self, download_id: str, timeout_ms: int | None = None) -> Any:
|
|
161
|
+
params: JsonObject = {"downloadId": download_id}
|
|
162
|
+
if timeout_ms is not None:
|
|
163
|
+
params["timeoutMs"] = timeout_ms
|
|
164
|
+
return self.request("downloadPath", params)
|
|
165
|
+
|
|
166
|
+
def browser_user_history(self, **params: Any) -> Any:
|
|
167
|
+
return self.get_user_history(**params)
|
|
168
|
+
|
|
169
|
+
def read_clipboard_text(self, tab_id: int) -> Any:
|
|
170
|
+
return self.request("readClipboardText", {"tabId": tab_id})
|
|
171
|
+
|
|
172
|
+
def write_clipboard_text(self, tab_id: int, text: str) -> Any:
|
|
173
|
+
return self.request("writeClipboardText", {"tabId": tab_id, "text": text})
|
|
174
|
+
|
|
175
|
+
def read_clipboard(self, tab_id: int) -> Any:
|
|
176
|
+
return self.request("readClipboard", {"tabId": tab_id})
|
|
177
|
+
|
|
178
|
+
def write_clipboard(self, tab_id: int, items: list[JsonObject]) -> Any:
|
|
179
|
+
return self.request("writeClipboard", {"tabId": tab_id, "items": items})
|
|
180
|
+
|
|
181
|
+
def turn_ended(self) -> Any:
|
|
182
|
+
return self.request("turnEnded")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def connect_open_browser_use(
|
|
186
|
+
socket_path: str,
|
|
187
|
+
session_id: str = "open-browser-use-python",
|
|
188
|
+
turn_id: str | None = None,
|
|
189
|
+
timeout: float = 10.0,
|
|
190
|
+
) -> "OpenBrowserUseBrowser":
|
|
191
|
+
browser = OpenBrowserUseBrowser(
|
|
192
|
+
OpenBrowserUseClient(
|
|
193
|
+
socket_path=socket_path,
|
|
194
|
+
session_id=session_id,
|
|
195
|
+
turn_id=turn_id,
|
|
196
|
+
timeout=timeout,
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
browser.connect()
|
|
200
|
+
return browser
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class OpenBrowserUseBrowser:
|
|
204
|
+
def __init__(self, client: OpenBrowserUseClient) -> None:
|
|
205
|
+
self.client = client
|
|
206
|
+
self.cdp = OpenBrowserUseCdp(client)
|
|
207
|
+
|
|
208
|
+
def connect(self) -> "OpenBrowserUseBrowser":
|
|
209
|
+
self.client.connect()
|
|
210
|
+
return self
|
|
211
|
+
|
|
212
|
+
def close(self) -> None:
|
|
213
|
+
self.client.close()
|
|
214
|
+
|
|
215
|
+
def new_tab(
|
|
216
|
+
self,
|
|
217
|
+
url: str | None = None,
|
|
218
|
+
wait_until: LoadState = "load",
|
|
219
|
+
timeout: float = DEFAULT_NAVIGATION_TIMEOUT,
|
|
220
|
+
) -> "OpenBrowserUseTab":
|
|
221
|
+
result = self.client.create_tab()
|
|
222
|
+
tab = self.tab(_tab_id_from_value(result, "create_tab response"))
|
|
223
|
+
if url:
|
|
224
|
+
tab.goto(url, wait_until=wait_until, timeout=timeout)
|
|
225
|
+
return tab
|
|
226
|
+
|
|
227
|
+
def tab(self, tab_id: int) -> "OpenBrowserUseTab":
|
|
228
|
+
return OpenBrowserUseTab(self, tab_id)
|
|
229
|
+
|
|
230
|
+
def get_tabs(self) -> Any:
|
|
231
|
+
return self.client.get_tabs()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class OpenBrowserUseTab:
|
|
235
|
+
def __init__(self, browser: OpenBrowserUseBrowser, tab_id: int) -> None:
|
|
236
|
+
self.browser = browser
|
|
237
|
+
self.id = tab_id
|
|
238
|
+
self.playwright = OpenBrowserUseTabPlaywright(self)
|
|
239
|
+
|
|
240
|
+
def goto(
|
|
241
|
+
self,
|
|
242
|
+
url: str,
|
|
243
|
+
wait_until: LoadState = "load",
|
|
244
|
+
timeout: float = DEFAULT_NAVIGATION_TIMEOUT,
|
|
245
|
+
) -> Any:
|
|
246
|
+
return self.browser.cdp.navigate(self.id, url, wait_until=wait_until, timeout=timeout)
|
|
247
|
+
|
|
248
|
+
def wait_for_load_state(
|
|
249
|
+
self,
|
|
250
|
+
state: LoadState = "load",
|
|
251
|
+
timeout: float = DEFAULT_NAVIGATION_TIMEOUT,
|
|
252
|
+
) -> None:
|
|
253
|
+
self.browser.cdp.wait_for_load_state(self.id, state=state, timeout=timeout)
|
|
254
|
+
|
|
255
|
+
def dom_snapshot(self) -> str:
|
|
256
|
+
value = self.browser.cdp.evaluate(self.id, "document.body?.innerText ?? ''")
|
|
257
|
+
return "" if value is None else str(value)
|
|
258
|
+
|
|
259
|
+
def evaluate(self, expression: str, await_promise: bool | None = None) -> Any:
|
|
260
|
+
return self.browser.cdp.evaluate(self.id, expression, await_promise=await_promise)
|
|
261
|
+
|
|
262
|
+
def title(self) -> str:
|
|
263
|
+
value = self.evaluate("document.title ?? ''")
|
|
264
|
+
return "" if value is None else str(value)
|
|
265
|
+
|
|
266
|
+
def url(self) -> str:
|
|
267
|
+
value = self.evaluate("location.href")
|
|
268
|
+
return "" if value is None else str(value)
|
|
269
|
+
|
|
270
|
+
def wait_for_timeout(self, timeout_ms: float) -> None:
|
|
271
|
+
if timeout_ms < 0:
|
|
272
|
+
raise ValueError("timeout_ms must be non-negative")
|
|
273
|
+
time.sleep(timeout_ms / 1000)
|
|
274
|
+
|
|
275
|
+
def locator(self, selector: str) -> "OpenBrowserUseLocator":
|
|
276
|
+
return OpenBrowserUseLocator(self, selector)
|
|
277
|
+
|
|
278
|
+
def close(self) -> Any:
|
|
279
|
+
return self.browser.cdp.call(self.id, "Page.close")
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class OpenBrowserUseTabPlaywright:
|
|
283
|
+
def __init__(self, tab: OpenBrowserUseTab) -> None:
|
|
284
|
+
self.tab = tab
|
|
285
|
+
|
|
286
|
+
def wait_for_load_state(
|
|
287
|
+
self,
|
|
288
|
+
state: LoadState = "load",
|
|
289
|
+
timeout: float = DEFAULT_NAVIGATION_TIMEOUT,
|
|
290
|
+
) -> None:
|
|
291
|
+
self.tab.wait_for_load_state(state=state, timeout=timeout)
|
|
292
|
+
|
|
293
|
+
def dom_snapshot(self) -> str:
|
|
294
|
+
return self.tab.dom_snapshot()
|
|
295
|
+
|
|
296
|
+
def title(self) -> str:
|
|
297
|
+
return self.tab.title()
|
|
298
|
+
|
|
299
|
+
def url(self) -> str:
|
|
300
|
+
return self.tab.url()
|
|
301
|
+
|
|
302
|
+
def wait_for_timeout(self, timeout_ms: float) -> None:
|
|
303
|
+
self.tab.wait_for_timeout(timeout_ms)
|
|
304
|
+
|
|
305
|
+
def locator(self, selector: str) -> "OpenBrowserUseLocator":
|
|
306
|
+
return self.tab.locator(selector)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class OpenBrowserUseLocator:
|
|
310
|
+
def __init__(self, tab: OpenBrowserUseTab, selector: str) -> None:
|
|
311
|
+
if not selector:
|
|
312
|
+
raise ValueError("locator requires a selector")
|
|
313
|
+
self.tab = tab
|
|
314
|
+
self.selector = selector
|
|
315
|
+
|
|
316
|
+
def inner_text(self, timeout_ms: int | None = None) -> str:
|
|
317
|
+
value = self.tab.evaluate(
|
|
318
|
+
_locator_inner_text_expression(self.selector, timeout_ms),
|
|
319
|
+
await_promise=True,
|
|
320
|
+
)
|
|
321
|
+
return "" if value is None else str(value)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class OpenBrowserUseCdp:
|
|
325
|
+
def __init__(self, client: OpenBrowserUseClient) -> None:
|
|
326
|
+
self.client = client
|
|
327
|
+
self._attached_tab_ids: set[int] = set()
|
|
328
|
+
|
|
329
|
+
def call(
|
|
330
|
+
self,
|
|
331
|
+
tab_id: int,
|
|
332
|
+
method: str,
|
|
333
|
+
command_params: JsonObject | None = None,
|
|
334
|
+
timeout_ms: int | None = None,
|
|
335
|
+
) -> Any:
|
|
336
|
+
self.ensure_attached(tab_id)
|
|
337
|
+
params: JsonObject = {
|
|
338
|
+
"target": {"tabId": tab_id},
|
|
339
|
+
"method": method,
|
|
340
|
+
"commandParams": command_params or {},
|
|
341
|
+
}
|
|
342
|
+
if timeout_ms is not None:
|
|
343
|
+
params["timeoutMs"] = timeout_ms
|
|
344
|
+
return self.client.request("executeCdp", params)
|
|
345
|
+
|
|
346
|
+
def evaluate(self, tab_id: int, expression: str, await_promise: bool | None = None) -> Any:
|
|
347
|
+
command_params: JsonObject = {
|
|
348
|
+
"expression": expression,
|
|
349
|
+
"returnByValue": True,
|
|
350
|
+
}
|
|
351
|
+
if await_promise is not None:
|
|
352
|
+
command_params["awaitPromise"] = await_promise
|
|
353
|
+
result = self.call(tab_id, "Runtime.evaluate", command_params)
|
|
354
|
+
if isinstance(result, dict) and isinstance(result.get("exceptionDetails"), dict):
|
|
355
|
+
raise RuntimeError(str(result["exceptionDetails"].get("text", "Open Browser Use evaluation failed")))
|
|
356
|
+
if isinstance(result, dict) and isinstance(result.get("result"), dict):
|
|
357
|
+
return result["result"].get("value")
|
|
358
|
+
return None
|
|
359
|
+
|
|
360
|
+
def navigate(
|
|
361
|
+
self,
|
|
362
|
+
tab_id: int,
|
|
363
|
+
url: str,
|
|
364
|
+
wait_until: LoadState = "load",
|
|
365
|
+
timeout: float = DEFAULT_NAVIGATION_TIMEOUT,
|
|
366
|
+
) -> Any:
|
|
367
|
+
if not url:
|
|
368
|
+
raise ValueError("goto requires a URL")
|
|
369
|
+
_assert_supported_load_state(wait_until)
|
|
370
|
+
self.call(tab_id, "Page.enable")
|
|
371
|
+
result = self.call(tab_id, "Page.navigate", {"url": url}, timeout_ms=int(timeout * 1000))
|
|
372
|
+
if isinstance(result, dict) and result.get("errorText"):
|
|
373
|
+
raise RuntimeError(f"Browser failed to navigate tab {tab_id}: {result['errorText']}")
|
|
374
|
+
self.wait_for_load_state(tab_id, state=wait_until, timeout=timeout)
|
|
375
|
+
return result
|
|
376
|
+
|
|
377
|
+
def wait_for_load_state(
|
|
378
|
+
self,
|
|
379
|
+
tab_id: int,
|
|
380
|
+
state: LoadState = "load",
|
|
381
|
+
timeout: float = DEFAULT_NAVIGATION_TIMEOUT,
|
|
382
|
+
) -> None:
|
|
383
|
+
_assert_supported_load_state(state)
|
|
384
|
+
self.call(tab_id, "Page.enable")
|
|
385
|
+
deadline = time.monotonic() + timeout
|
|
386
|
+
while True:
|
|
387
|
+
if _document_state_matches(self.read_document_state(tab_id), state):
|
|
388
|
+
return
|
|
389
|
+
if time.monotonic() >= deadline:
|
|
390
|
+
raise TimeoutError(f"Timed out waiting for {state} in tab {tab_id}")
|
|
391
|
+
time.sleep(0.1)
|
|
392
|
+
|
|
393
|
+
def read_document_state(self, tab_id: int) -> JsonObject | None:
|
|
394
|
+
try:
|
|
395
|
+
value = self.evaluate(tab_id, "({ href: window.location.href, readyState: document.readyState })")
|
|
396
|
+
except Exception:
|
|
397
|
+
return None
|
|
398
|
+
return value if isinstance(value, dict) else None
|
|
399
|
+
|
|
400
|
+
def ensure_attached(self, tab_id: int) -> None:
|
|
401
|
+
if tab_id in self._attached_tab_ids:
|
|
402
|
+
return
|
|
403
|
+
self.client.attach(tab_id)
|
|
404
|
+
self._attached_tab_ids.add(tab_id)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def encode_frame(value: JsonObject) -> bytes:
|
|
408
|
+
payload = json.dumps(value, separators=(",", ":")).encode("utf-8")
|
|
409
|
+
return struct.pack(_native_u32(), len(payload)) + payload
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def read_frame(sock: socket.socket) -> JsonObject:
|
|
413
|
+
header = _read_exact(sock, 4)
|
|
414
|
+
(length,) = struct.unpack(_native_u32(), header)
|
|
415
|
+
payload = _read_exact(sock, length)
|
|
416
|
+
value = json.loads(payload.decode("utf-8"))
|
|
417
|
+
if not isinstance(value, dict):
|
|
418
|
+
raise RuntimeError("Open Browser Use response must be a JSON object")
|
|
419
|
+
return value
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _read_exact(sock: socket.socket, length: int) -> bytes:
|
|
423
|
+
chunks: list[bytes] = []
|
|
424
|
+
remaining = length
|
|
425
|
+
while remaining > 0:
|
|
426
|
+
chunk = sock.recv(remaining)
|
|
427
|
+
if not chunk:
|
|
428
|
+
raise EOFError("Open Browser Use socket closed")
|
|
429
|
+
chunks.append(chunk)
|
|
430
|
+
remaining -= len(chunk)
|
|
431
|
+
return b"".join(chunks)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _native_u32() -> str:
|
|
435
|
+
return "=I"
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _tab_id_from_value(value: Any, label: str) -> int:
|
|
439
|
+
if not isinstance(value, dict):
|
|
440
|
+
raise RuntimeError(f"{label} did not include a tab object")
|
|
441
|
+
tab_id = value.get("id")
|
|
442
|
+
if isinstance(tab_id, int) and tab_id > 0:
|
|
443
|
+
return tab_id
|
|
444
|
+
if isinstance(tab_id, str) and tab_id.isdigit() and int(tab_id) > 0:
|
|
445
|
+
return int(tab_id)
|
|
446
|
+
raise RuntimeError(f"{label} did not include a numeric tab id")
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _assert_supported_load_state(state: str) -> None:
|
|
450
|
+
if state not in ("domcontentloaded", "load"):
|
|
451
|
+
raise ValueError(f'Unsupported load state "{state}". Use "domcontentloaded" or "load".')
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _document_state_matches(document_state: JsonObject | None, state: LoadState) -> bool:
|
|
455
|
+
ready_state = document_state.get("readyState") if isinstance(document_state, dict) else None
|
|
456
|
+
return ready_state == "complete" or (state == "domcontentloaded" and ready_state == "interactive")
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _locator_inner_text_expression(selector: str, timeout_ms: int | None) -> str:
|
|
460
|
+
timeout = DEFAULT_NAVIGATION_TIMEOUT * 1000 if timeout_ms is None else timeout_ms
|
|
461
|
+
if timeout < 0:
|
|
462
|
+
raise ValueError("timeout_ms must be non-negative")
|
|
463
|
+
selector_json = json.dumps(selector)
|
|
464
|
+
return f"""(async () => {{
|
|
465
|
+
const selector = {selector_json};
|
|
466
|
+
const deadline = performance.now() + {timeout};
|
|
467
|
+
while (true) {{
|
|
468
|
+
const element = document.querySelector(selector);
|
|
469
|
+
if (element) {{
|
|
470
|
+
return element.innerText ?? element.textContent ?? "";
|
|
471
|
+
}}
|
|
472
|
+
if (performance.now() >= deadline) {{
|
|
473
|
+
throw new Error(`Timed out waiting for locator ${{selector}}`);
|
|
474
|
+
}}
|
|
475
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
476
|
+
}}
|
|
477
|
+
}})()"""
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: open-browser-use-sdk
|
|
3
|
+
Version: 0.1.23
|
|
4
|
+
Summary: Python SDK for Open Browser Use.
|
|
5
|
+
Author: iFurySt
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/iFurySt/open-codex-browser-use
|
|
8
|
+
Project-URL: Repository, https://github.com/iFurySt/open-codex-browser-use
|
|
9
|
+
Project-URL: Issues, https://github.com/iFurySt/open-codex-browser-use/issues
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# Open Browser Use Python SDK
|
|
14
|
+
|
|
15
|
+
Python client for controlling a real Chrome profile through Open Browser Use.
|
|
16
|
+
The package distribution is `open-browser-use-sdk`; the import module remains
|
|
17
|
+
`open_browser_use`.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
pip install open-browser-use-sdk
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The SDK expects the `open-browser-use` CLI and Chrome extension to already be
|
|
26
|
+
installed and connected:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
open-browser-use ping
|
|
30
|
+
open-browser-use info
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
import json
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
from open_browser_use import connect_open_browser_use
|
|
40
|
+
|
|
41
|
+
registry = json.loads(Path("/tmp/open-browser-use/active.json").read_text())
|
|
42
|
+
|
|
43
|
+
browser = connect_open_browser_use(
|
|
44
|
+
socket_path=registry["socketPath"],
|
|
45
|
+
session_id="python-sdk-example",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
browser.client.name_session("Python SDK example - OBU")
|
|
50
|
+
tab = browser.new_tab()
|
|
51
|
+
tab.goto("https://example.com", wait_until="domcontentloaded")
|
|
52
|
+
print(tab.title())
|
|
53
|
+
finally:
|
|
54
|
+
browser.client.finalize_tabs([])
|
|
55
|
+
browser.close()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Use `OpenBrowserUseClient` directly when you need raw Browser Use JSON-RPC or
|
|
59
|
+
CDP methods.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
open_browser_use/__init__.py
|
|
4
|
+
open_browser_use/client.py
|
|
5
|
+
open_browser_use_sdk.egg-info/PKG-INFO
|
|
6
|
+
open_browser_use_sdk.egg-info/SOURCES.txt
|
|
7
|
+
open_browser_use_sdk.egg-info/dependency_links.txt
|
|
8
|
+
open_browser_use_sdk.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
open_browser_use
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "open-browser-use-sdk"
|
|
3
|
+
version = "0.1.23"
|
|
4
|
+
description = "Python SDK for Open Browser Use."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "iFurySt" }
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[project.urls]
|
|
13
|
+
Homepage = "https://github.com/iFurySt/open-codex-browser-use"
|
|
14
|
+
Repository = "https://github.com/iFurySt/open-codex-browser-use"
|
|
15
|
+
Issues = "https://github.com/iFurySt/open-codex-browser-use/issues"
|
|
16
|
+
|
|
17
|
+
[tool.setuptools.packages.find]
|
|
18
|
+
where = ["."]
|
|
19
|
+
include = ["open_browser_use*"]
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["setuptools>=77", "wheel"]
|
|
23
|
+
build-backend = "setuptools.build_meta"
|