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:
+132
-113
@@ -1,150 +1,169 @@
|
||||
"""DOM, content-extraction, and page-info namespaces: ``b.dom.*``, ``b.extract.*``, ``b.page.*``."""
|
||||
from __future__ import annotations
|
||||
|
||||
from browser_cli.sdk.base import Namespace
|
||||
from browser_cli.sdk.base import Namespace, sdk_command
|
||||
|
||||
def _selector_args(self, selector):
|
||||
return {"selector": selector}
|
||||
|
||||
def _selector_value_args(self, selector, value):
|
||||
return {"selector": selector, "value": value}
|
||||
|
||||
def _extract_markdown(self, result, *args, **kwargs) -> str:
|
||||
from browser_cli.markdown import render_markdown
|
||||
|
||||
return render_markdown(result)
|
||||
|
||||
class DomNS(Namespace):
|
||||
"""Query and drive page elements in the active (or specified) tab."""
|
||||
"""Query and drive page elements in the active (or specified) tab."""
|
||||
|
||||
def query(self, selector: str) -> list[dict]:
|
||||
return self._c._cmd("dom.query", {"selector": selector}) or []
|
||||
@sdk_command("dom.query", _selector_args, default=[])
|
||||
def query(self, selector: str) -> list[dict]:
|
||||
"""Return elements matching a CSS selector."""
|
||||
|
||||
def click(self, selector: str) -> None:
|
||||
self._c._cmd("dom.click", {"selector": selector})
|
||||
@sdk_command("dom.click", _selector_args, return_result=False)
|
||||
def click(self, selector: str) -> None:
|
||||
"""Click the first element matching a CSS selector."""
|
||||
|
||||
def type(self, selector: str, text: str) -> None:
|
||||
self._c._cmd("dom.type", {"selector": selector, "text": text})
|
||||
@sdk_command("dom.type", lambda self, selector, text: {"selector": selector, "text": text}, return_result=False)
|
||||
def type(self, selector: str, text: str) -> None:
|
||||
"""Type text into the first matching element."""
|
||||
|
||||
def attr(self, selector: str, attr: str) -> list[str]:
|
||||
return self._c._cmd("dom.attr", {"selector": selector, "attr": attr}) or []
|
||||
@sdk_command("dom.attr", lambda self, selector, attr: {"selector": selector, "attr": attr}, default=[])
|
||||
def attr(self, selector: str, attr: str) -> list[str]:
|
||||
"""Return an attribute from all matching elements."""
|
||||
|
||||
def text(self, selector: str) -> list[str]:
|
||||
return self._c._cmd("dom.text", {"selector": selector}) or []
|
||||
@sdk_command("dom.text", _selector_args, default=[])
|
||||
def text(self, selector: str) -> list[str]:
|
||||
"""Return text from all matching elements."""
|
||||
|
||||
def exists(self, selector: str) -> bool:
|
||||
return self._c._cmd("dom.exists", {"selector": selector}) or False
|
||||
@sdk_command("dom.exists", _selector_args, default=False)
|
||||
def exists(self, selector: str) -> bool:
|
||||
"""Return whether a selector exists."""
|
||||
|
||||
def scroll(self, selector: str | None = None, *, x: int | None = None, y: int | None = None) -> None:
|
||||
"""Scroll to a CSS selector or to pixel coordinates."""
|
||||
self._c._cmd("dom.scroll", {"selector": selector, "x": x, "y": y})
|
||||
@sdk_command("dom.scroll", lambda self, selector=None, *, x=None, y=None: {"selector": selector, "x": x, "y": y}, return_result=False)
|
||||
def scroll(self, selector: str | None = None, *, x: int | None = None, y: int | None = None) -> None:
|
||||
"""Scroll to a CSS selector or to pixel coordinates."""
|
||||
|
||||
def select(self, selector: str, value: str) -> None:
|
||||
"""Set the value of a <select> element."""
|
||||
self._c._cmd("dom.select", {"selector": selector, "value": value})
|
||||
@sdk_command("dom.select", _selector_value_args, return_result=False)
|
||||
def select(self, selector: str, value: str) -> None:
|
||||
"""Set the value of a <select> element."""
|
||||
|
||||
def eval(self, code: str, tab_id: int | None = None):
|
||||
"""Evaluate JavaScript in the page's main world and return the result."""
|
||||
return self._c._cmd("dom.eval", {"code": code, "tabId": tab_id})
|
||||
@sdk_command("dom.eval", lambda self, code, tab_id=None: {"code": code, "tabId": tab_id})
|
||||
def eval(self, code: str, tab_id: int | None = None):
|
||||
"""Evaluate JavaScript in the page's main world and return the result."""
|
||||
|
||||
def key(self, key: str, selector: str | None = None) -> None:
|
||||
"""Dispatch a keyboard event. key examples: 'Enter', 'Tab', 'Escape', 'ArrowDown'."""
|
||||
self._c._cmd("dom.key", {"key": key, "selector": selector})
|
||||
@sdk_command("dom.key", lambda self, key, selector=None: {"key": key, "selector": selector}, return_result=False)
|
||||
def key(self, key: str, selector: str | None = None) -> None:
|
||||
"""Dispatch a keyboard event. key examples: 'Enter', 'Tab', 'Escape', 'ArrowDown'."""
|
||||
|
||||
def hover(self, selector: str) -> None:
|
||||
"""Dispatch mouseover/mouseenter on an element."""
|
||||
self._c._cmd("dom.hover", {"selector": selector})
|
||||
@sdk_command("dom.hover", _selector_args, return_result=False)
|
||||
def hover(self, selector: str) -> None:
|
||||
"""Dispatch mouseover/mouseenter on an element."""
|
||||
|
||||
def check(self, selector: str) -> None:
|
||||
"""Check a checkbox."""
|
||||
self._c._cmd("dom.check", {"selector": selector})
|
||||
@sdk_command("dom.check", _selector_args, return_result=False)
|
||||
def check(self, selector: str) -> None:
|
||||
"""Check a checkbox."""
|
||||
|
||||
def uncheck(self, selector: str) -> None:
|
||||
"""Uncheck a checkbox."""
|
||||
self._c._cmd("dom.uncheck", {"selector": selector})
|
||||
@sdk_command("dom.uncheck", _selector_args, return_result=False)
|
||||
def uncheck(self, selector: str) -> None:
|
||||
"""Uncheck a checkbox."""
|
||||
|
||||
def clear(self, selector: str) -> None:
|
||||
"""Clear the value of an input element."""
|
||||
self._c._cmd("dom.clear", {"selector": selector})
|
||||
@sdk_command("dom.clear", _selector_args, return_result=False)
|
||||
def clear(self, selector: str) -> None:
|
||||
"""Clear the value of an input element."""
|
||||
|
||||
def focus(self, selector: str) -> None:
|
||||
"""Focus an element."""
|
||||
self._c._cmd("dom.focus", {"selector": selector})
|
||||
@sdk_command("dom.focus", _selector_args, return_result=False)
|
||||
def focus(self, selector: str) -> None:
|
||||
"""Focus an element."""
|
||||
|
||||
def submit(self, selector: str) -> None:
|
||||
"""Submit the form containing the matched element."""
|
||||
self._c._cmd("dom.submit", {"selector": selector})
|
||||
@sdk_command("dom.submit", _selector_args, return_result=False)
|
||||
def submit(self, selector: str) -> None:
|
||||
"""Submit the form containing the matched element."""
|
||||
|
||||
def poll(
|
||||
self,
|
||||
selector: str,
|
||||
pattern: str,
|
||||
*,
|
||||
attr: str | None = None,
|
||||
timeout: float = 30.0,
|
||||
interval: float = 0.5,
|
||||
tab_id: int | None = None,
|
||||
) -> dict:
|
||||
"""Poll selector's text/value until it matches regex pattern.
|
||||
def poll(
|
||||
self,
|
||||
selector: str,
|
||||
pattern: str,
|
||||
*,
|
||||
attr: str | None = None,
|
||||
timeout: float = 30.0,
|
||||
interval: float = 0.5,
|
||||
tab_id: int | None = None,
|
||||
) -> dict:
|
||||
"""Poll selector's text/value until it matches regex pattern.
|
||||
|
||||
Returns ``{"selector": ..., "value": ..., "pattern": ...}`` when matched.
|
||||
"""
|
||||
return self._c._cmd("dom.poll", {
|
||||
"selector": selector,
|
||||
"pattern": pattern,
|
||||
"attr": attr,
|
||||
"timeout": int(timeout * 1000),
|
||||
"interval": int(interval * 1000),
|
||||
"tabId": tab_id,
|
||||
})
|
||||
Returns ``{"selector": ..., "value": ..., "pattern": ...}`` when matched.
|
||||
"""
|
||||
return self.command("dom.poll", {
|
||||
"selector": selector,
|
||||
"pattern": pattern,
|
||||
"attr": attr,
|
||||
"timeout": int(timeout * 1000),
|
||||
"interval": int(interval * 1000),
|
||||
"tabId": tab_id,
|
||||
})
|
||||
|
||||
def wait_for(
|
||||
self,
|
||||
selector: str,
|
||||
*,
|
||||
timeout: float = 10.0,
|
||||
visible: bool = False,
|
||||
hidden: bool = False,
|
||||
tab_id: int | None = None,
|
||||
) -> dict:
|
||||
"""Wait until a CSS selector appears (or disappears) in the DOM.
|
||||
def wait_for(
|
||||
self,
|
||||
selector: str,
|
||||
*,
|
||||
timeout: float = 10.0,
|
||||
visible: bool = False,
|
||||
hidden: bool = False,
|
||||
tab_id: int | None = None,
|
||||
) -> dict:
|
||||
"""Wait until a CSS selector appears (or disappears) in the DOM.
|
||||
|
||||
Args:
|
||||
selector: CSS selector to watch.
|
||||
timeout: Max seconds to wait before raising ``RuntimeError``.
|
||||
visible: Wait until the element has non-zero dimensions.
|
||||
hidden: Wait until the element is absent or has ``offsetParent == null``.
|
||||
tab_id: Tab to watch. Defaults to the active tab.
|
||||
"""
|
||||
return self._c._cmd("dom.wait_for", {
|
||||
"selector": selector,
|
||||
"timeout": int(timeout * 1000),
|
||||
"visible": visible,
|
||||
"hidden": hidden,
|
||||
"tabId": tab_id,
|
||||
})
|
||||
Args:
|
||||
selector: CSS selector to watch.
|
||||
timeout: Max seconds to wait before raising ``RuntimeError``.
|
||||
visible: Wait until the element has non-zero dimensions.
|
||||
hidden: Wait until the element is absent or has ``offsetParent == null``.
|
||||
tab_id: Tab to watch. Defaults to the active tab.
|
||||
"""
|
||||
return self.command("dom.wait_for", {
|
||||
"selector": selector,
|
||||
"timeout": int(timeout * 1000),
|
||||
"visible": visible,
|
||||
"hidden": hidden,
|
||||
"tabId": tab_id,
|
||||
})
|
||||
|
||||
class ExtractNS(Namespace):
|
||||
"""Extract structured content from the active tab."""
|
||||
"""Extract structured content from the active tab."""
|
||||
|
||||
def links(self) -> list[dict]:
|
||||
return self._c._cmd("extract.links", {}) or []
|
||||
@sdk_command("extract.links", default=[])
|
||||
def links(self) -> list[dict]:
|
||||
"""Return links from the active tab."""
|
||||
|
||||
def images(self) -> list[dict]:
|
||||
return self._c._cmd("extract.images", {}) or []
|
||||
@sdk_command("extract.images", default=[])
|
||||
def images(self) -> list[dict]:
|
||||
"""Return images from the active tab."""
|
||||
|
||||
def text(self) -> str:
|
||||
return self._c._cmd("extract.text", {}) or ""
|
||||
@sdk_command("extract.text", default="")
|
||||
def text(self) -> str:
|
||||
"""Return plain text from the active tab."""
|
||||
|
||||
def json(self, selector: str):
|
||||
return self._c._cmd("extract.json", {"selector": selector})
|
||||
@sdk_command("extract.json", lambda self, selector: {"selector": selector})
|
||||
def json(self, selector: str):
|
||||
"""Extract JSON-like structured data from a selector."""
|
||||
|
||||
def html(self) -> str:
|
||||
"""Return the full HTML source of the active tab."""
|
||||
return self._c._cmd("extract.html", {}) or ""
|
||||
@sdk_command("extract.html", default="")
|
||||
def html(self) -> str:
|
||||
"""Return the full HTML source of the active tab."""
|
||||
|
||||
def markdown(self, selector: str | None = None) -> str:
|
||||
"""Extract the page's main content as clean Markdown.
|
||||
@sdk_command("extract.markdown", lambda self, selector=None: {"selector": selector}, mapper=_extract_markdown)
|
||||
def markdown(self, selector: str | None = None) -> str:
|
||||
"""Extract the page's main content as clean Markdown.
|
||||
|
||||
The extractor may return either Markdown or raw HTML; both are
|
||||
normalized to Markdown here so SDK and CLI callers get identical output.
|
||||
"""
|
||||
from browser_cli.markdown import render_markdown
|
||||
|
||||
return render_markdown(self._c._cmd("extract.markdown", {"selector": selector}))
|
||||
The extractor may return either Markdown or raw HTML; both are
|
||||
normalized to Markdown here so SDK and CLI callers get identical output.
|
||||
"""
|
||||
|
||||
class PageNS(Namespace):
|
||||
"""Inspect the active page."""
|
||||
"""Inspect the active page."""
|
||||
|
||||
def info(self) -> dict:
|
||||
"""Return title, URL, readyState, lang, and meta tags of the active tab."""
|
||||
return self._c._cmd("page.info", {}) or {}
|
||||
@sdk_command("page.info", default={})
|
||||
def info(self) -> dict:
|
||||
"""Return title, URL, readyState, lang, and meta tags of the active tab."""
|
||||
|
||||
Reference in New Issue
Block a user