refactor: reorganize client transport and extension internals
- Split client, native, remote, serve, markdown, and SDK internals into focused packages with direct imports. - Move local and remote transport framing/protocol helpers behind clearer module boundaries. - Break up the extension injected DOM logic into a separate content dispatch bundle and dedicated content modules. - Add explicit client handling for passive remote discovery without noisy PQ warnings. - Keep behavior covered with updated unit, integration, and extension tests.
This commit is contained in:
+137
-97
@@ -2,126 +2,166 @@
|
||||
browser_cli — Python SDK for controlling your running browser.
|
||||
|
||||
Usage:
|
||||
from browser_cli import BrowserCLI
|
||||
b = BrowserCLI()
|
||||
from browser_cli import BrowserCLI
|
||||
b = BrowserCLI()
|
||||
|
||||
tabs = b.tabs.list() # list[Tab]
|
||||
tabs[0].close()
|
||||
tabs[0].move(forward=True)
|
||||
tabs = b.tabs.list() # list[Tab]
|
||||
tabs[0].close()
|
||||
tabs[0].move(forward=True)
|
||||
|
||||
groups = b.groups.list() # list[Group]
|
||||
groups[0].tabs()
|
||||
groups[0].add_tab("https://example.com")
|
||||
groups = b.groups.list() # list[Group]
|
||||
groups[0].tabs()
|
||||
groups[0].add_tab("https://example.com")
|
||||
|
||||
b.nav.open("https://example.com")
|
||||
b.dom.click("#submit")
|
||||
b.session.save("work")
|
||||
b.nav.open("https://example.com")
|
||||
b.dom.click("#submit")
|
||||
b.session.save("work")
|
||||
|
||||
# When multiple browser instances are active, pass the alias:
|
||||
b = BrowserCLI(browser="brave")
|
||||
# When multiple browser instances are active, pass the alias:
|
||||
b = BrowserCLI(browser="brave")
|
||||
|
||||
Commands are grouped into namespaces on the client:
|
||||
b.nav navigation (open, reload, back, forward, focus, search)
|
||||
b.tabs tabs (list, open, close, move, status, mute, sort, ...)
|
||||
b.groups tab groups (list, create, add_tab, move, close)
|
||||
b.windows browser windows (list, open, close, rename)
|
||||
b.dom page elements (query, click, type, wait_for, eval, ...)
|
||||
b.extract content extraction (links, images, text, json, markdown)
|
||||
b.page page info
|
||||
b.storage localStorage / sessionStorage
|
||||
b.cookies cookies (list, get, set)
|
||||
b.session sessions (save, load, list, diff, ...)
|
||||
b.perf performance profile + background jobs
|
||||
b.extension control the extension itself
|
||||
b.nav navigation (open, reload, back, forward, focus, search)
|
||||
b.tabs tabs (list, open, close, move, status, mute, sort, ...)
|
||||
b.groups tab groups (list, create, add_tab, move, close)
|
||||
b.windows browser windows (list, open, close, rename)
|
||||
b.dom page elements (query, click, type, wait_for, eval, ...)
|
||||
b.extract content extraction (links, images, text, json, markdown)
|
||||
b.page page info
|
||||
b.storage localStorage / sessionStorage
|
||||
b.cookies cookies (list, get, set)
|
||||
b.session sessions (save, load, list, diff, ...)
|
||||
b.perf performance profile + background jobs
|
||||
b.extension control the extension itself
|
||||
b.decorators workflow decorators for scripts
|
||||
"""
|
||||
from browser_cli.client import BrowserNotConnected, active_browser_targets, remote_browser_targets, send_command
|
||||
from collections.abc import Callable
|
||||
|
||||
from browser_cli.client import active_browser_targets, remote_browser_targets, send_command, send_command_async
|
||||
from browser_cli.errors import BrowserNotConnected
|
||||
from browser_cli.models import BrowserCounts, Group, Tab
|
||||
from browser_cli.sdk import (
|
||||
CookiesNS,
|
||||
DomNS,
|
||||
ExtensionNS,
|
||||
ExtractNS,
|
||||
GroupsNS,
|
||||
NavigationNS,
|
||||
PageNS,
|
||||
PerfNS,
|
||||
SessionNS,
|
||||
StorageNS,
|
||||
TabsNS,
|
||||
WindowsNS,
|
||||
CookiesNS,
|
||||
DecoratorsNS,
|
||||
DomNS,
|
||||
ExtensionNS,
|
||||
ExtractNS,
|
||||
GroupsNS,
|
||||
NAMESPACE_SPECS,
|
||||
NavigationNS,
|
||||
PageNS,
|
||||
PerfNS,
|
||||
SessionNS,
|
||||
StorageNS,
|
||||
TabsNS,
|
||||
WindowsNS,
|
||||
)
|
||||
from browser_cli.sdk.factories import FactoryMixin
|
||||
from browser_cli.sdk.routing import RoutingMixin
|
||||
|
||||
__all__ = ["BrowserCLI", "BrowserCounts", "BrowserNotConnected", "Tab", "Group"]
|
||||
from browser_cli.async_sdk import AsyncBrowserCLI
|
||||
|
||||
__all__ = ["BrowserCLI", "AsyncBrowserCLI", "BrowserCounts", "BrowserNotConnected", "Tab", "Group"]
|
||||
|
||||
class BrowserCLI(FactoryMixin, RoutingMixin):
|
||||
"""Client for a running browser, with commands grouped into namespaces.
|
||||
"""Client for a running browser, with commands grouped into namespaces.
|
||||
|
||||
The client itself holds the connection target (browser/remote/key) and the
|
||||
shared machinery; the actual commands live on namespace accessors such as
|
||||
:attr:`tabs`, :attr:`dom`, and :attr:`session`. Object construction
|
||||
(``Tab``/``Group``) comes from :class:`~browser_cli.sdk.factories.FactoryMixin`
|
||||
and multi-browser fan-out from :class:`~browser_cli.sdk.routing.RoutingMixin`.
|
||||
The client itself holds the connection target (browser/remote/key) and the
|
||||
shared machinery; the actual commands live on namespace accessors such as
|
||||
:attr:`tabs`, :attr:`dom`, and :attr:`session`. Object construction
|
||||
(``Tab``/``Group``) comes from :class:`~browser_cli.sdk.factories.FactoryMixin`
|
||||
and multi-browser fan-out from :class:`~browser_cli.sdk.routing.RoutingMixin`.
|
||||
"""
|
||||
|
||||
_browser: str | None
|
||||
_remote: str | None
|
||||
_key: str | None
|
||||
_command_sender: Callable
|
||||
nav: NavigationNS
|
||||
tabs: TabsNS
|
||||
groups: GroupsNS
|
||||
windows: WindowsNS
|
||||
dom: DomNS
|
||||
extract: ExtractNS
|
||||
page: PageNS
|
||||
storage: StorageNS
|
||||
cookies: CookiesNS
|
||||
session: SessionNS
|
||||
perf: PerfNS
|
||||
extension: ExtensionNS
|
||||
decorators: DecoratorsNS
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
browser: str | None = None,
|
||||
remote: str | None = None,
|
||||
key: str | None = None,
|
||||
*,
|
||||
_command_sender=None,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
browser: Profile alias to target. Required when multiple browser
|
||||
instances are active. Equivalent to ``--browser`` on the CLI.
|
||||
remote: Connect to a remote browser exposed via ``browser-cli serve``.
|
||||
Format: ``"host:port"`` (e.g. ``"browser-host.example:8765"``).
|
||||
Can be combined with ``browser`` to route to a specific
|
||||
remote profile.
|
||||
key: Path to Ed25519 private key PEM for pubkey auth, or ``"agent"``
|
||||
to use a key from the SSH agent (YubiKey, gpg-agent, etc.).
|
||||
Defaults to ``~/.config/browser-cli/client.key.pem`` if that file exists.
|
||||
"""
|
||||
self._browser = browser
|
||||
self._remote = remote
|
||||
self._key = key if key else None
|
||||
self._command_sender = _command_sender or send_command
|
||||
|
||||
def __init__(self, browser: str | None = None, remote: str | None = None, key: str | None = None):
|
||||
"""
|
||||
Args:
|
||||
browser: Profile alias to target. Required when multiple browser
|
||||
instances are active. Equivalent to ``--browser`` on the CLI.
|
||||
remote: Connect to a remote browser exposed via ``browser-cli serve``.
|
||||
Format: ``"host:port"`` (e.g. ``"192.168.1.10:8765"``).
|
||||
Can be combined with ``browser`` to route to a specific
|
||||
remote profile.
|
||||
key: Path to Ed25519 private key PEM for pubkey auth, or ``"agent"``
|
||||
to use a key from the SSH agent (YubiKey, gpg-agent, etc.).
|
||||
Defaults to ``~/.config/browser-cli/client.key.pem`` if that file exists.
|
||||
"""
|
||||
self._browser = browser
|
||||
self._remote = remote
|
||||
self._key = key if key else None
|
||||
for name, namespace_type in NAMESPACE_SPECS:
|
||||
setattr(self, name, namespace_type(self))
|
||||
self.decorators = DecoratorsNS(self)
|
||||
|
||||
# Command namespaces.
|
||||
self.nav = NavigationNS(self)
|
||||
self.tabs = TabsNS(self)
|
||||
self.groups = GroupsNS(self)
|
||||
self.windows = WindowsNS(self)
|
||||
self.dom = DomNS(self)
|
||||
self.extract = ExtractNS(self)
|
||||
self.page = PageNS(self)
|
||||
self.storage = StorageNS(self)
|
||||
self.cookies = CookiesNS(self)
|
||||
self.session = SessionNS(self)
|
||||
self.perf = PerfNS(self)
|
||||
self.extension = ExtensionNS(self)
|
||||
@property
|
||||
def browser(self) -> str | None:
|
||||
"""Target browser/profile alias, equivalent to ``--browser``."""
|
||||
return self._browser
|
||||
|
||||
@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 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
|
||||
|
||||
@property
|
||||
def key(self) -> str | None:
|
||||
"""Ed25519 key spec used for remote auth, if explicitly configured."""
|
||||
return self._key
|
||||
def dispatch(self, command: str, args: dict | None = None):
|
||||
"""Dispatch one browser command using this client's target settings."""
|
||||
return self._command_sender(command, args, profile=self._browser, remote=self._remote, key=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 require_tab(self, data, error: str):
|
||||
"""Convert a tab-like command response into a bound Tab."""
|
||||
return self.require_tab_response(data, error)
|
||||
|
||||
def command(self, command: str, args: dict | None = None):
|
||||
"""Send a raw browser-cli command and return its response.
|
||||
_FIELD_MISSING = object()
|
||||
|
||||
This is the SDK escape hatch for commands that do not have a dedicated
|
||||
namespace method yet.
|
||||
"""
|
||||
return self._cmd(command, args or {})
|
||||
def field(self, result, key, default=None, *, fallback=_FIELD_MISSING):
|
||||
"""Read a named field from command output."""
|
||||
if fallback is self._FIELD_MISSING:
|
||||
return self._field(result, key, default)
|
||||
return self._field(result, key, default, fallback=fallback)
|
||||
|
||||
def clients(self) -> list[dict]:
|
||||
"""Return the active browser clients known to this connection."""
|
||||
return self._cmd("clients.list", {})
|
||||
def _cmd(self, command: str, args: dict | None = None):
|
||||
return self.dispatch(command, args)
|
||||
|
||||
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
|
||||
namespace method yet.
|
||||
"""
|
||||
return self._cmd(command, args or {})
|
||||
|
||||
def clients(self) -> list[dict]:
|
||||
"""Return the active browser clients known to this connection."""
|
||||
return self._cmd("clients.list", {})
|
||||
|
||||
Reference in New Issue
Block a user