mobilerun-core 0.3.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.
- mobilerun_core-0.3.0/.github/workflows/ci.yml +36 -0
- mobilerun_core-0.3.0/.github/workflows/publish.yml +90 -0
- mobilerun_core-0.3.0/.gitignore +26 -0
- mobilerun_core-0.3.0/PKG-INFO +239 -0
- mobilerun_core-0.3.0/README.md +214 -0
- mobilerun_core-0.3.0/mobilerun_core/__init__.py +14 -0
- mobilerun_core-0.3.0/mobilerun_core/_a11y.py +197 -0
- mobilerun_core-0.3.0/mobilerun_core/_utils.py +12 -0
- mobilerun_core-0.3.0/mobilerun_core/connection/__init__.py +107 -0
- mobilerun_core-0.3.0/mobilerun_core/connection/_unsupported.py +141 -0
- mobilerun_core-0.3.0/mobilerun_core/connection/cloud.py +194 -0
- mobilerun_core-0.3.0/mobilerun_core/connection/framework.py +96 -0
- mobilerun_core-0.3.0/mobilerun_core/detect.py +89 -0
- mobilerun_core-0.3.0/mobilerun_core/device.py +447 -0
- mobilerun_core-0.3.0/mobilerun_core/hitl.py +25 -0
- mobilerun_core-0.3.0/mobilerun_core/mobilerun.py +217 -0
- mobilerun_core-0.3.0/mobilerun_core/sync.py +168 -0
- mobilerun_core-0.3.0/pyproject.toml +41 -0
- mobilerun_core-0.3.0/tests/test_abstraction.py +264 -0
- mobilerun_core-0.3.0/tests/test_integration.py +71 -0
- mobilerun_core-0.3.0/uv.lock +238 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v3
|
|
21
|
+
|
|
22
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
23
|
+
run: uv python install ${{ matrix.python-version }}
|
|
24
|
+
|
|
25
|
+
- name: Create venv and install
|
|
26
|
+
run: |
|
|
27
|
+
uv venv --python ${{ matrix.python-version }}
|
|
28
|
+
uv pip install -e . pytest
|
|
29
|
+
|
|
30
|
+
- name: Run tests
|
|
31
|
+
run: .venv/bin/python -m pytest tests/test_abstraction.py -v
|
|
32
|
+
|
|
33
|
+
- name: Lint with ruff
|
|
34
|
+
run: |
|
|
35
|
+
uv pip install ruff
|
|
36
|
+
.venv/bin/ruff check mobilerun_core tests
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*.*.*"
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
inputs:
|
|
9
|
+
target:
|
|
10
|
+
description: "Where to publish"
|
|
11
|
+
required: true
|
|
12
|
+
default: "pypi"
|
|
13
|
+
type: choice
|
|
14
|
+
options: [pypi, testpypi]
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
build:
|
|
18
|
+
name: Build distribution
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- name: Install uv
|
|
24
|
+
uses: astral-sh/setup-uv@v3
|
|
25
|
+
|
|
26
|
+
- name: Set up Python
|
|
27
|
+
run: uv python install 3.12
|
|
28
|
+
|
|
29
|
+
- name: Verify tag matches pyproject version
|
|
30
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
31
|
+
run: |
|
|
32
|
+
TAG="${GITHUB_REF_NAME#v}"
|
|
33
|
+
VER=$(uv run --with tomli python -c "import tomli; print(tomli.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
34
|
+
if [ "$TAG" != "$VER" ]; then
|
|
35
|
+
echo "::error::Tag v$TAG does not match pyproject version $VER"
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
echo "tag and pyproject agree on $VER"
|
|
39
|
+
|
|
40
|
+
- name: Build sdist + wheel
|
|
41
|
+
run: uv build
|
|
42
|
+
|
|
43
|
+
- name: Show built artifacts
|
|
44
|
+
run: ls -la dist/
|
|
45
|
+
|
|
46
|
+
- uses: actions/upload-artifact@v4
|
|
47
|
+
with:
|
|
48
|
+
name: dist
|
|
49
|
+
path: dist/
|
|
50
|
+
if-no-files-found: error
|
|
51
|
+
|
|
52
|
+
publish-pypi:
|
|
53
|
+
name: Publish to PyPI
|
|
54
|
+
needs: build
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.target == 'pypi')
|
|
57
|
+
environment:
|
|
58
|
+
name: pypi
|
|
59
|
+
url: https://pypi.org/p/mobilerun-core
|
|
60
|
+
permissions:
|
|
61
|
+
id-token: write # required for trusted publishing
|
|
62
|
+
steps:
|
|
63
|
+
- uses: actions/download-artifact@v4
|
|
64
|
+
with:
|
|
65
|
+
name: dist
|
|
66
|
+
path: dist/
|
|
67
|
+
|
|
68
|
+
- name: Publish via Trusted Publishing
|
|
69
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
70
|
+
|
|
71
|
+
publish-testpypi:
|
|
72
|
+
name: Publish to TestPyPI
|
|
73
|
+
needs: build
|
|
74
|
+
runs-on: ubuntu-latest
|
|
75
|
+
if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi'
|
|
76
|
+
environment:
|
|
77
|
+
name: testpypi
|
|
78
|
+
url: https://test.pypi.org/p/mobilerun-core
|
|
79
|
+
permissions:
|
|
80
|
+
id-token: write
|
|
81
|
+
steps:
|
|
82
|
+
- uses: actions/download-artifact@v4
|
|
83
|
+
with:
|
|
84
|
+
name: dist
|
|
85
|
+
path: dist/
|
|
86
|
+
|
|
87
|
+
- name: Publish to TestPyPI
|
|
88
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
89
|
+
with:
|
|
90
|
+
repository-url: https://test.pypi.org/legacy/
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Secrets — never commit
|
|
2
|
+
.env
|
|
3
|
+
.env.*
|
|
4
|
+
!.env.example
|
|
5
|
+
|
|
6
|
+
# Python
|
|
7
|
+
__pycache__/
|
|
8
|
+
*.pyc
|
|
9
|
+
*.pyo
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.mypy_cache/
|
|
12
|
+
.ruff_cache/
|
|
13
|
+
.coverage
|
|
14
|
+
htmlcov/
|
|
15
|
+
|
|
16
|
+
# Virtualenvs / build artifacts
|
|
17
|
+
.venv/
|
|
18
|
+
venv/
|
|
19
|
+
build/
|
|
20
|
+
dist/
|
|
21
|
+
*.egg-info/
|
|
22
|
+
|
|
23
|
+
# Editor / OS noise
|
|
24
|
+
.DS_Store
|
|
25
|
+
.idea/
|
|
26
|
+
.vscode/
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mobilerun-core
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Unified Python facade for mobilerun cloud and local device connections, providing high-level automation actions, human-in-the-loop approval hooks for destructive operations, and structured capability-aware error handling.
|
|
5
|
+
Project-URL: Homepage, https://github.com/droidrun/mobilerun-core
|
|
6
|
+
Project-URL: Repository, https://github.com/droidrun/mobilerun-core
|
|
7
|
+
Project-URL: Issues, https://github.com/droidrun/mobilerun-core/issues
|
|
8
|
+
Author: droidrun
|
|
9
|
+
License: Apache-2.0
|
|
10
|
+
Keywords: adb,android,automation,device,ios,mobile,ui-automation
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Classifier: Topic :: Software Development :: Testing
|
|
21
|
+
Requires-Python: <3.14,>=3.11
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
23
|
+
Requires-Dist: mobilerun-sdk>=3.1.0
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
<picture align="center">
|
|
27
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/droidrun/mobilerun/main/static/mobilerun-dark.png">
|
|
28
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/droidrun/mobilerun/main/static/mobilerun.png">
|
|
29
|
+
<img src="https://raw.githubusercontent.com/droidrun/mobilerun/main/static/mobilerun.png" width="full">
|
|
30
|
+
</picture>
|
|
31
|
+
|
|
32
|
+
<p align="center">
|
|
33
|
+
<strong>mobilerun-core is the programmatic Python API behind Mobilerun.</strong><br>
|
|
34
|
+
One sync facade — `Mobilerun()` — for driving Android (and iOS, on cloud) devices, whether they live in the Mobilerun cloud or on a USB cable. Pick a device id, get a `Device`, call <code>tap_text</code>, <code>scroll_until</code>, <code>wait_for_app</code>. No async/await ceremony, no SDK juggling.
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
<div align="center">
|
|
38
|
+
|
|
39
|
+
<a href="https://docs.mobilerun.ai">📕 Documentation</a>
|
|
40
|
+
·
|
|
41
|
+
<a href="https://cloud.mobilerun.ai">☁️ Mobilerun Cloud</a>
|
|
42
|
+
·
|
|
43
|
+
<a href="https://github.com/droidrun/mobilerun">🤖 Mobilerun Framework</a>
|
|
44
|
+
|
|
45
|
+
[](https://github.com/droidrun/mobilerun-core/stargazers)
|
|
46
|
+
[](https://pypi.org/project/mobilerun-core/)
|
|
47
|
+
[](https://pypi.org/project/mobilerun-core/)
|
|
48
|
+
[](https://mobilerun.ai)
|
|
49
|
+
[](https://x.com/mobilerun_ai)
|
|
50
|
+
[](https://discord.gg/ZZbKEZZkwK)
|
|
51
|
+
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
- 🧰 **One API for two transports** — same helpers run against a cloud device (over `mobilerun-sdk`) or a local Android phone (over `mobilerun-core-cli`).
|
|
55
|
+
- 🪄 **Auto-detection** — pass any device id, the library figures out cloud vs local. Or override explicitly.
|
|
56
|
+
- 🎯 **High-level helpers** — `tap_text`, `tap_node`, `scroll_until`, `wait_for_app`, `open_and_settle`, `find_nodes`, `assert_on`, `screen_size`, … built on top of the raw verbs.
|
|
57
|
+
- 🛡️ **HITL gate** — destructive verbs (`uninstall`, local `install_apk`) route through a callable you control. Default denies; you opt in per turn.
|
|
58
|
+
- 🧭 **Agent-friendly errors** — when a backend doesn't support a verb, you get a structured `UnsupportedOperation` with `verb`, `backend`, and an `alternative` field instead of an opaque traceback.
|
|
59
|
+
- 🪶 **Pure library** — sync, heredoc-friendly, no daemon, no server, no agent loop.
|
|
60
|
+
|
|
61
|
+
Use the library when you want to script a device directly from Python — locally or in the cloud. Use [Mobilerun Framework](https://github.com/droidrun/mobilerun) when you want a full LLM agent driving the device. Use [Mobilerun Cloud](https://cloud.mobilerun.ai) when you want hosted devices and managed infrastructure.
|
|
62
|
+
|
|
63
|
+
## 📦 Installation
|
|
64
|
+
|
|
65
|
+
> **Note:** Python 3.14 is not currently supported. Please use Python `>=3.11,<3.14`.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# cloud-only is enough to start
|
|
69
|
+
uv add mobilerun-core
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# add local-Android (ADB) support
|
|
74
|
+
uv add mobilerun-core mobilerun-core-cli
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Cloud credentials are picked up lazily — `Mobilerun()` does not touch the environment until you make a cloud call. For local-only use, no env vars are required.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# only needed for cloud
|
|
81
|
+
export MOBILERUN_CLOUD_API_KEY=...
|
|
82
|
+
export MOBILERUN_API_BASE_URL=https://api.mobilerun.ai/v1
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## 🚀 Quickstart
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from mobilerun_core import Mobilerun
|
|
89
|
+
|
|
90
|
+
m = Mobilerun()
|
|
91
|
+
|
|
92
|
+
# auto-detect cloud vs local from the device id
|
|
93
|
+
d = m.connect("550e8400-e29b-41d4-a716-446655440000") # cloud (UUID)
|
|
94
|
+
d = m.connect("R5CT123456") # local (ADB serial)
|
|
95
|
+
d = m.connect(some_id, cloud=True) # explicit override
|
|
96
|
+
|
|
97
|
+
# drive the device
|
|
98
|
+
d.open_and_settle("com.instagram.android")
|
|
99
|
+
d.tap_text("Search")
|
|
100
|
+
d.type("droidrun")
|
|
101
|
+
d.key("enter")
|
|
102
|
+
png_b64 = d.screenshot()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Cloud-side discovery is supported too:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
m = Mobilerun()
|
|
109
|
+
d = m.ensure_device(filters={"name": ["pixel - test"]}) # one matching ready device
|
|
110
|
+
all_devices = m.list_devices(filters={"state": ["ready"]})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## 🧱 Concepts
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
Mobilerun() single user-facing facade
|
|
117
|
+
│
|
|
118
|
+
│ .connect(id) → Device helpers (tap_text, scroll_until, …)
|
|
119
|
+
│ │
|
|
120
|
+
│ ▼
|
|
121
|
+
│ Connection (Protocol)
|
|
122
|
+
│ │
|
|
123
|
+
│ ┌───────┴────────┐
|
|
124
|
+
│ ▼ ▼
|
|
125
|
+
│ MobilerunCloud MobilerunFramework
|
|
126
|
+
│ (mobilerun-sdk) (mobilerun-core-cli)
|
|
127
|
+
│ id = UUID id = ADB serial
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- **`Mobilerun`** — the only class users construct. Lazy cloud creds; framework-only use needs no env vars.
|
|
131
|
+
- **`Device`** — what you get back from `.connect()` / `.ensure_device()`. All the helpers live here.
|
|
132
|
+
- **`Connection`** — sync per-device contract (`tap`, `swipe`, `type`, `ui`, `screenshot`, `app_*`). One implementation per transport.
|
|
133
|
+
|
|
134
|
+
## 🪄 Backend selection
|
|
135
|
+
|
|
136
|
+
Explicit override always wins. Otherwise:
|
|
137
|
+
|
|
138
|
+
| `device_id` shape | `adb devices` shows it? | Result |
|
|
139
|
+
| ---------------------------------- | ------------------------ | ------------- |
|
|
140
|
+
| UUID | no | cloud |
|
|
141
|
+
| UUID | yes, cloud creds set | error (pin it)|
|
|
142
|
+
| UUID | yes, no cloud creds | framework |
|
|
143
|
+
| ADB serial / emulator / `IP:port` | yes | framework |
|
|
144
|
+
| anything else | no | error |
|
|
145
|
+
|
|
146
|
+
`adb devices` lookups filter `state=="device"` — `offline` / `unauthorized` rows don't count.
|
|
147
|
+
|
|
148
|
+
## 🛡️ HITL gate
|
|
149
|
+
|
|
150
|
+
Destructive verbs route through a `HitlGate` callable. Default is `deny_all` — pass your own gate to allow specific actions per turn.
|
|
151
|
+
|
|
152
|
+
Gated today:
|
|
153
|
+
- `device.uninstall(package)`
|
|
154
|
+
- `device.install_apk(path, …)` (framework / local APK installs only)
|
|
155
|
+
|
|
156
|
+
Not gated: `tap`, `swipe`, `type`, `key("power")`, `app_stop`. Those are either non-destructive or trivially reversible.
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from mobilerun_core import Mobilerun, HitlDenied
|
|
160
|
+
|
|
161
|
+
def my_gate(action: str, args: dict) -> None:
|
|
162
|
+
if not user_approved(action, args):
|
|
163
|
+
raise HitlDenied(action)
|
|
164
|
+
|
|
165
|
+
m = Mobilerun(hitl_gate=my_gate)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## 🧭 Agent-friendly unsupported verbs
|
|
169
|
+
|
|
170
|
+
The `Connection` Protocol is one surface, but each backend supports only a subset by design. When a verb isn't supported on the active backend, `UnsupportedOperation` is raised — subclasses `NotImplementedError` for back-compat, but carries structured fields an agent can branch on:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from mobilerun_core import UnsupportedOperation
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
device.install_apk("/tmp/app.apk") # not supported on a cloud device
|
|
177
|
+
except UnsupportedOperation as e:
|
|
178
|
+
payload = e.as_dict()
|
|
179
|
+
# {
|
|
180
|
+
# "error": "unsupported_operation",
|
|
181
|
+
# "verb": "app_install_apk",
|
|
182
|
+
# "backend": "cloud",
|
|
183
|
+
# "reason": "...",
|
|
184
|
+
# "alternative": null,
|
|
185
|
+
# "hint": null,
|
|
186
|
+
# }
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Introspect without calling:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
UnsupportedOperation.is_supported(MobilerunCloud, "app_install_apk") # False
|
|
193
|
+
UnsupportedOperation.describe(MobilerunCloud, "app_install_apk")
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## ⚙️ Features
|
|
197
|
+
|
|
198
|
+
- **Two interchangeable transports** — `MobilerunCloud` wraps `mobilerun-sdk`; `MobilerunFramework` wraps `mobilerun-core-cli` for USB / wireless Android.
|
|
199
|
+
- **Sync API** — heredoc-friendly. No `await`, no event loop.
|
|
200
|
+
- **Helpers, not just verbs** — `tap_text`, `tap_and_wait`, `scroll_until`, `wait_for_app`, `wait_for_idle`, `open_and_settle`, `find_nodes`, `assert_on`, `screen_size`, …
|
|
201
|
+
- **Normalized return shapes** — `ui()` always returns a plain `dict`; `screenshot()` always returns base64 PNG.
|
|
202
|
+
- **Lazy cloud credentials** — `Mobilerun()` doesn't touch the env until you make a cloud call.
|
|
203
|
+
- **Back-compat** — `Device(device_id, sdk_client, hitl_gate)` constructor still works for callers pinned to 0.2.x.
|
|
204
|
+
|
|
205
|
+
## ☁️ Framework vs Cloud vs Core
|
|
206
|
+
|
|
207
|
+
| | mobilerun-core (this lib) | [Mobilerun Framework](https://github.com/droidrun/mobilerun) | [Mobilerun Cloud](https://cloud.mobilerun.ai) |
|
|
208
|
+
| --- | --- | --- | --- |
|
|
209
|
+
| What | Programmatic device-control API | Full LLM agent + CLI | Hosted devices + REST + dashboard |
|
|
210
|
+
| Best for | Code-level scripting, custom tools, custom agents | Natural-language tasks, reasoning, vision | Managed phones, fleet workflows, APIs |
|
|
211
|
+
| Where it runs | Wherever your Python runs | Wherever your Python runs | Managed by Mobilerun |
|
|
212
|
+
| LLM included? | No (you bring it) | Yes (OpenAI / Anthropic / etc) | N/A |
|
|
213
|
+
|
|
214
|
+
Most users start with the Framework. Reach for `mobilerun-core` when you want to build something the Framework doesn't ship — a custom agent loop, a test runner, a recording / replay tool, or batch automation.
|
|
215
|
+
|
|
216
|
+
## 💡 Example use cases
|
|
217
|
+
|
|
218
|
+
- Mobile app QA and regression testing.
|
|
219
|
+
- End-to-end flows in CI that target a real device or an emulator.
|
|
220
|
+
- Hybrid dev/CI workflows: same script targets your phone over USB locally, and a cloud device in CI.
|
|
221
|
+
- Building higher-level agent frameworks on top of a stable device API.
|
|
222
|
+
- Recording / replaying user flows for benchmarking.
|
|
223
|
+
|
|
224
|
+
## 🤝 Contributing
|
|
225
|
+
|
|
226
|
+
Issues and PRs welcome. The library aims to stay small and sharply-scoped — please open an issue before adding new surface.
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
git clone https://github.com/droidrun/mobilerun-core.git
|
|
230
|
+
cd mobilerun-core
|
|
231
|
+
uv venv
|
|
232
|
+
uv pip install -e . pytest ruff
|
|
233
|
+
.venv/bin/python -m pytest tests/test_abstraction.py -v
|
|
234
|
+
.venv/bin/ruff check mobilerun_core tests
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## 📄 License
|
|
238
|
+
|
|
239
|
+
Apache-2.0. See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
<picture align="center">
|
|
2
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/droidrun/mobilerun/main/static/mobilerun-dark.png">
|
|
3
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/droidrun/mobilerun/main/static/mobilerun.png">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/droidrun/mobilerun/main/static/mobilerun.png" width="full">
|
|
5
|
+
</picture>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>mobilerun-core is the programmatic Python API behind Mobilerun.</strong><br>
|
|
9
|
+
One sync facade — `Mobilerun()` — for driving Android (and iOS, on cloud) devices, whether they live in the Mobilerun cloud or on a USB cable. Pick a device id, get a `Device`, call <code>tap_text</code>, <code>scroll_until</code>, <code>wait_for_app</code>. No async/await ceremony, no SDK juggling.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<div align="center">
|
|
13
|
+
|
|
14
|
+
<a href="https://docs.mobilerun.ai">📕 Documentation</a>
|
|
15
|
+
·
|
|
16
|
+
<a href="https://cloud.mobilerun.ai">☁️ Mobilerun Cloud</a>
|
|
17
|
+
·
|
|
18
|
+
<a href="https://github.com/droidrun/mobilerun">🤖 Mobilerun Framework</a>
|
|
19
|
+
|
|
20
|
+
[](https://github.com/droidrun/mobilerun-core/stargazers)
|
|
21
|
+
[](https://pypi.org/project/mobilerun-core/)
|
|
22
|
+
[](https://pypi.org/project/mobilerun-core/)
|
|
23
|
+
[](https://mobilerun.ai)
|
|
24
|
+
[](https://x.com/mobilerun_ai)
|
|
25
|
+
[](https://discord.gg/ZZbKEZZkwK)
|
|
26
|
+
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
- 🧰 **One API for two transports** — same helpers run against a cloud device (over `mobilerun-sdk`) or a local Android phone (over `mobilerun-core-cli`).
|
|
30
|
+
- 🪄 **Auto-detection** — pass any device id, the library figures out cloud vs local. Or override explicitly.
|
|
31
|
+
- 🎯 **High-level helpers** — `tap_text`, `tap_node`, `scroll_until`, `wait_for_app`, `open_and_settle`, `find_nodes`, `assert_on`, `screen_size`, … built on top of the raw verbs.
|
|
32
|
+
- 🛡️ **HITL gate** — destructive verbs (`uninstall`, local `install_apk`) route through a callable you control. Default denies; you opt in per turn.
|
|
33
|
+
- 🧭 **Agent-friendly errors** — when a backend doesn't support a verb, you get a structured `UnsupportedOperation` with `verb`, `backend`, and an `alternative` field instead of an opaque traceback.
|
|
34
|
+
- 🪶 **Pure library** — sync, heredoc-friendly, no daemon, no server, no agent loop.
|
|
35
|
+
|
|
36
|
+
Use the library when you want to script a device directly from Python — locally or in the cloud. Use [Mobilerun Framework](https://github.com/droidrun/mobilerun) when you want a full LLM agent driving the device. Use [Mobilerun Cloud](https://cloud.mobilerun.ai) when you want hosted devices and managed infrastructure.
|
|
37
|
+
|
|
38
|
+
## 📦 Installation
|
|
39
|
+
|
|
40
|
+
> **Note:** Python 3.14 is not currently supported. Please use Python `>=3.11,<3.14`.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# cloud-only is enough to start
|
|
44
|
+
uv add mobilerun-core
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# add local-Android (ADB) support
|
|
49
|
+
uv add mobilerun-core mobilerun-core-cli
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Cloud credentials are picked up lazily — `Mobilerun()` does not touch the environment until you make a cloud call. For local-only use, no env vars are required.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# only needed for cloud
|
|
56
|
+
export MOBILERUN_CLOUD_API_KEY=...
|
|
57
|
+
export MOBILERUN_API_BASE_URL=https://api.mobilerun.ai/v1
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 🚀 Quickstart
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from mobilerun_core import Mobilerun
|
|
64
|
+
|
|
65
|
+
m = Mobilerun()
|
|
66
|
+
|
|
67
|
+
# auto-detect cloud vs local from the device id
|
|
68
|
+
d = m.connect("550e8400-e29b-41d4-a716-446655440000") # cloud (UUID)
|
|
69
|
+
d = m.connect("R5CT123456") # local (ADB serial)
|
|
70
|
+
d = m.connect(some_id, cloud=True) # explicit override
|
|
71
|
+
|
|
72
|
+
# drive the device
|
|
73
|
+
d.open_and_settle("com.instagram.android")
|
|
74
|
+
d.tap_text("Search")
|
|
75
|
+
d.type("droidrun")
|
|
76
|
+
d.key("enter")
|
|
77
|
+
png_b64 = d.screenshot()
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Cloud-side discovery is supported too:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
m = Mobilerun()
|
|
84
|
+
d = m.ensure_device(filters={"name": ["pixel - test"]}) # one matching ready device
|
|
85
|
+
all_devices = m.list_devices(filters={"state": ["ready"]})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## 🧱 Concepts
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
Mobilerun() single user-facing facade
|
|
92
|
+
│
|
|
93
|
+
│ .connect(id) → Device helpers (tap_text, scroll_until, …)
|
|
94
|
+
│ │
|
|
95
|
+
│ ▼
|
|
96
|
+
│ Connection (Protocol)
|
|
97
|
+
│ │
|
|
98
|
+
│ ┌───────┴────────┐
|
|
99
|
+
│ ▼ ▼
|
|
100
|
+
│ MobilerunCloud MobilerunFramework
|
|
101
|
+
│ (mobilerun-sdk) (mobilerun-core-cli)
|
|
102
|
+
│ id = UUID id = ADB serial
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- **`Mobilerun`** — the only class users construct. Lazy cloud creds; framework-only use needs no env vars.
|
|
106
|
+
- **`Device`** — what you get back from `.connect()` / `.ensure_device()`. All the helpers live here.
|
|
107
|
+
- **`Connection`** — sync per-device contract (`tap`, `swipe`, `type`, `ui`, `screenshot`, `app_*`). One implementation per transport.
|
|
108
|
+
|
|
109
|
+
## 🪄 Backend selection
|
|
110
|
+
|
|
111
|
+
Explicit override always wins. Otherwise:
|
|
112
|
+
|
|
113
|
+
| `device_id` shape | `adb devices` shows it? | Result |
|
|
114
|
+
| ---------------------------------- | ------------------------ | ------------- |
|
|
115
|
+
| UUID | no | cloud |
|
|
116
|
+
| UUID | yes, cloud creds set | error (pin it)|
|
|
117
|
+
| UUID | yes, no cloud creds | framework |
|
|
118
|
+
| ADB serial / emulator / `IP:port` | yes | framework |
|
|
119
|
+
| anything else | no | error |
|
|
120
|
+
|
|
121
|
+
`adb devices` lookups filter `state=="device"` — `offline` / `unauthorized` rows don't count.
|
|
122
|
+
|
|
123
|
+
## 🛡️ HITL gate
|
|
124
|
+
|
|
125
|
+
Destructive verbs route through a `HitlGate` callable. Default is `deny_all` — pass your own gate to allow specific actions per turn.
|
|
126
|
+
|
|
127
|
+
Gated today:
|
|
128
|
+
- `device.uninstall(package)`
|
|
129
|
+
- `device.install_apk(path, …)` (framework / local APK installs only)
|
|
130
|
+
|
|
131
|
+
Not gated: `tap`, `swipe`, `type`, `key("power")`, `app_stop`. Those are either non-destructive or trivially reversible.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from mobilerun_core import Mobilerun, HitlDenied
|
|
135
|
+
|
|
136
|
+
def my_gate(action: str, args: dict) -> None:
|
|
137
|
+
if not user_approved(action, args):
|
|
138
|
+
raise HitlDenied(action)
|
|
139
|
+
|
|
140
|
+
m = Mobilerun(hitl_gate=my_gate)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## 🧭 Agent-friendly unsupported verbs
|
|
144
|
+
|
|
145
|
+
The `Connection` Protocol is one surface, but each backend supports only a subset by design. When a verb isn't supported on the active backend, `UnsupportedOperation` is raised — subclasses `NotImplementedError` for back-compat, but carries structured fields an agent can branch on:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from mobilerun_core import UnsupportedOperation
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
device.install_apk("/tmp/app.apk") # not supported on a cloud device
|
|
152
|
+
except UnsupportedOperation as e:
|
|
153
|
+
payload = e.as_dict()
|
|
154
|
+
# {
|
|
155
|
+
# "error": "unsupported_operation",
|
|
156
|
+
# "verb": "app_install_apk",
|
|
157
|
+
# "backend": "cloud",
|
|
158
|
+
# "reason": "...",
|
|
159
|
+
# "alternative": null,
|
|
160
|
+
# "hint": null,
|
|
161
|
+
# }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Introspect without calling:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
UnsupportedOperation.is_supported(MobilerunCloud, "app_install_apk") # False
|
|
168
|
+
UnsupportedOperation.describe(MobilerunCloud, "app_install_apk")
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## ⚙️ Features
|
|
172
|
+
|
|
173
|
+
- **Two interchangeable transports** — `MobilerunCloud` wraps `mobilerun-sdk`; `MobilerunFramework` wraps `mobilerun-core-cli` for USB / wireless Android.
|
|
174
|
+
- **Sync API** — heredoc-friendly. No `await`, no event loop.
|
|
175
|
+
- **Helpers, not just verbs** — `tap_text`, `tap_and_wait`, `scroll_until`, `wait_for_app`, `wait_for_idle`, `open_and_settle`, `find_nodes`, `assert_on`, `screen_size`, …
|
|
176
|
+
- **Normalized return shapes** — `ui()` always returns a plain `dict`; `screenshot()` always returns base64 PNG.
|
|
177
|
+
- **Lazy cloud credentials** — `Mobilerun()` doesn't touch the env until you make a cloud call.
|
|
178
|
+
- **Back-compat** — `Device(device_id, sdk_client, hitl_gate)` constructor still works for callers pinned to 0.2.x.
|
|
179
|
+
|
|
180
|
+
## ☁️ Framework vs Cloud vs Core
|
|
181
|
+
|
|
182
|
+
| | mobilerun-core (this lib) | [Mobilerun Framework](https://github.com/droidrun/mobilerun) | [Mobilerun Cloud](https://cloud.mobilerun.ai) |
|
|
183
|
+
| --- | --- | --- | --- |
|
|
184
|
+
| What | Programmatic device-control API | Full LLM agent + CLI | Hosted devices + REST + dashboard |
|
|
185
|
+
| Best for | Code-level scripting, custom tools, custom agents | Natural-language tasks, reasoning, vision | Managed phones, fleet workflows, APIs |
|
|
186
|
+
| Where it runs | Wherever your Python runs | Wherever your Python runs | Managed by Mobilerun |
|
|
187
|
+
| LLM included? | No (you bring it) | Yes (OpenAI / Anthropic / etc) | N/A |
|
|
188
|
+
|
|
189
|
+
Most users start with the Framework. Reach for `mobilerun-core` when you want to build something the Framework doesn't ship — a custom agent loop, a test runner, a recording / replay tool, or batch automation.
|
|
190
|
+
|
|
191
|
+
## 💡 Example use cases
|
|
192
|
+
|
|
193
|
+
- Mobile app QA and regression testing.
|
|
194
|
+
- End-to-end flows in CI that target a real device or an emulator.
|
|
195
|
+
- Hybrid dev/CI workflows: same script targets your phone over USB locally, and a cloud device in CI.
|
|
196
|
+
- Building higher-level agent frameworks on top of a stable device API.
|
|
197
|
+
- Recording / replaying user flows for benchmarking.
|
|
198
|
+
|
|
199
|
+
## 🤝 Contributing
|
|
200
|
+
|
|
201
|
+
Issues and PRs welcome. The library aims to stay small and sharply-scoped — please open an issue before adding new surface.
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
git clone https://github.com/droidrun/mobilerun-core.git
|
|
205
|
+
cd mobilerun-core
|
|
206
|
+
uv venv
|
|
207
|
+
uv pip install -e . pytest ruff
|
|
208
|
+
.venv/bin/python -m pytest tests/test_abstraction.py -v
|
|
209
|
+
.venv/bin/ruff check mobilerun_core tests
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## 📄 License
|
|
213
|
+
|
|
214
|
+
Apache-2.0. See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from mobilerun_core.connection import UnsupportedOperation
|
|
2
|
+
from mobilerun_core.device import Device, DeviceHandle
|
|
3
|
+
from mobilerun_core.hitl import HitlDenied, HitlGate
|
|
4
|
+
from mobilerun_core.mobilerun import Mobilerun
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"Mobilerun",
|
|
8
|
+
"Device",
|
|
9
|
+
"DeviceHandle",
|
|
10
|
+
"HitlGate",
|
|
11
|
+
"HitlDenied",
|
|
12
|
+
"UnsupportedOperation",
|
|
13
|
+
]
|
|
14
|
+
__version__ = "0.3.0"
|