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:
2026-06-13 23:31:24 +02:00
parent fd5447cbb9
commit 076914e5b7
88 changed files with 7491 additions and 5228 deletions
+137 -97
View File
@@ -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", {})