roveapi 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ node_modules/
2
+ dist/
3
+ .env
4
+ .env.local
5
+ *.log
6
+ .DS_Store
7
+ .turbo
8
+ coverage/
roveapi-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: roveapi
3
+ Version: 1.0.0
4
+ Summary: Browser automation SDK for AI agents — Playwright-as-a-Service with accessibility trees
5
+ Project-URL: Homepage, https://roveapi.com
6
+ Project-URL: Documentation, https://roveapi.com/docs
7
+ Project-URL: Repository, https://github.com/roveapi/roveapi-python
8
+ License-Expression: MIT
9
+ Keywords: a11y,accessibility,ai-agent,automation,browser,mcp,playwright
10
+ Requires-Python: >=3.9
11
+ Requires-Dist: httpx>=0.27
12
+ Description-Content-Type: text/markdown
13
+
14
+ # roveapi
15
+
16
+ Python SDK for the [Rove](https://roveapi.com) browser automation API — Playwright-as-a-Service with accessibility trees for 77% fewer tokens.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pip install roveapi
22
+ ```
23
+
24
+ ## Quickstart
25
+
26
+ ```python
27
+ from roveapi import BrowserClient
28
+
29
+ client = BrowserClient(api_key="rvp_live_...")
30
+
31
+ # One-shot screenshot
32
+ screenshot = client.screenshot(url="https://example.com")
33
+ print(screenshot["url"]) # signed S3 URL
34
+
35
+ # Session with context manager (auto-close)
36
+ with client.session() as session:
37
+ session.navigate("https://example.com")
38
+ tree = session.get_a11y_tree()
39
+ print(tree["result"]["tree"]) # ~26K tokens vs ~114K for screenshot
40
+
41
+ session.click(label="Get Started")
42
+ session.fill(selector="#email", value="user@example.com")
43
+ ```
44
+
45
+ ## Error Handling
46
+
47
+ ```python
48
+ from roveapi import BrowserClient, InsufficientCreditsError, RateLimitError
49
+
50
+ try:
51
+ with client.session() as session:
52
+ session.navigate("https://example.com")
53
+ except InsufficientCreditsError:
54
+ print("Top up credits at roveapi.com")
55
+ except RateLimitError as e:
56
+ print(f"Retry after {e.retry_after} seconds")
57
+ ```
58
+
59
+ ## API
60
+
61
+ ### `BrowserClient(api_key, base_url?, retries?, retry_delay?)`
62
+
63
+ - `client.session(**options)` → `Session` (context manager)
64
+ - `client.screenshot(url, **options)` → dict
65
+ - `client.extract(url, schema, **options)` → dict
66
+ - `client.usage()` → dict
67
+
68
+ ### `Session`
69
+
70
+ All 12 browser actions: `navigate()`, `get_a11y_tree()`, `click()`, `fill()`, `select()`, `scroll()`, `wait_for()`, `screenshot()`, `get_text()`, `get_attribute()`, `evaluate()`, `close()`.
71
+
72
+ Auto-retry on 503 with exponential backoff. No retry on 402 or 410.
@@ -0,0 +1,59 @@
1
+ # roveapi
2
+
3
+ Python SDK for the [Rove](https://roveapi.com) browser automation API — Playwright-as-a-Service with accessibility trees for 77% fewer tokens.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install roveapi
9
+ ```
10
+
11
+ ## Quickstart
12
+
13
+ ```python
14
+ from roveapi import BrowserClient
15
+
16
+ client = BrowserClient(api_key="rvp_live_...")
17
+
18
+ # One-shot screenshot
19
+ screenshot = client.screenshot(url="https://example.com")
20
+ print(screenshot["url"]) # signed S3 URL
21
+
22
+ # Session with context manager (auto-close)
23
+ with client.session() as session:
24
+ session.navigate("https://example.com")
25
+ tree = session.get_a11y_tree()
26
+ print(tree["result"]["tree"]) # ~26K tokens vs ~114K for screenshot
27
+
28
+ session.click(label="Get Started")
29
+ session.fill(selector="#email", value="user@example.com")
30
+ ```
31
+
32
+ ## Error Handling
33
+
34
+ ```python
35
+ from roveapi import BrowserClient, InsufficientCreditsError, RateLimitError
36
+
37
+ try:
38
+ with client.session() as session:
39
+ session.navigate("https://example.com")
40
+ except InsufficientCreditsError:
41
+ print("Top up credits at roveapi.com")
42
+ except RateLimitError as e:
43
+ print(f"Retry after {e.retry_after} seconds")
44
+ ```
45
+
46
+ ## API
47
+
48
+ ### `BrowserClient(api_key, base_url?, retries?, retry_delay?)`
49
+
50
+ - `client.session(**options)` → `Session` (context manager)
51
+ - `client.screenshot(url, **options)` → dict
52
+ - `client.extract(url, schema, **options)` → dict
53
+ - `client.usage()` → dict
54
+
55
+ ### `Session`
56
+
57
+ All 12 browser actions: `navigate()`, `get_a11y_tree()`, `click()`, `fill()`, `select()`, `scroll()`, `wait_for()`, `screenshot()`, `get_text()`, `get_attribute()`, `evaluate()`, `close()`.
58
+
59
+ Auto-retry on 503 with exponential backoff. No retry on 402 or 410.
@@ -0,0 +1,18 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "roveapi"
7
+ version = "1.0.0"
8
+ description = "Browser automation SDK for AI agents — Playwright-as-a-Service with accessibility trees"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ dependencies = ["httpx>=0.27"]
13
+ keywords = ["browser", "automation", "playwright", "a11y", "accessibility", "mcp", "ai-agent"]
14
+
15
+ [project.urls]
16
+ Homepage = "https://roveapi.com"
17
+ Documentation = "https://roveapi.com/docs"
18
+ Repository = "https://github.com/roveapi/roveapi-python"
@@ -0,0 +1,23 @@
1
+ from .client import BrowserClient
2
+ from .session import Session
3
+ from .errors import (
4
+ RoveError,
5
+ InsufficientCreditsError,
6
+ SessionExpiredError,
7
+ RateLimitError,
8
+ BrowserCrashError,
9
+ PoolExhaustedError,
10
+ InvalidParamsError,
11
+ )
12
+
13
+ __all__ = [
14
+ "BrowserClient",
15
+ "Session",
16
+ "RoveError",
17
+ "InsufficientCreditsError",
18
+ "SessionExpiredError",
19
+ "RateLimitError",
20
+ "BrowserCrashError",
21
+ "PoolExhaustedError",
22
+ "InvalidParamsError",
23
+ ]
@@ -0,0 +1,161 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ import random
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+ from .errors import (
10
+ RoveError,
11
+ InsufficientCreditsError,
12
+ SessionExpiredError,
13
+ RateLimitError,
14
+ BrowserCrashError,
15
+ PoolExhaustedError,
16
+ InvalidParamsError,
17
+ )
18
+ from .session import Session
19
+
20
+
21
+ def _map_error(response: httpx.Response) -> RoveError:
22
+ try:
23
+ body = response.json()
24
+ except Exception:
25
+ body = {}
26
+
27
+ detail = body.get("detail", response.reason_phrase)
28
+ code = (body.get("type", "") or "").rsplit("/", 1)[-1] or "unknown"
29
+
30
+ match response.status_code:
31
+ case 400:
32
+ return InvalidParamsError(detail)
33
+ case 402:
34
+ return InsufficientCreditsError(detail)
35
+ case 410:
36
+ return SessionExpiredError(detail)
37
+ case 429:
38
+ retry_after = int(response.headers.get("retry-after", "60"))
39
+ return RateLimitError(retry_after, detail)
40
+ case 503:
41
+ if code == "pool-exhausted":
42
+ return PoolExhaustedError(detail)
43
+ return BrowserCrashError(detail)
44
+ case _:
45
+ return RoveError(detail, response.status_code, code, detail)
46
+
47
+
48
+ class BrowserClient:
49
+ """Rove browser automation client."""
50
+
51
+ def __init__(
52
+ self,
53
+ api_key: str,
54
+ base_url: str = "https://api.roveapi.com",
55
+ retries: int = 3,
56
+ retry_delay: float = 1.0,
57
+ ):
58
+ self.api_key = api_key
59
+ self.base_url = base_url
60
+ self.retries = retries
61
+ self.retry_delay = retry_delay
62
+ self._client = httpx.Client(
63
+ base_url=base_url,
64
+ headers={
65
+ "authorization": f"Bearer {api_key}",
66
+ "content-type": "application/json",
67
+ },
68
+ timeout=60.0,
69
+ )
70
+
71
+ def __enter__(self):
72
+ return self
73
+
74
+ def __exit__(self, *_: Any):
75
+ self._client.close()
76
+
77
+ def close(self):
78
+ self._client.close()
79
+
80
+ def _request(self, method: str, path: str, json: dict[str, Any] | None = None) -> dict[str, Any]:
81
+ last_error: Exception | None = None
82
+ for attempt in range(self.retries + 1):
83
+ try:
84
+ response = self._client.request(method, path, json=json)
85
+ if response.is_success:
86
+ return response.json()
87
+ error = _map_error(response)
88
+ if isinstance(error, (InsufficientCreditsError, SessionExpiredError, InvalidParamsError)):
89
+ raise error
90
+ raise error
91
+ except (InsufficientCreditsError, SessionExpiredError, InvalidParamsError):
92
+ raise
93
+ except Exception as e:
94
+ last_error = e
95
+ if attempt < self.retries:
96
+ jitter = random.uniform(0.85, 1.15)
97
+ delay = self.retry_delay * (2**attempt) * jitter
98
+ time.sleep(delay)
99
+ raise last_error # type: ignore[misc]
100
+
101
+ def session(
102
+ self,
103
+ *,
104
+ viewport: dict[str, int] | None = None,
105
+ record_video: bool = False,
106
+ locale: str | None = None,
107
+ timezone: str | None = None,
108
+ user_agent: str | None = None,
109
+ block_resources: list[str] | None = None,
110
+ ) -> Session:
111
+ """Create a persistent browser session."""
112
+ body: dict[str, Any] = {}
113
+ if viewport:
114
+ body["viewport"] = viewport
115
+ if record_video:
116
+ body["record_video"] = True
117
+ if locale:
118
+ body["locale"] = locale
119
+ if timezone:
120
+ body["timezone"] = timezone
121
+ if user_agent:
122
+ body["user_agent"] = user_agent
123
+ if block_resources:
124
+ body["block_resources"] = block_resources
125
+
126
+ data = self._request("POST", "/v1/browser/session", json=body)
127
+ return Session(data["session_id"], self)
128
+
129
+ def screenshot(
130
+ self,
131
+ url: str,
132
+ *,
133
+ selector: str | None = None,
134
+ full_page: bool = False,
135
+ format: str = "png",
136
+ viewport: dict[str, int] | None = None,
137
+ ) -> dict[str, Any]:
138
+ """One-shot screenshot (no persistent session)."""
139
+ body: dict[str, Any] = {"url": url, "full_page": full_page, "format": format}
140
+ if selector:
141
+ body["selector"] = selector
142
+ if viewport:
143
+ body["viewport"] = viewport
144
+ return self._request("POST", "/v1/browser/screenshot", json=body)
145
+
146
+ def extract(
147
+ self,
148
+ url: str,
149
+ schema: dict[str, str],
150
+ *,
151
+ wait_for_selector: str | None = None,
152
+ ) -> dict[str, Any]:
153
+ """One-shot structured data extraction."""
154
+ body: dict[str, Any] = {"url": url, "schema": schema}
155
+ if wait_for_selector:
156
+ body["wait_for_selector"] = wait_for_selector
157
+ return self._request("POST", "/v1/browser/extract", json=body)
158
+
159
+ def usage(self) -> dict[str, Any]:
160
+ """Get usage and credit balance."""
161
+ return self._request("GET", "/v1/account/usage")
@@ -0,0 +1,37 @@
1
+ class RoveError(Exception):
2
+ def __init__(self, message: str, status: int, code: str, detail: str | None = None):
3
+ super().__init__(message)
4
+ self.status = status
5
+ self.code = code
6
+ self.detail = detail
7
+
8
+
9
+ class InsufficientCreditsError(RoveError):
10
+ def __init__(self, detail: str | None = None):
11
+ super().__init__("Insufficient credits", 402, "insufficient-credits", detail)
12
+
13
+
14
+ class SessionExpiredError(RoveError):
15
+ def __init__(self, detail: str | None = None):
16
+ super().__init__("Session expired", 410, "session-expired", detail)
17
+
18
+
19
+ class RateLimitError(RoveError):
20
+ def __init__(self, retry_after: int, detail: str | None = None):
21
+ super().__init__("Rate limit exceeded", 429, "rate-limit-exceeded", detail)
22
+ self.retry_after = retry_after
23
+
24
+
25
+ class BrowserCrashError(RoveError):
26
+ def __init__(self, detail: str | None = None):
27
+ super().__init__("Browser crash", 503, "browser-crash", detail)
28
+
29
+
30
+ class PoolExhaustedError(RoveError):
31
+ def __init__(self, detail: str | None = None):
32
+ super().__init__("Pool exhausted", 503, "pool-exhausted", detail)
33
+
34
+
35
+ class InvalidParamsError(RoveError):
36
+ def __init__(self, detail: str | None = None):
37
+ super().__init__("Invalid parameters", 400, "invalid-params", detail)
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from .client import BrowserClient
7
+
8
+
9
+ class Session:
10
+ """A persistent browser session. Use as a context manager for auto-close."""
11
+
12
+ def __init__(self, session_id: str, client: BrowserClient):
13
+ self.id = session_id
14
+ self._client = client
15
+
16
+ def __enter__(self):
17
+ return self
18
+
19
+ def __exit__(self, *_: Any):
20
+ self.close()
21
+
22
+ def _action(self, action: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
23
+ return self._client._request("POST", "/v1/browser/action", json={
24
+ "session_id": self.id,
25
+ "action": action,
26
+ "params": params or {},
27
+ })
28
+
29
+ def navigate(self, url: str, *, wait_until: str = "networkidle", timeout_ms: int = 30000) -> dict[str, Any]:
30
+ return self._action("navigate", {"url": url, "wait_until": wait_until, "timeout_ms": timeout_ms})
31
+
32
+ def get_a11y_tree(self, *, include_hidden: bool = False, root_selector: str | None = None) -> dict[str, Any]:
33
+ params: dict[str, Any] = {"include_hidden": include_hidden}
34
+ if root_selector:
35
+ params["root_selector"] = root_selector
36
+ return self._action("get_a11y_tree", params)
37
+
38
+ def click(self, *, selector: str | None = None, label: str | None = None, button: str = "left") -> dict[str, Any]:
39
+ params: dict[str, Any] = {"button": button}
40
+ if selector:
41
+ params["selector"] = selector
42
+ if label:
43
+ params["label"] = label
44
+ return self._action("click", params)
45
+
46
+ def fill(self, *, selector: str | None = None, label: str | None = None, value: str, clear_first: bool = True) -> dict[str, Any]:
47
+ params: dict[str, Any] = {"value": value, "clear_first": clear_first}
48
+ if selector:
49
+ params["selector"] = selector
50
+ if label:
51
+ params["label"] = label
52
+ return self._action("fill", params)
53
+
54
+ def select(self, *, selector: str | None = None, value: str | None = None, label_text: str | None = None) -> dict[str, Any]:
55
+ params: dict[str, Any] = {}
56
+ if selector:
57
+ params["selector"] = selector
58
+ if value:
59
+ params["value"] = value
60
+ if label_text:
61
+ params["label_text"] = label_text
62
+ return self._action("select", params)
63
+
64
+ def scroll(self, *, direction: str = "down", amount: int = 500, selector: str | None = None) -> dict[str, Any]:
65
+ params: dict[str, Any] = {"direction": direction, "amount": amount}
66
+ if selector:
67
+ params["selector"] = selector
68
+ return self._action("scroll", params)
69
+
70
+ def wait_for(self, type: str, value: str, *, state: str | None = None, timeout_ms: int = 10000) -> dict[str, Any]:
71
+ params: dict[str, Any] = {"type": type, "value": value, "timeout_ms": timeout_ms}
72
+ if state:
73
+ params["state"] = state
74
+ return self._action("wait_for", params)
75
+
76
+ def screenshot(self, *, selector: str | None = None, full_page: bool = False, format: str = "png") -> dict[str, Any]:
77
+ params: dict[str, Any] = {"full_page": full_page, "format": format}
78
+ if selector:
79
+ params["selector"] = selector
80
+ return self._action("screenshot", params)
81
+
82
+ def get_text(self, selector: str, *, trim: bool = True) -> dict[str, Any]:
83
+ return self._action("get_text", {"selector": selector, "trim": trim})
84
+
85
+ def get_attribute(self, selector: str, attribute: str) -> dict[str, Any]:
86
+ return self._action("get_attribute", {"selector": selector, "attribute": attribute})
87
+
88
+ def evaluate(self, expression: str) -> dict[str, Any]:
89
+ return self._action("evaluate", {"expression": expression})
90
+
91
+ def close(self) -> dict[str, Any]:
92
+ return self._action("close_session")