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

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:
2026-06-11 13:58:41 +02:00
parent 0813ae2de9
commit fd5447cbb9
52 changed files with 3344 additions and 2348 deletions
+81 -69
View File
@@ -290,88 +290,100 @@ from browser_cli import BrowserCLI
b = BrowserCLI()
```
Every CLI command has a corresponding SDK method. The call blocks until the browser responds and returns the data directly as a Python object.
Commands are grouped into namespaces on the client (`b.tabs`, `b.dom`, `b.session`, ...). Each call blocks until the browser responds and returns the data directly as a Python object.
```python
# Navigation
b.open("https://example.com")
tab = b.open_tab("https://example.com") # returns a bound Tab object
tab = b.open_tab("https://example.com", wait=True, timeout=10)
b.open("https://example.com", background=True)
b.open("https://example.com", window="work")
b.reload()
b.hard_reload()
b.back()
b.forward(tab_id=1234)
b.focus_url("github")
# Navigation ── b.nav
b.nav.open("https://example.com")
b.nav.open("https://example.com", background=True)
b.nav.open("https://example.com", window="work")
b.nav.reload()
b.nav.hard_reload()
b.nav.back()
b.nav.forward(tab_id=1234)
b.nav.focus("github")
b.nav.to(1234, "https://example.com") # navigate a specific tab in place
b.nav.search("google", "python asyncio")
# Tabs
tabs = b.tabs_list() # list[Tab]; in multi-browser mode each tab.browser is set
tabs = b.tabs() # short alias for tabs_list()
active = b.active_tab() # active Tab object
tab = b.tab(1234) # tab by ID
tab = b.find_tab("github") # first matching tab or None
tabs = b.find_tabs("github") # alias for tabs_query()
b.tabs_active(1234)
b.tabs_close(1234)
b.close_tab(tab) # accepts Tab or tab ID
b.tabs_close_inactive()
b.tabs_close_duplicates()
b.tabs_filter("youtube") # list of matching tabs
b.tabs_query("pull request")
counts = b.tabs_count("github") # int, or BrowserCounts(total=..., by_browser=...) in multi-browser mode
html = b.tabs_html() # full HTML string of active tab
b.tabs_sort(by="domain")
b.tabs_merge_windows()
b.tabs_dedupe()
# Tabs ── b.tabs
tabs = b.tabs.list() # list[Tab]; in multi-browser mode each tab.browser is set
tab = b.tabs.open("https://example.com") # returns a bound Tab object
tab = b.tabs.open("https://example.com", wait=True, timeout=10)
active = b.tabs.active() # active Tab object
tab = b.tabs.get(1234) # tab by ID
tab = b.tabs.first("github") # first matching tab or None
b.tabs.activate(1234)
b.tabs.close(1234)
b.tabs.close(tab_ids=[1, 2, 3]) # close many in one round-trip (IDs or Tab objects)
b.tabs.close_inactive()
b.tabs.close_duplicates()
b.tabs.filter("youtube") # list of matching tabs
b.tabs.query("pull request")
counts = b.tabs.count("github") # int, or BrowserCounts(total=..., by_browser=...) in multi-browser mode
html = b.tabs.html() # full HTML string of active tab
b.tabs.sort(by="domain")
b.tabs.merge_windows()
b.tabs.dedupe()
# Bound Tab helpers
tab = b.active_tab()
tab = b.tabs.active()
tab.pin()
tab.screenshot()
tab.refresh()
tab.wait_for_load(timeout=10)
tab.watch_url(r"/done$")
# Tab groups
groups = b.group_list() # list[Group]; in multi-browser mode each group.browser is set
groups = b.groups() # short alias for group_list()
b.groups_create("research") # plural alias for group_create()
b.group_create("research") # creates group, returns Group
b.group_close(42)
b.group_tabs(42) # tabs inside a group
b.group_count() # int, or BrowserCounts(...) in multi-browser mode
# Tab groups ── b.groups
groups = b.groups.list() # list[Group]; in multi-browser mode each group.browser is set
b.groups.create("research") # creates group, returns Group
b.groups.close(42)
b.groups.tabs(42) # tabs inside a group
b.groups.add_tab(42, "https://example.com")
b.groups.count() # int, or BrowserCounts(...) in multi-browser mode
# Windows
windows = b.windows_list() # in multi-browser mode each dict has a "browser" key
b.windows_rename(1, "work")
b.windows_open()
b.windows_open("https://example.com")
b.windows_close(1)
# Windows ── b.windows
windows = b.windows.list() # in multi-browser mode each dict has a "browser" key
b.windows.rename(1, "work")
b.windows.open()
b.windows.open("https://example.com")
b.windows.close(1)
# DOM (active tab must be http/https)
elements = b.dom_query("h2") # list of { tag, text, attrs }
texts = b.dom_text(".article p") # list of strings
attrs = b.dom_attr("a", "href") # list of strings
exists = b.dom_exists(".cookie-banner")# bool
b.dom_click(".accept-button")
b.dom_type("#search", "hello world")
b.wait_for_selector("#results", visible=True, timeout=10)
# DOM ── b.dom (active tab must be http/https)
elements = b.dom.query("h2") # list of { tag, text, attrs }
texts = b.dom.text(".article p") # list of strings
attrs = b.dom.attr("a", "href") # list of strings
exists = b.dom.exists(".cookie-banner")# bool
b.dom.click(".accept-button")
b.dom.type("#search", "hello world")
b.dom.wait_for("#results", visible=True, timeout=10)
b.dom.eval("document.title")
# Extract
links = b.extract_links() # list of { text, href }
images = b.extract_images() # list of { alt, src }
text = b.extract_text() # string
data = b.extract_json("#app-data") # parsed Python object
# Extract ── b.extract
links = b.extract.links() # list of { text, href }
images = b.extract.images() # list of { alt, src }
text = b.extract.text() # string
data = b.extract.json("#app-data") # parsed Python object
md = b.extract.markdown("article")
# Sessions
b.session_save("before-meeting")
b.session_load("before-meeting")
sessions = b.session_list() # [{ name, tabs, savedAt }, ...]
b.session_remove("before-meeting")
diff = b.session_diff("session-a", "session-b")
# Page / storage / cookies
info = b.page.info()
b.storage.set("token", "abc")
val = b.storage.get("token")
cookies = b.cookies.list(domain="example.com")
# Sessions ── b.session
b.session.save("before-meeting")
b.session.load("before-meeting")
sessions = b.session.list() # [{ name, tabs, savedAt }, ...]
b.session.remove("before-meeting")
diff = b.session.diff("session-a", "session-b")
# diff = { "added": [...urls], "removed": [...urls] }
b.session_auto_save(True)
b.session.auto_save(True)
# Performance + extension
b.perf.status()
b.perf.set_profile("gentle")
b.extension.reload()
# Misc
clients = b.clients()
@@ -385,7 +397,7 @@ from browser_cli import BrowserCLI, BrowserNotConnected
b = BrowserCLI()
try:
tabs = b.tabs_list()
tabs = b.tabs.list()
except BrowserNotConnected:
print("Browser is not running or extension is not loaded")
except RuntimeError as e:
@@ -397,11 +409,11 @@ from browser_cli import BrowserCLI, BrowserCounts
b = BrowserCLI()
tabs = b.tabs_list()
tabs = b.tabs.list()
for tab in tabs:
print(tab.browser, tab.title)
counts = b.tabs_count()
counts = b.tabs.count()
if isinstance(counts, BrowserCounts):
print(counts.total)
print(counts.by_browser)