roveapi 1.0.0__py3-none-any.whl
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.
- roveapi/__init__.py +23 -0
- roveapi/client.py +161 -0
- roveapi/errors.py +37 -0
- roveapi/session.py +92 -0
- roveapi-1.0.0.dist-info/METADATA +72 -0
- roveapi-1.0.0.dist-info/RECORD +7 -0
- roveapi-1.0.0.dist-info/WHEEL +4 -0
roveapi/__init__.py
ADDED
|
@@ -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
|
+
]
|
roveapi/client.py
ADDED
|
@@ -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")
|
roveapi/errors.py
ADDED
|
@@ -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)
|
roveapi/session.py
ADDED
|
@@ -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")
|
|
@@ -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,7 @@
|
|
|
1
|
+
roveapi/__init__.py,sha256=yDehc7Y5fPhWA6U-YkVhFPmL6ZyLhPMQjXrWP1CdvKs,474
|
|
2
|
+
roveapi/client.py,sha256=kH1msLWFyXD8KoyhiqlDwAhgb-I6bQgKg2CqJq0Fi_U,5046
|
|
3
|
+
roveapi/errors.py,sha256=zRKrR0jrReczbwWywOdshG_d7H2xiDNvyKlHNMp3mns,1303
|
|
4
|
+
roveapi/session.py,sha256=NGOVAvo2ZoKviAMWIdE2M27AawPoe5Jtd0c09oQ8XzM,3842
|
|
5
|
+
roveapi-1.0.0.dist-info/METADATA,sha256=lNzw-9N8HOQ2PDwH26S1AOwqoF8GaxSaM2UAAe8vBg4,2163
|
|
6
|
+
roveapi-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
roveapi-1.0.0.dist-info/RECORD,,
|