devicebase 2026.4.1__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.
- devicebase/__init__.py +44 -0
- devicebase/client.py +372 -0
- devicebase/http_client.py +374 -0
- devicebase/models.py +151 -0
- devicebase/websocket_client.py +401 -0
- devicebase-2026.4.1.dist-info/METADATA +224 -0
- devicebase-2026.4.1.dist-info/RECORD +9 -0
- devicebase-2026.4.1.dist-info/WHEEL +4 -0
- devicebase-2026.4.1.dist-info/licenses/LICENSE +21 -0
devicebase/__init__.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""DeviceBase Python SDK for TestClaw device automation."""
|
|
2
|
+
|
|
3
|
+
from devicebase.client import DeviceBaseClient
|
|
4
|
+
from devicebase.http_client import (
|
|
5
|
+
AuthenticationError,
|
|
6
|
+
DeviceBaseError,
|
|
7
|
+
DeviceNotFoundError,
|
|
8
|
+
ValidationError,
|
|
9
|
+
)
|
|
10
|
+
from devicebase.models import (
|
|
11
|
+
AppInfo,
|
|
12
|
+
Bounds,
|
|
13
|
+
DeviceInfo,
|
|
14
|
+
HierarchyInfo,
|
|
15
|
+
InputTextRequest,
|
|
16
|
+
LaunchAppRequest,
|
|
17
|
+
OperationResult,
|
|
18
|
+
Point,
|
|
19
|
+
)
|
|
20
|
+
from devicebase.websocket_client import MinicapClient, MinitouchClient
|
|
21
|
+
|
|
22
|
+
__version__ = "2026.4.1"
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
# Main client
|
|
26
|
+
"DeviceBaseClient",
|
|
27
|
+
# Exceptions
|
|
28
|
+
"AuthenticationError",
|
|
29
|
+
"DeviceBaseError",
|
|
30
|
+
"DeviceNotFoundError",
|
|
31
|
+
"ValidationError",
|
|
32
|
+
# Models
|
|
33
|
+
"AppInfo",
|
|
34
|
+
"Bounds",
|
|
35
|
+
"DeviceInfo",
|
|
36
|
+
"HierarchyInfo",
|
|
37
|
+
"InputTextRequest",
|
|
38
|
+
"LaunchAppRequest",
|
|
39
|
+
"OperationResult",
|
|
40
|
+
"Point",
|
|
41
|
+
# WebSocket clients
|
|
42
|
+
"MinicapClient",
|
|
43
|
+
"MinitouchClient",
|
|
44
|
+
]
|
devicebase/client.py
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""Main DeviceBase client providing high-level access to all device operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
7
|
+
|
|
8
|
+
from devicebase.http_client import (
|
|
9
|
+
AuthenticationError,
|
|
10
|
+
DeviceBaseHttpClient,
|
|
11
|
+
)
|
|
12
|
+
from devicebase.models import (
|
|
13
|
+
AppInfo,
|
|
14
|
+
Bounds,
|
|
15
|
+
DeviceInfo,
|
|
16
|
+
HierarchyInfo,
|
|
17
|
+
OperationResult,
|
|
18
|
+
Point,
|
|
19
|
+
)
|
|
20
|
+
from devicebase.websocket_client import MinicapClient, MinitouchClient
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DeviceBaseClient:
|
|
24
|
+
"""Main client for interacting with the DeviceBase API.
|
|
25
|
+
|
|
26
|
+
This client provides a unified interface for all device automation operations,
|
|
27
|
+
including HTTP-based device control and WebSocket-based streaming and touch control.
|
|
28
|
+
|
|
29
|
+
Configuration can be provided via constructor parameters or environment variables:
|
|
30
|
+
- DEVICEBASE_BASE_URL: API base URL (default: https://api.devicebase.cn)
|
|
31
|
+
- DEVICEBASE_API_KEY: JWT API key for authentication
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
```python
|
|
35
|
+
import asyncio
|
|
36
|
+
from devicebase import DeviceBaseClient
|
|
37
|
+
|
|
38
|
+
# Using environment variables
|
|
39
|
+
client = DeviceBaseClient()
|
|
40
|
+
|
|
41
|
+
# Or with explicit configuration
|
|
42
|
+
client = DeviceBaseClient(
|
|
43
|
+
base_url="https://api.devicebase.cn",
|
|
44
|
+
api_key="your-jwt-token"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Get device info
|
|
48
|
+
info = client.get_device_info("device123")
|
|
49
|
+
|
|
50
|
+
# Control the device
|
|
51
|
+
client.tap("device123", Point(x=100, y=200))
|
|
52
|
+
client.launch_app("device123", "com.example.app")
|
|
53
|
+
|
|
54
|
+
# WebSocket streaming (async)
|
|
55
|
+
async def stream_screen():
|
|
56
|
+
async for frame in client.stream_minicap("device123"):
|
|
57
|
+
# Process JPEG frame
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
asyncio.run(stream_screen())
|
|
61
|
+
```
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
DEFAULT_BASE_URL = "https://api.devicebase.cn"
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
base_url: str | None = None,
|
|
69
|
+
api_key: str | None = None,
|
|
70
|
+
timeout: float = 30.0,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Initialize the DeviceBase client.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
base_url: The base URL of the DeviceBase API. If not provided,
|
|
76
|
+
reads from DEVICEBASE_BASE_URL environment variable,
|
|
77
|
+
defaults to https://api.devicebase.cn.
|
|
78
|
+
api_key: JWT API key for authentication. If not provided,
|
|
79
|
+
reads from DEVICEBASE_API_KEY environment variable.
|
|
80
|
+
timeout: Request timeout in seconds for HTTP operations.
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
AuthenticationError: If no API key is provided via parameter
|
|
84
|
+
or environment variable.
|
|
85
|
+
"""
|
|
86
|
+
self._base_url = base_url or os.environ.get(
|
|
87
|
+
"DEVICEBASE_BASE_URL", self.DEFAULT_BASE_URL
|
|
88
|
+
)
|
|
89
|
+
self._api_key = api_key or os.environ.get("DEVICEBASE_API_KEY")
|
|
90
|
+
|
|
91
|
+
if not self._api_key:
|
|
92
|
+
raise AuthenticationError(
|
|
93
|
+
"API key is required. Provide it via 'api_key' parameter "
|
|
94
|
+
"or DEVICEBASE_API_KEY environment variable."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
self._http = DeviceBaseHttpClient(
|
|
98
|
+
base_url=self._base_url,
|
|
99
|
+
api_key=self._api_key,
|
|
100
|
+
timeout=timeout,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def close(self) -> None:
|
|
104
|
+
"""Close the client and release all resources."""
|
|
105
|
+
self._http.close()
|
|
106
|
+
|
|
107
|
+
def __enter__(self) -> DeviceBaseClient:
|
|
108
|
+
"""Context manager entry."""
|
|
109
|
+
return self
|
|
110
|
+
|
|
111
|
+
def __exit__(self, *args: object) -> None:
|
|
112
|
+
"""Context manager exit."""
|
|
113
|
+
self.close()
|
|
114
|
+
|
|
115
|
+
# Device Info
|
|
116
|
+
def get_device_info(self, serial: str) -> DeviceInfo:
|
|
117
|
+
"""Get detailed information about a device.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
serial: The device unique identifier.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
DeviceInfo containing device status, hardware info, and connection state.
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
DeviceNotFoundError: If the device is not found or not connected.
|
|
127
|
+
ValidationError: If the serial is invalid.
|
|
128
|
+
"""
|
|
129
|
+
return self._http.get_device_info(serial)
|
|
130
|
+
|
|
131
|
+
# Touch Operations
|
|
132
|
+
def tap(self, serial: str, x: int, y: int) -> OperationResult:
|
|
133
|
+
"""Perform a single tap at the specified coordinates.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
serial: The device unique identifier.
|
|
137
|
+
x: Horizontal coordinate (pixels from left).
|
|
138
|
+
y: Vertical coordinate (pixels from top).
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
OperationResult indicating success or failure.
|
|
142
|
+
"""
|
|
143
|
+
return self._http.tap(serial, Point(x=x, y=y))
|
|
144
|
+
|
|
145
|
+
def double_tap(self, serial: str, x: int, y: int) -> OperationResult:
|
|
146
|
+
"""Perform a double tap at the specified coordinates.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
serial: The device unique identifier.
|
|
150
|
+
x: Horizontal coordinate.
|
|
151
|
+
y: Vertical coordinate.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
OperationResult indicating success or failure.
|
|
155
|
+
"""
|
|
156
|
+
return self._http.double_tap(serial, Point(x=x, y=y))
|
|
157
|
+
|
|
158
|
+
def long_press(self, serial: str, x: int, y: int) -> OperationResult:
|
|
159
|
+
"""Perform a long press at the specified coordinates.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
serial: The device unique identifier.
|
|
163
|
+
x: Horizontal coordinate.
|
|
164
|
+
y: Vertical coordinate.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
OperationResult indicating success or failure.
|
|
168
|
+
"""
|
|
169
|
+
return self._http.long_press(serial, Point(x=x, y=y))
|
|
170
|
+
|
|
171
|
+
def swipe(
|
|
172
|
+
self, serial: str, x1: int, y1: int, x2: int, y2: int
|
|
173
|
+
) -> OperationResult:
|
|
174
|
+
"""Perform a swipe gesture from start to end coordinates.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
serial: The device unique identifier.
|
|
178
|
+
x1: Starting X coordinate.
|
|
179
|
+
y1: Starting Y coordinate.
|
|
180
|
+
x2: Ending X coordinate.
|
|
181
|
+
y2: Ending Y coordinate.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
OperationResult indicating success or failure.
|
|
185
|
+
"""
|
|
186
|
+
return self._http.swipe(serial, Bounds(x1=x1, y1=y1, x2=x2, y2=y2))
|
|
187
|
+
|
|
188
|
+
# Navigation
|
|
189
|
+
def back(self, serial: str) -> OperationResult:
|
|
190
|
+
"""Simulate the device back button press.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
serial: The device unique identifier.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
OperationResult indicating success or failure.
|
|
197
|
+
"""
|
|
198
|
+
return self._http.back(serial)
|
|
199
|
+
|
|
200
|
+
def home(self, serial: str) -> OperationResult:
|
|
201
|
+
"""Simulate the device home button press.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
serial: The device unique identifier.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
OperationResult indicating success or failure.
|
|
208
|
+
"""
|
|
209
|
+
return self._http.home(serial)
|
|
210
|
+
|
|
211
|
+
# App Operations
|
|
212
|
+
def launch_app(self, serial: str, app_name: str) -> OperationResult:
|
|
213
|
+
"""Launch an application on the device.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
serial: The device unique identifier.
|
|
217
|
+
app_name: The package name or identifier of the app to launch.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
OperationResult indicating success or failure.
|
|
221
|
+
"""
|
|
222
|
+
return self._http.launch_app(serial, app_name)
|
|
223
|
+
|
|
224
|
+
def get_current_app(self, serial: str) -> AppInfo:
|
|
225
|
+
"""Get information about the currently running foreground app.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
serial: The device unique identifier.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
AppInfo containing the current app name and details.
|
|
232
|
+
"""
|
|
233
|
+
return self._http.get_current_app(serial)
|
|
234
|
+
|
|
235
|
+
# Text Input
|
|
236
|
+
def input_text(self, serial: str, text: str) -> OperationResult:
|
|
237
|
+
"""Input text into the currently focused field.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
serial: The device unique identifier.
|
|
241
|
+
text: The text to input.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
OperationResult indicating success or failure.
|
|
245
|
+
"""
|
|
246
|
+
return self._http.input_text(serial, text)
|
|
247
|
+
|
|
248
|
+
def clear_text(self, serial: str) -> OperationResult:
|
|
249
|
+
"""Clear text in the currently focused field.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
serial: The device unique identifier.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
OperationResult indicating success or failure.
|
|
256
|
+
"""
|
|
257
|
+
return self._http.clear_text(serial)
|
|
258
|
+
|
|
259
|
+
# UI Hierarchy
|
|
260
|
+
def dump_hierarchy(self, serial: str) -> HierarchyInfo:
|
|
261
|
+
"""Get the current UI hierarchy structure.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
serial: The device unique identifier.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
HierarchyInfo containing the UI element tree.
|
|
268
|
+
"""
|
|
269
|
+
return self._http.dump_hierarchy(serial)
|
|
270
|
+
|
|
271
|
+
# Screenshots
|
|
272
|
+
def get_screenshot(self, serial: str) -> bytes:
|
|
273
|
+
"""Get a screenshot of the device screen as JPEG bytes.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
serial: The device unique identifier.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Raw JPEG image bytes.
|
|
280
|
+
|
|
281
|
+
Raises:
|
|
282
|
+
DeviceNotFoundError: If the device is not found.
|
|
283
|
+
"""
|
|
284
|
+
return self._http.get_screenshot(serial)
|
|
285
|
+
|
|
286
|
+
def download_screenshot(self, serial: str) -> bytes:
|
|
287
|
+
"""Download screenshot as a file attachment.
|
|
288
|
+
|
|
289
|
+
The filename will be {serial}_screenshot.jpg.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
serial: The device unique identifier.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Raw JPEG image bytes.
|
|
296
|
+
"""
|
|
297
|
+
return self._http.download_screenshot(serial)
|
|
298
|
+
|
|
299
|
+
# WebSocket Clients
|
|
300
|
+
def minicap_client(self, serial: str) -> MinicapClient:
|
|
301
|
+
"""Create a minicap WebSocket client for screen streaming.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
serial: The device unique identifier.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
MinicapClient configured for the device.
|
|
308
|
+
|
|
309
|
+
Example:
|
|
310
|
+
```python
|
|
311
|
+
client = DeviceBaseClient()
|
|
312
|
+
minicap = client.minicap_client("device123")
|
|
313
|
+
|
|
314
|
+
async for frame in minicap.stream_frames():
|
|
315
|
+
# Process JPEG frame
|
|
316
|
+
pass
|
|
317
|
+
```
|
|
318
|
+
"""
|
|
319
|
+
return MinicapClient(
|
|
320
|
+
base_url=self._base_url,
|
|
321
|
+
serial=serial,
|
|
322
|
+
api_key=self._api_key,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
def minitouch_client(self, serial: str) -> MinitouchClient:
|
|
326
|
+
"""Create a minitouch WebSocket client for touch control.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
serial: The device unique identifier.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
MinitouchClient configured for the device.
|
|
333
|
+
|
|
334
|
+
Example:
|
|
335
|
+
```python
|
|
336
|
+
client = DeviceBaseClient()
|
|
337
|
+
minitouch = client.minitouch_client("device123")
|
|
338
|
+
|
|
339
|
+
async with minitouch:
|
|
340
|
+
await minitouch.tap(100, 200)
|
|
341
|
+
```
|
|
342
|
+
"""
|
|
343
|
+
return MinitouchClient(
|
|
344
|
+
base_url=self._base_url,
|
|
345
|
+
serial=serial,
|
|
346
|
+
api_key=self._api_key,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# Async streaming convenience methods
|
|
350
|
+
def stream_minicap(self, serial: str) -> AsyncIterator[bytes]:
|
|
351
|
+
"""Stream JPEG frames from the device screen.
|
|
352
|
+
|
|
353
|
+
This is a convenience method that creates a minicap client
|
|
354
|
+
and yields frames from its stream.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
serial: The device unique identifier.
|
|
358
|
+
|
|
359
|
+
Yields:
|
|
360
|
+
JPEG image bytes for each frame.
|
|
361
|
+
|
|
362
|
+
Example:
|
|
363
|
+
```python
|
|
364
|
+
client = DeviceBaseClient()
|
|
365
|
+
|
|
366
|
+
async for frame in client.stream_minicap("device123"):
|
|
367
|
+
with open("frame.jpg", "wb") as f:
|
|
368
|
+
f.write(frame)
|
|
369
|
+
```
|
|
370
|
+
"""
|
|
371
|
+
client = self.minicap_client(serial)
|
|
372
|
+
return client.stream_frames()
|