feat: group multi-browser output by source
Testing / remote-protocol-compat (0.9.3) (push) Successful in 52s
Testing / test (push) Successful in 1m2s
Testing / remote-protocol-compat (0.9.5) (push) Successful in 1m0s
Package Extension / package-extension (push) Successful in 1m11s
Build & Publish Package / publish (push) Successful in 1m7s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 52s
Testing / test (push) Successful in 1m2s
Testing / remote-protocol-compat (0.9.5) (push) Successful in 1m0s
Package Extension / package-extension (push) Successful in 1m11s
Build & Publish Package / publish (push) Successful in 1m7s
- Add browser source grouping metadata to SDK-created tabs, groups, list results, and aggregate count results. - Render grouped local/remote browser tables consistently for clients, tabs, groups, windows, sessions, and remote status output. - Document remote control, auth, HTTP gateway usage, and the refreshed project structure in the README. - Add coverage for grouped output and BrowserCounts browser_groups. - Bump the Python package, extension manifest, and lockfile to 0.15.6. - Add a just publish helper for building and publishing release artifacts.
This commit is contained in:
@@ -22,60 +22,74 @@ tab_option = click.option("--tab", "tab_id", type=int, default=None, help="Tab I
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
"""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.
|
||||
"""Render a count result.
|
||||
|
||||
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:
|
||||
_console.print(f"[bold]{result}[/bold] {noun}(s){single_suffix}")
|
||||
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", no_wrap=True)
|
||||
table.add_column(f"{noun.capitalize()}s", justify="right")
|
||||
rendered_groups: set[str] = set()
|
||||
for name, count in result.by_browser.items():
|
||||
group = result.browser_groups.get(name)
|
||||
if group:
|
||||
if group not in rendered_groups:
|
||||
group_total = sum(
|
||||
browser_count
|
||||
for browser_name, browser_count in result.by_browser.items()
|
||||
if result.browser_groups.get(browser_name) == group
|
||||
)
|
||||
table.add_row(f"[bold]{group}[/bold]", str(group_total))
|
||||
rendered_groups.add(group)
|
||||
display_name = name.removeprefix(f"{group}:")
|
||||
table.add_row(f" {display_name}", str(count))
|
||||
else:
|
||||
table.add_row(name, str(count))
|
||||
table.add_row("Total", str(result.total))
|
||||
_console.print(table)
|
||||
else:
|
||||
_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.
|
||||
"""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"))
|
||||
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).
|
||||
"""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 PermissionError as e:
|
||||
_console.print(f"[red]Blocked:[/red] {e}")
|
||||
raise SystemExit(1)
|
||||
except RuntimeError as e:
|
||||
_console.print(f"[red]Browser error:[/red] {e}")
|
||||
raise SystemExit(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 PermissionError as e:
|
||||
_console.print(f"[red]Blocked:[/red] {e}")
|
||||
raise SystemExit(1)
|
||||
except RuntimeError as e:
|
||||
_console.print(f"[red]Browser error:[/red] {e}")
|
||||
raise SystemExit(1)
|
||||
|
||||
return wrapper
|
||||
return wrapper
|
||||
|
||||
@@ -36,7 +36,7 @@ def _ensure_unique_browser_alias(alias: str, target_browser: str | None) -> None
|
||||
if alias in profiles and alias != target_profile:
|
||||
raise click.ClickException(f"Browser alias '{alias}' already exists")
|
||||
|
||||
def _append_clients(into, label, *, profile=None, remote=None, key=None, quiet_remote_warning=False):
|
||||
def _append_clients(into, label, *, profile=None, remote=None, key=None, quiet_remote_warning=False, profile_group=None):
|
||||
"""Query clients.list for one target and append each, tagged with *label*."""
|
||||
if quiet_remote_warning:
|
||||
result = send_command(
|
||||
@@ -50,6 +50,8 @@ def _append_clients(into, label, *, profile=None, remote=None, key=None, quiet_r
|
||||
result = send_command("clients.list", profile=profile, remote=remote, key=key)
|
||||
for c in (result or []):
|
||||
c["profile"] = label
|
||||
if profile_group:
|
||||
c["profileGroup"] = profile_group
|
||||
into.append(c)
|
||||
|
||||
@click.group("clients", invoke_without_command=True)
|
||||
@@ -89,7 +91,14 @@ def _collect_remote_alias_clients(all_clients: list, browser_alias: str, key) ->
|
||||
sys.exit(1)
|
||||
for target in targets:
|
||||
try:
|
||||
_append_clients(all_clients, target.display_name, profile=target.profile, remote=resolved.remote, key=key)
|
||||
_append_clients(
|
||||
all_clients,
|
||||
target.display_name,
|
||||
profile=target.profile,
|
||||
remote=resolved.remote,
|
||||
key=key,
|
||||
profile_group=target.display_group,
|
||||
)
|
||||
except (BrowserNotConnected, RuntimeError):
|
||||
continue
|
||||
|
||||
@@ -109,10 +118,11 @@ def _collect_local_and_saved_remote_clients(all_clients: list) -> None:
|
||||
for profile_name, sock_path in profiles.items():
|
||||
display_profile = display_browser_name(profile_name, sock_path)
|
||||
try:
|
||||
_append_clients(all_clients, display_profile, profile=profile_name)
|
||||
_append_clients(all_clients, display_profile, profile=profile_name, profile_group="local")
|
||||
except (BrowserNotConnected, RuntimeError):
|
||||
all_clients.append({
|
||||
"profile": display_profile,
|
||||
"profileGroup": "local",
|
||||
"name": "—",
|
||||
"version": "—",
|
||||
"extensionVersion": "disconnected",
|
||||
@@ -130,6 +140,7 @@ def _collect_local_and_saved_remote_clients(all_clients: list) -> None:
|
||||
profile=target.profile,
|
||||
remote=target.remote,
|
||||
quiet_remote_warning=True,
|
||||
profile_group=target.display_group,
|
||||
)
|
||||
except (BrowserNotConnected, RuntimeError):
|
||||
continue
|
||||
@@ -137,13 +148,25 @@ def _collect_local_and_saved_remote_clients(all_clients: list) -> None:
|
||||
def _print_clients(all_clients: list) -> None:
|
||||
from rich.table import Table
|
||||
table = Table(show_header=True, header_style="bold cyan")
|
||||
table.add_column("Profile")
|
||||
table.add_column("Profile", no_wrap=True)
|
||||
table.add_column("Browser")
|
||||
table.add_column("Version")
|
||||
table.add_column("Extension Version")
|
||||
rendered_groups: set[str] = set()
|
||||
groups = {c.get("profileGroup") for c in all_clients if c.get("profileGroup")}
|
||||
grouped = bool(groups and groups != {"local"})
|
||||
for c in all_clients:
|
||||
group = c.get("profileGroup") if grouped else None
|
||||
if group:
|
||||
if group not in rendered_groups:
|
||||
table.add_row(f"[bold]{group}[/bold]", "", "", "")
|
||||
rendered_groups.add(group)
|
||||
profile = str(c.get("profile", "")).removeprefix(f"{group}:")
|
||||
profile = f" {profile}"
|
||||
else:
|
||||
profile = c.get("profile", "")
|
||||
table.add_row(
|
||||
c.get("profile", ""),
|
||||
profile,
|
||||
c.get("name", ""),
|
||||
c.get("version", ""),
|
||||
c.get("extensionVersion", ""),
|
||||
|
||||
@@ -1,33 +1,19 @@
|
||||
import click
|
||||
from browser_cli.commands import client_from_ctx, gentle_mode_option, handle_errors, print_counts
|
||||
from browser_cli.commands.rendering import print_browser_grouped_table_rows
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
console = Console()
|
||||
|
||||
def _print_groups(groups, *, show_browser: bool = False) -> None:
|
||||
if not groups:
|
||||
console.print("[yellow]No groups found[/yellow]")
|
||||
return
|
||||
table = Table(show_header=True, header_style="bold cyan")
|
||||
if show_browser:
|
||||
table.add_column("Browser")
|
||||
table.add_column("ID", style="dim", no_wrap=True)
|
||||
table.add_column("Name")
|
||||
table.add_column("Color", width=10)
|
||||
table.add_column("Collapsed", width=10)
|
||||
table.add_column("Tabs", width=6)
|
||||
for g in groups:
|
||||
row = [
|
||||
(g.browser or "") if show_browser else None,
|
||||
str(g.id),
|
||||
g.title or "",
|
||||
g.color or "",
|
||||
"yes" if g.collapsed else "no",
|
||||
str(g.tab_count),
|
||||
]
|
||||
table.add_row(*[value for value in row if value is not None])
|
||||
console.print(table)
|
||||
columns = [
|
||||
("ID", lambda g: g.id),
|
||||
("Name", lambda g: g.title or ""),
|
||||
("Color", lambda g: g.color or ""),
|
||||
("Collapsed", lambda g: "yes" if g.collapsed else "no"),
|
||||
("Tabs", lambda g: g.tab_count),
|
||||
]
|
||||
print_browser_grouped_table_rows(groups, columns, console=console, empty_message="[yellow]No groups found[/yellow]")
|
||||
|
||||
@click.group("groups")
|
||||
def group_group():
|
||||
|
||||
@@ -8,6 +8,7 @@ from rich.table import Table
|
||||
|
||||
from browser_cli import BrowserCLI
|
||||
from browser_cli.commands import handle_errors
|
||||
from browser_cli.commands.rendering import print_browser_grouped_table_rows
|
||||
from browser_cli.remote.registry import REMOTE_REGISTRY_PATH, load_remotes, save_remote_key
|
||||
|
||||
console = Console()
|
||||
@@ -23,14 +24,23 @@ def remote_group():
|
||||
def remote_status(endpoint, key):
|
||||
"""Probe a remote endpoint and show server/client status."""
|
||||
client = BrowserCLI(remote=endpoint, key=key)
|
||||
clients = client.clients()
|
||||
table = Table(show_header=True, header_style="bold cyan")
|
||||
table.add_column("Profile")
|
||||
table.add_column("Browser")
|
||||
table.add_column("Extension")
|
||||
for item in clients:
|
||||
table.add_row(str(item.get("profile", "")), str(item.get("name", "")), str(item.get("extensionVersion", "")))
|
||||
console.print(table)
|
||||
clients = [
|
||||
{**item, "profileLabel": item.get("profile", ""), "profileGroup": endpoint}
|
||||
for item in client.clients()
|
||||
]
|
||||
columns = [
|
||||
("Browser", lambda item: item.get("name", "")),
|
||||
("Extension", lambda item: item.get("extensionVersion", "")),
|
||||
]
|
||||
print_browser_grouped_table_rows(
|
||||
clients,
|
||||
columns,
|
||||
console=console,
|
||||
empty_message="[yellow]No browser clients found[/yellow]",
|
||||
browser_getter=lambda item: item.get("profileLabel", ""),
|
||||
group_getter=lambda item: item.get("profileGroup", ""),
|
||||
browser_header="Profile",
|
||||
)
|
||||
|
||||
@remote_group.command("trust")
|
||||
@click.argument("endpoint")
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from browser_cli.commands.rendering.common import (
|
||||
Column,
|
||||
item_value,
|
||||
print_browser_grouped_table_rows,
|
||||
print_table_rows,
|
||||
print_tree,
|
||||
shorten,
|
||||
@@ -44,6 +45,7 @@ __all__ = [
|
||||
"group_tree_label",
|
||||
"item_value",
|
||||
"no_wrap_text",
|
||||
"print_browser_grouped_table_rows",
|
||||
"print_table_rows",
|
||||
"print_tree",
|
||||
"scoped_browser_label",
|
||||
|
||||
@@ -82,3 +82,56 @@ def print_table_rows(
|
||||
for row in rows:
|
||||
table.add_row(*[text_value(getter(row)) for _header, getter in columns])
|
||||
Console(width=terminal_width(console)).print(table)
|
||||
|
||||
def print_browser_grouped_table_rows(
|
||||
rows: Sequence[Row],
|
||||
columns: Sequence[Column],
|
||||
*,
|
||||
console: Console,
|
||||
empty_message: str,
|
||||
browser_getter: Callable[[Row], CellValue | None] = lambda row: item_value(row, "browser"),
|
||||
group_getter: Callable[[Row], CellValue | None] = lambda row: item_value(row, "browser_group", item_value(row, "browserGroup")),
|
||||
browser_header: str = "Browser",
|
||||
show_header: bool = True,
|
||||
header_style: str = "bold cyan",
|
||||
) -> None:
|
||||
"""Render rows with optional local/remote browser grouping.
|
||||
|
||||
Rows without a browser label are rendered as a normal table. Rows with
|
||||
``browser_group``/``browserGroup`` get a group header (for example ``local``
|
||||
or a remote host) and a short indented profile label below it.
|
||||
"""
|
||||
if not rows:
|
||||
console.print(empty_message)
|
||||
return
|
||||
|
||||
show_browser = any(bool(browser_getter(row)) for row in rows)
|
||||
if not show_browser:
|
||||
print_table_rows(
|
||||
rows,
|
||||
columns,
|
||||
console=console,
|
||||
empty_message=empty_message,
|
||||
show_header=show_header,
|
||||
header_style=header_style,
|
||||
)
|
||||
return
|
||||
|
||||
table = Table(show_header=show_header, header_style=header_style)
|
||||
table.add_column(browser_header, no_wrap=True)
|
||||
for header, _getter in columns:
|
||||
table.add_column(header)
|
||||
|
||||
rendered_groups: set[str] = set()
|
||||
for row in rows:
|
||||
browser = text_value(browser_getter(row))
|
||||
group = text_value(group_getter(row))
|
||||
if group:
|
||||
if group not in rendered_groups:
|
||||
table.add_row(f"[bold]{group}[/bold]", *["" for _header, _getter in columns])
|
||||
rendered_groups.add(group)
|
||||
browser = browser.removeprefix(f"{group}:")
|
||||
browser = f" {browser}"
|
||||
table.add_row(browser, *[text_value(getter(row)) for _header, getter in columns])
|
||||
|
||||
Console(width=terminal_width(console)).print(table)
|
||||
|
||||
@@ -105,24 +105,20 @@ def session_diff(name_a, name_b):
|
||||
def session_list():
|
||||
"""List all saved sessions."""
|
||||
from datetime import datetime
|
||||
from rich.table import Table
|
||||
from browser_cli.commands.rendering import print_browser_grouped_table_rows
|
||||
sessions = client_from_ctx().session.list()
|
||||
if not sessions:
|
||||
console.print("[yellow]No saved sessions[/yellow]")
|
||||
return
|
||||
show_browser = any("browser" in s for s in sessions)
|
||||
table = Table(show_header=True, header_style="bold cyan")
|
||||
if show_browser:
|
||||
table.add_column("Browser")
|
||||
table.add_column("Name")
|
||||
table.add_column("Tabs", width=6)
|
||||
table.add_column("Saved at")
|
||||
for s in sessions:
|
||||
saved = datetime.fromtimestamp(s["savedAt"] / 1000).strftime("%Y-%m-%d %H:%M") if s.get("savedAt") else ""
|
||||
row = [s.get("browser", "")] if show_browser else []
|
||||
row.extend([s["name"], str(s["tabs"]), saved])
|
||||
table.add_row(*row)
|
||||
console.print(table)
|
||||
def saved_at(session):
|
||||
return datetime.fromtimestamp(session["savedAt"] / 1000).strftime("%Y-%m-%d %H:%M") if session.get("savedAt") else ""
|
||||
|
||||
columns = [
|
||||
("Name", lambda session: session["name"]),
|
||||
("Tabs", lambda session: session["tabs"]),
|
||||
("Saved at", saved_at),
|
||||
]
|
||||
print_browser_grouped_table_rows(sessions, columns, console=console, empty_message="[yellow]No saved sessions[/yellow]")
|
||||
|
||||
@session_group.command("remove")
|
||||
@click.argument("name")
|
||||
|
||||
@@ -2,25 +2,22 @@ import base64
|
||||
import binascii
|
||||
import click
|
||||
from browser_cli.commands import client_from_ctx, gentle_mode_option, handle_errors, print_counts, tab_option
|
||||
from browser_cli.commands.rendering import build_tabs_tree, print_table_rows, print_tree
|
||||
from browser_cli.commands.rendering import build_tabs_tree, print_browser_grouped_table_rows, print_tree
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
console = Console()
|
||||
|
||||
def _print_tabs(tabs, *, show_browser: bool = False) -> None:
|
||||
columns = []
|
||||
if show_browser:
|
||||
columns.append(("Browser", lambda tab: tab.browser or ""))
|
||||
columns.extend([
|
||||
columns = [
|
||||
("ID", lambda tab: tab.id),
|
||||
("Window", lambda tab: tab.window_id),
|
||||
("Active", lambda tab: "[green]✓[/green]" if tab.active else ""),
|
||||
("Muted", lambda tab: "[yellow]✓[/yellow]" if tab.muted else ""),
|
||||
("Title", lambda tab: (tab.title or "")[:60]),
|
||||
("URL", lambda tab: (tab.url or "")[:80]),
|
||||
])
|
||||
print_table_rows(tabs, columns, console=console, empty_message="[yellow]No tabs found[/yellow]")
|
||||
]
|
||||
print_browser_grouped_table_rows(tabs, columns, console=console, empty_message="[yellow]No tabs found[/yellow]")
|
||||
|
||||
@click.group("tabs")
|
||||
def tabs_group():
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
import click
|
||||
from browser_cli.commands import client_from_ctx, handle_errors
|
||||
from browser_cli.commands.rendering import build_windows_tree, print_table_rows, print_tree
|
||||
from browser_cli.commands.rendering import build_windows_tree, print_browser_grouped_table_rows, print_tree
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
|
||||
def _print_windows(windows: list[dict], *, show_browser: bool = False) -> None:
|
||||
columns = []
|
||||
if show_browser:
|
||||
columns.append(("Browser", lambda window: window.get("browser", "")))
|
||||
columns.extend([
|
||||
columns = [
|
||||
("ID", lambda window: window.get("id", "")),
|
||||
("Alias", lambda window: window.get("alias") or ""),
|
||||
("Tabs", lambda window: window.get("tabCount", "")),
|
||||
("State", lambda window: window.get("state") or ""),
|
||||
])
|
||||
print_table_rows(windows, columns, console=console, empty_message="[yellow]No windows found[/yellow]")
|
||||
]
|
||||
print_browser_grouped_table_rows(windows, columns, console=console, empty_message="[yellow]No windows found[/yellow]")
|
||||
|
||||
@click.group("windows")
|
||||
def windows_group():
|
||||
|
||||
@@ -31,6 +31,7 @@ class BrowserCounts:
|
||||
"""Aggregated per-browser counts returned in implicit multi-browser mode."""
|
||||
total: int
|
||||
by_browser: dict[str, int]
|
||||
browser_groups: dict[str, str] = field(default_factory=dict)
|
||||
|
||||
# ── Tab ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
+101
-94
@@ -11,107 +11,114 @@ from typing import Any, Protocol, cast
|
||||
|
||||
from browser_cli.models import Group, Tab
|
||||
|
||||
def _target_group(target) -> str | None:
|
||||
if target is None:
|
||||
return None
|
||||
return getattr(target, "display_group", None) or ("local" if target.remote is None else None)
|
||||
|
||||
class _FactoryClient(Protocol):
|
||||
_key: str | None
|
||||
_key: str | None
|
||||
|
||||
class FactoryMixin:
|
||||
"""Turn raw response dicts into bound ``Tab``/``Group`` objects.
|
||||
"""Turn raw response dicts into bound ``Tab``/``Group`` objects.
|
||||
|
||||
Mixed into :class:`~browser_cli.BrowserCLI`; relies on the client providing
|
||||
``_browser``/``_remote``/``_key`` and being constructible via ``type(self)``.
|
||||
"""
|
||||
Mixed into :class:`~browser_cli.BrowserCLI`; relies on the client providing
|
||||
``_browser``/``_remote``/``_key`` and being constructible via ``type(self)``.
|
||||
"""
|
||||
|
||||
def tab_from(
|
||||
self,
|
||||
data: dict,
|
||||
*,
|
||||
browser_profile: str | None = None,
|
||||
browser_name: str | None = None,
|
||||
browser_remote: str | None = None,
|
||||
browser_type: str | None = None,
|
||||
browser_group: str | None = None,
|
||||
) -> Tab:
|
||||
tab = Tab(
|
||||
id=data["id"],
|
||||
window_id=data.get("windowId", 0),
|
||||
active=data.get("active", False),
|
||||
muted=data.get("muted", False),
|
||||
title=data.get("title") or "",
|
||||
url=data.get("url") or "",
|
||||
group_id=data.get("groupId") or None,
|
||||
index=data.get("index", 0) or 0,
|
||||
browser=browser_name,
|
||||
browser_name=browser_type,
|
||||
browser_group=browser_group,
|
||||
)
|
||||
client = cast(_FactoryClient, self)
|
||||
tab._browser = self if browser_profile is None else cast(Any, type(self))(
|
||||
browser=browser_profile,
|
||||
remote=browser_remote,
|
||||
key=client._key,
|
||||
_command_sender=getattr(self, "_command_sender", None),
|
||||
)
|
||||
return tab
|
||||
def tab_from(
|
||||
self,
|
||||
data: dict,
|
||||
*,
|
||||
browser_profile: str | None = None,
|
||||
browser_name: str | None = None,
|
||||
browser_remote: str | None = None,
|
||||
browser_type: str | None = None,
|
||||
browser_group: str | None = None,
|
||||
) -> Tab:
|
||||
tab = Tab(
|
||||
id=data["id"],
|
||||
window_id=data.get("windowId", 0),
|
||||
active=data.get("active", False),
|
||||
muted=data.get("muted", False),
|
||||
title=data.get("title") or "",
|
||||
url=data.get("url") or "",
|
||||
group_id=data.get("groupId") or None,
|
||||
index=data.get("index", 0) or 0,
|
||||
browser=browser_name,
|
||||
browser_name=browser_type,
|
||||
browser_group=browser_group,
|
||||
)
|
||||
client = cast(_FactoryClient, self)
|
||||
tab._browser = self if browser_profile is None else cast(Any, type(self))(
|
||||
browser=browser_profile,
|
||||
remote=browser_remote,
|
||||
key=client._key,
|
||||
_command_sender=getattr(self, "_command_sender", None),
|
||||
)
|
||||
return tab
|
||||
|
||||
def require_tab_response(self, data, error: str) -> Tab:
|
||||
"""Build a bound Tab from a tab-shaped response, or raise ``RuntimeError(error)``."""
|
||||
if not isinstance(data, dict) or "id" not in data:
|
||||
raise RuntimeError(error)
|
||||
return self.tab_from(data)
|
||||
def require_tab_response(self, data, error: str) -> Tab:
|
||||
"""Build a bound Tab from a tab-shaped response, or raise ``RuntimeError(error)``."""
|
||||
if not isinstance(data, dict) or "id" not in data:
|
||||
raise RuntimeError(error)
|
||||
return self.tab_from(data)
|
||||
|
||||
def group_from(
|
||||
self,
|
||||
data: dict,
|
||||
*,
|
||||
browser_profile: str | None = None,
|
||||
browser_name: str | None = None,
|
||||
browser_remote: str | None = None,
|
||||
browser_type: str | None = None,
|
||||
browser_group: str | None = None,
|
||||
) -> Group:
|
||||
group = Group(
|
||||
id=data["id"],
|
||||
title=data.get("title") or "",
|
||||
color=data.get("color") or "",
|
||||
collapsed=data.get("collapsed", False),
|
||||
tab_count=data.get("tabCount", 0),
|
||||
window_id=data.get("windowId"),
|
||||
browser=browser_name,
|
||||
browser_name=browser_type,
|
||||
browser_group=browser_group,
|
||||
)
|
||||
client = cast(_FactoryClient, self)
|
||||
group._browser = self if browser_profile is None else cast(Any, type(self))(
|
||||
browser=browser_profile,
|
||||
remote=browser_remote,
|
||||
key=client._key,
|
||||
_command_sender=getattr(self, "_command_sender", None),
|
||||
)
|
||||
return group
|
||||
def group_from(
|
||||
self,
|
||||
data: dict,
|
||||
*,
|
||||
browser_profile: str | None = None,
|
||||
browser_name: str | None = None,
|
||||
browser_remote: str | None = None,
|
||||
browser_type: str | None = None,
|
||||
browser_group: str | None = None,
|
||||
) -> Group:
|
||||
group = Group(
|
||||
id=data["id"],
|
||||
title=data.get("title") or "",
|
||||
color=data.get("color") or "",
|
||||
collapsed=data.get("collapsed", False),
|
||||
tab_count=data.get("tabCount", 0),
|
||||
window_id=data.get("windowId"),
|
||||
browser=browser_name,
|
||||
browser_name=browser_type,
|
||||
browser_group=browser_group,
|
||||
)
|
||||
client = cast(_FactoryClient, self)
|
||||
group._browser = self if browser_profile is None else cast(Any, type(self))(
|
||||
browser=browser_profile,
|
||||
remote=browser_remote,
|
||||
key=client._key,
|
||||
_command_sender=getattr(self, "_command_sender", None),
|
||||
)
|
||||
return group
|
||||
|
||||
def tab_from_target(self, data: dict, target) -> Tab:
|
||||
"""Build a Tab, tagging it with *target* in multi-browser mode (``None`` = local)."""
|
||||
return self.tab_from(
|
||||
data,
|
||||
browser_profile=target.profile if target else None,
|
||||
browser_name=target.display_name if target else None,
|
||||
browser_remote=target.remote if target else None,
|
||||
browser_type=getattr(target, "browser_name", None) if target else None,
|
||||
browser_group=getattr(target, "display_group", None) if target else None,
|
||||
)
|
||||
def tab_from_target(self, data: dict, target) -> Tab:
|
||||
"""Build a Tab, tagging it with *target* in multi-browser mode (``None`` = local)."""
|
||||
return self.tab_from(
|
||||
data,
|
||||
browser_profile=target.profile if target else None,
|
||||
browser_name=target.display_name if target else None,
|
||||
browser_remote=target.remote if target else None,
|
||||
browser_type=getattr(target, "browser_name", None) if target else None,
|
||||
browser_group=_target_group(target),
|
||||
)
|
||||
|
||||
def group_from_target(self, data: dict, target) -> Group:
|
||||
"""Build a Group, tagging it with *target* in multi-browser mode (``None`` = local)."""
|
||||
return self.group_from(
|
||||
data,
|
||||
browser_profile=target.profile if target else None,
|
||||
browser_name=target.display_name if target else None,
|
||||
browser_remote=target.remote if target else None,
|
||||
browser_type=getattr(target, "browser_name", None) if target else None,
|
||||
browser_group=getattr(target, "display_group", None) if target else None,
|
||||
)
|
||||
def group_from_target(self, data: dict, target) -> Group:
|
||||
"""Build a Group, tagging it with *target* in multi-browser mode (``None`` = local)."""
|
||||
return self.group_from(
|
||||
data,
|
||||
browser_profile=target.profile if target else None,
|
||||
browser_name=target.display_name if target else None,
|
||||
browser_remote=target.remote if target else None,
|
||||
browser_type=getattr(target, "browser_name", None) if target else None,
|
||||
browser_group=_target_group(target),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def tag_browser(item: dict, target) -> dict:
|
||||
"""Return *item* as-is locally, or with a ``browser`` key in multi-browser mode."""
|
||||
return item if target is None else {**item, "browser": target.display_name}
|
||||
@staticmethod
|
||||
def tag_browser(item: dict, target) -> dict:
|
||||
"""Return *item* as-is locally, or with browser metadata in multi-browser mode."""
|
||||
if target is None:
|
||||
return item
|
||||
return {**item, "browser": target.display_name, "browserGroup": _target_group(target)}
|
||||
|
||||
@@ -126,7 +126,12 @@ class RoutingMixin:
|
||||
if not multi_results:
|
||||
return self._client.dispatch(command, args or {})
|
||||
by_browser = {target.display_name: int(count or 0) for target, count in multi_results}
|
||||
return BrowserCounts(total=sum(by_browser.values()), by_browser=by_browser)
|
||||
browser_groups = {
|
||||
target.display_name: target.display_group or "local"
|
||||
for target, _count in multi_results
|
||||
if target.display_group or target.remote is None
|
||||
}
|
||||
return BrowserCounts(total=sum(by_browser.values()), by_browser=by_browser, browser_groups=browser_groups)
|
||||
|
||||
def multi_list(self, command: str, args: dict | None, mapper):
|
||||
"""List command, flattening per-browser results in multi-browser mode.
|
||||
|
||||
Reference in New Issue
Block a user