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:
+106
-9
@@ -1,19 +1,116 @@
|
||||
"""Base class for SDK command namespaces.
|
||||
"""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 (``_cmd``, the multi-browser
|
||||
helpers, and the ``Tab``/``Group`` factories).
|
||||
they delegate to the client's shared infrastructure (command dispatch, the
|
||||
multi-browser helpers, and the ``Tab``/``Group`` factories).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from collections.abc import Callable
|
||||
from functools import wraps
|
||||
from typing import Any, TypeVar
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from browser_cli import BrowserCLI
|
||||
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
|
||||
|
||||
wrapper._browser_cli_command = name # type: ignore[attr-defined]
|
||||
return wrapper # type: ignore[return-value]
|
||||
|
||||
return decorator
|
||||
|
||||
class Namespace:
|
||||
"""A group of related SDK methods, bound to a BrowserCLI client."""
|
||||
"""A group of related SDK methods, bound to a BrowserCLI client."""
|
||||
|
||||
def __init__(self, client: "BrowserCLI"):
|
||||
self._c = 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)
|
||||
|
||||
Reference in New Issue
Block a user