refactor(api): namespaced SDK + dedicated transport layer
Testing / remote-protocol-compat (0.9.3) (push) Successful in 42s
Testing / remote-protocol-compat (0.9.5) (push) Successful in 44s
Package Extension / package-extension (push) Successful in 43s
Build & Publish Package / publish (push) Successful in 43s
Testing / test (push) Successful in 45s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 42s
Testing / remote-protocol-compat (0.9.5) (push) Successful in 44s
Package Extension / package-extension (push) Successful in 43s
Build & Publish Package / publish (push) Successful in 43s
Testing / test (push) Successful in 45s
Restructure the Python API and internals around composable namespaces and a standalone transport/endpoint layer. Bump to 0.12.0. Python API: - Replace flat methods (b.tabs_list(), b.group_list()) with namespaces: b.nav, b.tabs, b.groups, b.windows, b.dom, b.extract, b.page, b.storage, b.cookies, b.session, b.perf, b.extension. - Shrink browser_cli/__init__.py to a thin composition root; move all behaviour into browser_cli/sdk/ (one module per namespace + factories, base, routing). Internals: - Add browser_cli/transport.py and remote_transport.py to isolate IPC from command logic; client.py now delegates instead of owning transport. - Add browser_cli/endpoints.py for endpoint resolution and browser_cli/errors.py for shared error types. - Extract markdown rendering into browser_cli/markdown.py (out of extract). - Add USER_AGENT to version_manager. Tooling & tests: - Add justfile with common dev tasks. - Update CLI commands and demo to the namespaced API. - Rework tests for the new layout; add test_transport.py and test_refactor_boundaries.py to lock in module boundaries. BREAKING CHANGE: flat API methods are removed in favour of namespaces (e.g. b.tabs_list() -> b.tabs.list(), b.group_list() -> b.groups.list()).
This commit is contained in:
@@ -1,40 +1,79 @@
|
||||
"""Shared helpers for the Click CLI command modules.
|
||||
|
||||
Every CLI command is a thin presentation layer over the Python SDK: it builds a
|
||||
:class:`~browser_cli.BrowserCLI` from the global ``--browser/--remote/--key``
|
||||
options, calls the matching SDK namespace method, and renders the result. The
|
||||
SDK is the single source of truth for command strings, argument shapes, and
|
||||
multi-browser routing.
|
||||
"""
|
||||
import functools
|
||||
import click
|
||||
from browser_cli.client import BrowserNotConnected, active_browser_targets, remote_browser_targets, send_command
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
from browser_cli import BrowserCLI, BrowserCounts
|
||||
from browser_cli.client import BrowserNotConnected
|
||||
|
||||
_console = Console()
|
||||
|
||||
GENTLE_MODES = ["auto", "normal", "gentle", "ultra"]
|
||||
|
||||
def _handle(command, args=None, profile=None):
|
||||
try:
|
||||
return send_command(command, args or {}, profile=profile)
|
||||
except BrowserNotConnected as e:
|
||||
_console.print(f"[red]Error:[/red] {e}")
|
||||
raise SystemExit(1)
|
||||
except RuntimeError as e:
|
||||
_console.print(f"[red]Browser error:[/red] {e}")
|
||||
raise SystemExit(1)
|
||||
# Reusable ``--tab`` option: select a tab by ID (default: the active tab).
|
||||
tab_option = click.option("--tab", "tab_id", type=int, default=None, help="Tab ID (default: active tab)")
|
||||
|
||||
|
||||
def _handle_multi(command, args=None, profile=None, remote=None):
|
||||
try:
|
||||
if remote:
|
||||
return send_command(command, args or {}, profile=profile, remote=remote)
|
||||
return send_command(command, args or {}, profile=profile)
|
||||
except (BrowserNotConnected, RuntimeError):
|
||||
return None
|
||||
def gentle_mode_option(help_text: str):
|
||||
"""Reusable ``--gentle-mode`` Click option (throttle mode for large operations)."""
|
||||
return click.option(
|
||||
"--gentle-mode",
|
||||
type=click.Choice(GENTLE_MODES),
|
||||
default="auto",
|
||||
show_default=True,
|
||||
help=help_text,
|
||||
)
|
||||
|
||||
def print_counts(result, noun: str, *, single_suffix: str = "") -> None:
|
||||
"""Render a count result.
|
||||
|
||||
def _multi_browser_targets():
|
||||
root = click.get_current_context().find_root()
|
||||
if root.obj.get("browser_explicit"):
|
||||
return []
|
||||
remote = root.obj.get("remote")
|
||||
key = root.obj.get("key")
|
||||
if remote:
|
||||
targets = remote_browser_targets(remote, key=key)
|
||||
In multi-browser mode (*result* is a :class:`~browser_cli.BrowserCounts`) print a
|
||||
per-browser table with a Total row; otherwise print a single ``N noun(s)`` line.
|
||||
"""
|
||||
if isinstance(result, BrowserCounts):
|
||||
table = Table(show_header=True, header_style="bold cyan")
|
||||
table.add_column("Browser")
|
||||
table.add_column(f"{noun.capitalize()}s", justify="right")
|
||||
for name, count in result.by_browser.items():
|
||||
table.add_row(name, str(count))
|
||||
table.add_row("Total", str(result.total))
|
||||
_console.print(table)
|
||||
else:
|
||||
targets = active_browser_targets(key=key)
|
||||
if len(targets) <= 1 and not any(target.remote for target in targets):
|
||||
return []
|
||||
return targets
|
||||
_console.print(f"[bold]{result}[/bold] {noun}(s){single_suffix}")
|
||||
|
||||
def client_from_ctx() -> BrowserCLI:
|
||||
"""Build a BrowserCLI from the root context's global options.
|
||||
|
||||
Reads ``browser``/``remote``/``key`` set by the top-level ``main`` group.
|
||||
Falls back to an unconfigured client when a command group is invoked
|
||||
standalone (e.g. in unit tests).
|
||||
"""
|
||||
obj = click.get_current_context().find_root().obj or {}
|
||||
return BrowserCLI(browser=obj.get("browser"), remote=obj.get("remote"), key=obj.get("key"))
|
||||
|
||||
def handle_errors(fn):
|
||||
"""Decorate a CLI command so SDK exceptions become clean errors + exit(1).
|
||||
|
||||
Apply as the innermost decorator (directly above ``def``) so Click's option
|
||||
decorators attach their params to the wrapper.
|
||||
"""
|
||||
@functools.wraps(fn)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return fn(*args, **kwargs)
|
||||
except BrowserNotConnected as e:
|
||||
_console.print(f"[red]Error:[/red] {e}")
|
||||
raise SystemExit(1)
|
||||
except RuntimeError as e:
|
||||
_console.print(f"[red]Browser error:[/red] {e}")
|
||||
raise SystemExit(1)
|
||||
|
||||
return wrapper
|
||||
|
||||
Reference in New Issue
Block a user