feat(sdk): improve Python SDK ergonomics
- Position browser-cli as a CLI plus Python SDK in docs and package metadata. - Add public target properties and a raw command escape hatch for unsupported commands. - Add convenience helpers for opening, finding, closing, and accessing tabs. - Add plural group aliases and a wait_for_selector DOM convenience alias. - Extend bound Tab objects with screenshot, pin, refresh, load wait, and URL watch helpers. - Preserve remote auth key configuration when binding remote Tab and Group objects. - Bump project and extension versions to 0.9.9 and cover SDK additions with tests.
This commit is contained in:
+109
-1
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
browser_cli — Python API for controlling your running browser.
|
||||
browser_cli — Python SDK for controlling your running browser.
|
||||
|
||||
Usage:
|
||||
from browser_cli import BrowserCLI
|
||||
@@ -50,9 +50,32 @@ class BrowserCLI:
|
||||
self._remote = remote
|
||||
self._key = key if key else None
|
||||
|
||||
@property
|
||||
def browser(self) -> str | None:
|
||||
"""Target browser/profile alias, equivalent to ``--browser``."""
|
||||
return self._browser
|
||||
|
||||
@property
|
||||
def remote(self) -> str | None:
|
||||
"""Remote endpoint used by this client, if any."""
|
||||
return self._remote
|
||||
|
||||
@property
|
||||
def key(self) -> str | None:
|
||||
"""Ed25519 key spec used for remote auth, if explicitly configured."""
|
||||
return self._key
|
||||
|
||||
def _cmd(self, command: str, args: dict | None = None):
|
||||
return send_command(command, args, profile=self._browser, remote=self._remote, key=self._key)
|
||||
|
||||
def command(self, command: str, args: dict | None = None):
|
||||
"""Send a raw browser-cli command and return its response.
|
||||
|
||||
This is the SDK escape hatch for commands that do not have a dedicated
|
||||
convenience method yet.
|
||||
"""
|
||||
return self._cmd(command, args or {})
|
||||
|
||||
def _multi_browser_targets(self):
|
||||
if self._browser is not None:
|
||||
return []
|
||||
@@ -109,6 +132,7 @@ class BrowserCLI:
|
||||
tab._browser = self if browser_profile is None else BrowserCLI(
|
||||
browser=browser_profile,
|
||||
remote=browser_remote,
|
||||
key=self._key,
|
||||
)
|
||||
return tab
|
||||
|
||||
@@ -131,6 +155,7 @@ class BrowserCLI:
|
||||
group._browser = self if browser_profile is None else BrowserCLI(
|
||||
browser=browser_profile,
|
||||
remote=browser_remote,
|
||||
key=self._key,
|
||||
)
|
||||
return group
|
||||
|
||||
@@ -139,6 +164,27 @@ class BrowserCLI:
|
||||
def open(self, url: str, *, background: bool = False, window: str | None = None, group: str | None = None) -> None:
|
||||
self._cmd("navigate.open", {"url": url, "background": background, "window": window, "group": group})
|
||||
|
||||
def open_tab(
|
||||
self,
|
||||
url: str,
|
||||
*,
|
||||
wait: bool = False,
|
||||
timeout: float = 30.0,
|
||||
background: bool = False,
|
||||
window: str | None = None,
|
||||
group: str | None = None,
|
||||
) -> "Tab":
|
||||
"""Open URL in a new tab and return a bound :class:`Tab` object.
|
||||
|
||||
Set ``wait=True`` to block until the page reaches ``readyState=complete``.
|
||||
"""
|
||||
if wait:
|
||||
return self.open_wait(url, timeout=timeout, background=background, window=window, group=group)
|
||||
data = self._cmd("navigate.open", {"url": url, "background": background, "window": window, "group": group})
|
||||
if not isinstance(data, dict) or "id" not in data:
|
||||
raise RuntimeError("navigate.open returned unexpected data")
|
||||
return self._make_tab(data)
|
||||
|
||||
def reload(self, tab_id: int | None = None) -> None:
|
||||
self._cmd("navigate.reload", {"tabId": tab_id})
|
||||
|
||||
@@ -216,6 +262,32 @@ class BrowserCLI:
|
||||
|
||||
# ── Tabs ──────────────────────────────────────────────────────────────
|
||||
|
||||
def tabs(self) -> list[Tab]:
|
||||
"""Alias for :meth:`tabs_list`."""
|
||||
return self.tabs_list()
|
||||
|
||||
def tab(self, tab_id: int) -> Tab:
|
||||
"""Return a specific tab by ID."""
|
||||
return self.tabs_status(tab_id)
|
||||
|
||||
def active_tab(self) -> Tab:
|
||||
"""Return the active tab."""
|
||||
return self.tabs_status()
|
||||
|
||||
def find_tabs(self, search: str) -> list[Tab]:
|
||||
"""Alias for :meth:`tabs_query`."""
|
||||
return self.tabs_query(search)
|
||||
|
||||
def find_tab(self, search: str) -> Tab | None:
|
||||
"""Return the first tab matching *search*, or ``None``."""
|
||||
matches = self.tabs_query(search)
|
||||
return matches[0] if matches else None
|
||||
|
||||
def close_tab(self, tab: int | Tab) -> int:
|
||||
"""Close a tab by ID or :class:`Tab` object. Returns count closed."""
|
||||
tab_id = tab.id if isinstance(tab, Tab) else tab
|
||||
return self.tabs_close(tab_id)
|
||||
|
||||
def tabs_list(self) -> list[Tab]:
|
||||
"""Return all open tabs across all windows.
|
||||
|
||||
@@ -370,6 +442,14 @@ class BrowserCLI:
|
||||
|
||||
# ── Tab Groups ────────────────────────────────────────────────────────
|
||||
|
||||
def groups(self) -> list[Group]:
|
||||
"""Alias for :meth:`group_list`."""
|
||||
return self.group_list()
|
||||
|
||||
def groups_list(self) -> list[Group]:
|
||||
"""Alias for :meth:`group_list`."""
|
||||
return self.group_list()
|
||||
|
||||
def group_list(self) -> list[Group]:
|
||||
"""Return all tab groups.
|
||||
|
||||
@@ -394,6 +474,10 @@ class BrowserCLI:
|
||||
"""Return all tabs inside a group."""
|
||||
return [self._make_tab(t) for t in (self._cmd("group.tabs", {"groupId": group_id}) or [])]
|
||||
|
||||
def groups_count(self) -> int | BrowserCounts:
|
||||
"""Alias for :meth:`group_count`."""
|
||||
return self.group_count()
|
||||
|
||||
def group_count(self) -> int | BrowserCounts:
|
||||
"""Return the number of tab groups.
|
||||
|
||||
@@ -405,6 +489,10 @@ class BrowserCLI:
|
||||
return BrowserCounts(total=sum(by_browser.values()), by_browser=by_browser)
|
||||
return self._cmd("group.count", {})
|
||||
|
||||
def groups_query(self, search: str) -> list[Group]:
|
||||
"""Alias for :meth:`group_query`."""
|
||||
return self.group_query(search)
|
||||
|
||||
def group_query(self, search: str) -> list[Group]:
|
||||
"""Search groups by name."""
|
||||
return [self._make_group(g) for g in (self._cmd("group.query", {"search": search}) or [])]
|
||||
@@ -413,6 +501,14 @@ class BrowserCLI:
|
||||
"""Ungroup (and close) a tab group by ID."""
|
||||
self._cmd("group.close", {"groupId": group_id})
|
||||
|
||||
def groups_create(self, name: str) -> Group:
|
||||
"""Alias for :meth:`group_create`."""
|
||||
return self.group_create(name)
|
||||
|
||||
def group_open(self, name: str) -> Group:
|
||||
"""Alias for :meth:`group_create`."""
|
||||
return self.group_create(name)
|
||||
|
||||
def group_create(self, name: str) -> Group:
|
||||
"""Create a new tab group with *name*. Returns the created Group."""
|
||||
data = self._cmd("group.open", {"name": name})
|
||||
@@ -536,6 +632,18 @@ class BrowserCLI:
|
||||
"tabId": tab_id,
|
||||
})
|
||||
|
||||
def wait_for_selector(
|
||||
self,
|
||||
selector: str,
|
||||
*,
|
||||
timeout: float = 10.0,
|
||||
visible: bool = False,
|
||||
hidden: bool = False,
|
||||
tab_id: int | None = None,
|
||||
) -> dict:
|
||||
"""Alias for :meth:`dom_wait_for`."""
|
||||
return self.dom_wait_for(selector, timeout=timeout, visible=visible, hidden=hidden, tab_id=tab_id)
|
||||
|
||||
def dom_wait_for(
|
||||
self,
|
||||
selector: str,
|
||||
|
||||
@@ -95,6 +95,30 @@ class Tab:
|
||||
"""Return the full HTML source of this tab."""
|
||||
return self._b()._cmd("tabs.html", {"tabId": self.id})
|
||||
|
||||
def screenshot(self, *, format: str = "png", quality: int | None = None) -> str:
|
||||
"""Capture this tab's visible area. Returns a base64 data URL."""
|
||||
return self._b().tabs_screenshot(self.id, format=format, quality=quality)
|
||||
|
||||
def pin(self) -> None:
|
||||
"""Pin this tab."""
|
||||
self._b()._cmd("tabs.pin", {"tabId": self.id})
|
||||
|
||||
def unpin(self) -> None:
|
||||
"""Unpin this tab."""
|
||||
self._b()._cmd("tabs.unpin", {"tabId": self.id})
|
||||
|
||||
def refresh(self) -> Tab:
|
||||
"""Return a fresh snapshot of this tab."""
|
||||
return self._b().tabs_status(self.id)
|
||||
|
||||
def wait_for_load(self, *, timeout: float = 30.0, ready_state: str = "complete") -> Tab:
|
||||
"""Wait until this tab reaches the requested readyState."""
|
||||
return self._b().wait_for_load(self.id, timeout=timeout, ready_state=ready_state)
|
||||
|
||||
def watch_url(self, pattern: str, *, timeout: float = 30.0) -> Tab:
|
||||
"""Wait until this tab's URL matches regex *pattern*."""
|
||||
return self._b().tabs_watch_url(pattern, tab_id=self.id, timeout=timeout)
|
||||
|
||||
def open(self, url: str, *, background: bool = False) -> None:
|
||||
"""Navigate this tab to *url* in place."""
|
||||
self._b().navigate_tab(self.id, url)
|
||||
|
||||
Reference in New Issue
Block a user