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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user