Files
browser-cli/browser_cli/sdk/base.py
T
daniel156161 7cb2a8b618
Testing / remote-protocol-compat (0.9.5) (push) Successful in 1m4s
Testing / test (push) Successful in 1m22s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 1m7s
Package Extension / package-extension (push) Successful in 1m1s
Build & Publish Package / publish (push) Successful in 1m5s
refactor: modularize auth transport and markdown
- Split auth into focused package modules for agent keys, file keys,
  signing, and post-quantum transport helpers while keeping the public
  browser_cli.auth import surface intact.
- Move transport encoding internals into a package with separate codec and
  binary-hoisting helpers, preserving browser_cli.transport compatibility.
- Extract remote TCP auth/socket helpers and serve challenge setup out of the
  runtime paths to make connection handling easier to reason about.
- Move the extension markdown extractor into a dedicated content/markdown
  folder with separate root selection, code normalization, renderer, and utils.
- Centralize CLI Rich rendering helpers for tab/window tree and table output,
  and add rendering tests for the shared builders.
- Remove local typing ignores in SDK/decorator/script plumbing and bump the
  package and extension version to 0.15.3.
2026-06-15 01:23:57 +02:00

117 lines
4.0 KiB
Python

"""Base helpers for SDK command namespaces.
Each namespace (``b.tabs``, ``b.dom``, ...) is a thin object bound to its
:class:`~browser_cli.BrowserCLI` client. Namespaces hold no state of their own;
they delegate to the client's shared infrastructure (command dispatch, the
multi-browser helpers, and the ``Tab``/``Group`` factories).
"""
from __future__ import annotations
from collections.abc import Callable
from functools import wraps
from typing import Any, TypeVar, cast
F = TypeVar("F", bound=Callable)
_MISSING = object()
def _clone_default(value):
if isinstance(value, (dict, list, set)):
return value.copy()
return value
def sdk_command(
name: str,
args: Callable | None = None,
*,
default=_MISSING,
field: str | None = None,
fallback=_MISSING,
mapper: Callable | None = None,
return_result: bool = True,
):
"""Decorate a namespace method as a browser wire command.
This keeps the public method signature/docstring on the normal Python
method, while moving repetitive command-dispatch plumbing into one place.
The wrapped function body is intentionally unused; it exists only for
signature, docs, and type hints.
"""
def decorator(func: F) -> F:
@wraps(func)
def wrapper(self, *method_args, **method_kwargs):
payload = args(self, *method_args, **method_kwargs) if args is not None else {}
result = self.command(name, payload)
if not return_result:
return None
if mapper is not None:
return mapper(self, result, *method_args, **method_kwargs)
if field is not None:
if fallback is _MISSING:
return self.field(result, field, default)
return self.field(result, field, default, fallback=fallback)
if default is not _MISSING and not result:
return _clone_default(default)
return result
setattr(wrapper, "_browser_cli_command", name)
return cast(F, wrapper)
return decorator
class Namespace:
"""A group of related SDK methods, bound to a BrowserCLI client."""
def __init__(self, client: Any):
self._c = client
def command(self, name: str, args: dict | None = None):
"""Dispatch a browser command through the owning client."""
return self._c.dispatch(name, args)
def tab_from(self, data: dict):
"""Build a bound Tab from a raw command response dict."""
return self._c.tab_from(data)
def group_from(self, data: dict):
"""Build a bound Group from a raw command response dict."""
return self._c.group_from(data)
def tab_from_target(self, data: dict, target):
"""Build a bound Tab for a multi-browser target."""
return self._c.tab_from_target(data, target)
def group_from_target(self, data: dict, target):
"""Build a bound Group for a multi-browser target."""
return self._c.group_from_target(data, target)
def tag_browser(self, item: dict, target):
"""Annotate a raw dict with its browser in multi-browser mode."""
return self._c.tag_browser(item, target)
def multi_list(self, name: str, args: dict | None, mapper: Callable):
"""Run a list command with multi-browser fan-out support."""
return self._c.multi_list(name, args, mapper)
def multi_count(self, name: str, args: dict | None = None):
"""Run a count command with multi-browser fan-out support."""
return self._c.multi_count(name, args)
def apply_tab_filter(self, filter_fn: Callable):
"""Apply a Python-side tab filter using client semantics."""
return self._c.apply_tab_filter(filter_fn)
def toggle_tab(self, name: str, tab_id: int | None):
"""Run a tab toggle command and return the affected tab ID."""
return self._c.toggle_tab(name, tab_id)
def require_tab(self, data, error: str):
"""Convert a tab-like response into a bound Tab or raise a clean error."""
return self._c.require_tab(data, error)
def field(self, result, key, default=None, *, fallback=_MISSING):
"""Read a field from command output using the client's SDK semantics."""
if fallback is _MISSING:
return self._c.field(result, key, default)
return self._c.field(result, key, default, fallback=fallback)