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
- 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.
117 lines
4.0 KiB
Python
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)
|