Files
browser-cli/browser_cli/models.py
T
daniel156161 6fa931aa36
Testing / remote-protocol-compat (0.9.5) (push) Successful in 56s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 59s
Testing / test (push) Successful in 1m1s
Build & Publish Package / publish (push) Successful in 33s
Package Extension / package-extension (push) Successful in 36s
feat: harden remote serve and reuse connections
- Gate TCP serve commands with safe-by-default policies, per-key allow tokens, per-key rate limiting, and audit labels.
- Reuse authenticated encrypted remote sessions and parallelize/caches multi-browser fanout to reduce repeated handshake roundtrips.
- Increase paged native-host batch size with extension-side byte budgeting to speed large tab listings safely.
- Point install output at public Chrome Web Store / Firefox AMO listings by default, with --dev preserving unpacked workflows.
- Share search-engine metadata between CLI and SDK and bump the package/extension version to 0.16.0.
- Cover the new security, pooling, paging, install, and fanout behavior with expanded Python and extension tests.
2026-06-18 14:24:15 +02:00

186 lines
5.7 KiB
Python

"""
Typed dataclasses returned by the BrowserCLI Python API.
Each object is bound to a BrowserCLI instance so you can call actions
directly on it:
tabs = b.tabs.list()
tabs[0].close()
tabs[0].move(forward=True)
groups = b.groups.list()
groups[0].tabs()
groups[0].add_tab("https://example.com")
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Protocol
class BoundBrowser(Protocol):
tabs: Any
groups: Any
nav: Any
def dispatch(self, command: str, args: dict | None = None): ...
# ── BrowserCounts ───────────────────────────────────────────────────────────
@dataclass(frozen=True)
class BrowserCounts:
"""Aggregated per-browser counts returned in implicit multi-browser mode."""
total: int
by_browser: dict[str, int]
browser_groups: dict[str, str] = field(default_factory=dict)
# ── Tab ───────────────────────────────────────────────────────────────────────
@dataclass
class Tab:
"""A browser tab."""
id: int
window_id: int
active: bool
muted: bool = False
title: str = ""
url: str = ""
group_id: int | None = None
index: int = 0
browser: str | None = None
browser_name: str | None = None
browser_group: str | None = None
_browser: BoundBrowser | None = field(default=None, repr=False, compare=False, init=False)
def _b(self) -> BoundBrowser:
if self._browser is None:
raise RuntimeError("Tab is not bound to a BrowserCLI instance")
return self._browser
def _command(self, name: str, args: dict | None = None):
browser = self._b()
return browser.dispatch(name, args)
def close(self) -> None:
"""Close this tab."""
self._b().tabs.close(self.id)
def activate(self) -> None:
"""Switch browser focus to this tab."""
self._b().tabs.activate(self.id)
def mute(self) -> None:
"""Mute this tab."""
self._b().tabs.mute(self.id)
def unmute(self) -> None:
"""Unmute this tab."""
self._b().tabs.unmute(self.id)
def reload(self) -> None:
"""Reload this tab."""
self._b().nav.reload(self.id)
def hard_reload(self) -> None:
"""Hard-reload this tab (bypass cache)."""
self._b().nav.hard_reload(self.id)
def move(
self, *,
forward: bool = False,
backward: bool = False,
group_id: int | None = None,
window_id: int | None = None,
index: int | None = None,
) -> None:
"""Move this tab.
Args:
forward: Move one position to the right within the window.
backward: Move one position to the left within the window.
group_id: Move into the tab group with this ID.
window_id: Move to the window with this ID.
index: Absolute position index in the target window.
"""
self._b().tabs.move(
self.id,
forward=forward,
backward=backward,
group_id=group_id,
window_id=window_id,
index=index,
)
def html(self) -> str:
"""Return the full HTML source of this tab."""
return self._b().tabs.html(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().tabs.pin(self.id)
def unpin(self) -> None:
"""Unpin this tab."""
self._b().tabs.unpin(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().tabs.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().nav.to(self.id, url)
# ── Group ─────────────────────────────────────────────────────────────────────
@dataclass
class Group:
"""A browser tab group."""
id: int
title: str
color: str
collapsed: bool
tab_count: int
window_id: int | None = None
browser: str | None = None
browser_name: str | None = None
browser_group: str | None = None
_browser: BoundBrowser | None = field(default=None, repr=False, compare=False, init=False)
def _b(self) -> BoundBrowser:
if self._browser is None:
raise RuntimeError("Group is not bound to a BrowserCLI instance")
return self._browser
def _command(self, name: str, args: dict | None = None):
browser = self._b()
return browser.dispatch(name, args)
def close(self) -> None:
"""Ungroup (and close) this tab group."""
self._b().groups.close(self.id)
def tabs(self) -> list[Tab]:
"""Return all tabs inside this group."""
return self._b().groups.tabs(self.id)
def move(self, *, forward: bool = False, backward: bool = False) -> None:
"""Move this group forward or backward among groups."""
self._b().groups.move(str(self.id), forward=forward, backward=backward)
def add_tab(self, url: str | None = None) -> int | None:
"""Open a new tab inside this group. Returns the new tab ID."""
return self._b().groups.add_tab(self.id, url)