turboadb 0.1.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.
turboadb/README.md ADDED
@@ -0,0 +1,453 @@
1
+ # TurboADB
2
+
3
+ **An Android ADB + scrcpy device toolkit for automotive/embedded Android
4
+ (Android Automotive OS / IVI head units) and general Android work.**
5
+
6
+ TurboADB wraps Google's `adb` and `scrcpy` behind one robust, structured layer
7
+ you can drive three ways from a single `pip install turboadb`:
8
+
9
+ 1. **Python API** — `import turboadb` for scripts and test frameworks.
10
+ 2. **CLI** — `turboadb …`, fully argument-driven (argparse).
11
+ 3. **Desktop GUI** — `turboadb-gui`, a full PyQt5 app shipped as a prebuilt
12
+ Windows `.exe` (PyQt5 baked in) so it runs even where PyQt5 has no wheel
13
+ (e.g. Windows ARM64) — **no PyQt5 install required**.
14
+
15
+ Every action returns a structured result (`CommandResult` / `TransferResult` /
16
+ `StreamResult`), with a **safe mode** that returns an `OperationResult` instead
17
+ of raising, and a typed exception hierarchy (`ADBError` base). All blocking work
18
+ can run on background threads, so it never blocks or crashes the caller.
19
+
20
+ ---
21
+
22
+ ## Table of contents
23
+
24
+ - [Install](#install)
25
+ - [The GUI](#the-gui)
26
+ - [Quick start (API)](#quick-start-api)
27
+ - [Connecting — USB vs network (Wi-Fi / Ethernet)](#connecting--usb-vs-network-wifi--ethernet)
28
+ - [Running shell commands](#running-shell-commands)
29
+ - [Live logcat (continuous logs)](#live-logcat-continuous-logs)
30
+ - [File transfer (push / pull)](#file-transfer-push--pull)
31
+ - [App management](#app-management)
32
+ - [Media — screenshot & screen record](#media--screenshot--screen-record)
33
+ - [Port forwarding (forward / reverse)](#port-forwarding-forward--reverse)
34
+ - [scrcpy mirroring (the visual session)](#scrcpy-mirroring-the-visual-session)
35
+ - [Automotive (Android Automotive OS / IVI)](#automotive-android-automotive-os--ivi)
36
+ - [Result objects & error handling](#result-objects--error-handling)
37
+ - [CLI reference](#cli-reference)
38
+ - [API map](#api-map)
39
+ - [Building the GUI exe & releasing](#building-the-gui-exe--releasing)
40
+ - [License](#license)
41
+
42
+ ---
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ pip install turboadb
48
+ ```
49
+
50
+ That gives you the Python API, the `turboadb` CLI, and `turboadb-gui` (which
51
+ launches the bundled Windows exe; on other platforms install the GUI extra:
52
+ `pip install "turboadb[gui]"`).
53
+
54
+ TurboADB needs Google's **platform-tools (`adb`)** and, for mirroring,
55
+ **`scrcpy`**. It auto-detects them on your `PATH`, in the Android SDK, or in
56
+ common install locations. If they're missing it tells you exactly how to install
57
+ them — or set explicit paths:
58
+
59
+ ```bash
60
+ turboadb doctor # shows where adb / scrcpy were found (or how to get them)
61
+ ```
62
+
63
+ - `adb`: <https://developer.android.com/tools/releases/platform-tools>
64
+ - `scrcpy`: <https://github.com/Genymobile/scrcpy>
65
+ (Windows: `winget install scrcpy` · macOS: `brew install scrcpy` · Linux: `apt install scrcpy`)
66
+
67
+ Override detection any time with `ADBConfig(adb_path=..., scrcpy_path=...)`,
68
+ the `TURBOADB_ADB` / `TURBOADB_SCRCPY` environment variables, or the GUI's
69
+ Settings dialog.
70
+
71
+ ---
72
+
73
+ ## The GUI
74
+
75
+ ```bash
76
+ turboadb-gui # or: python -m turboadb.gui (from source)
77
+ turboadb-shortcut # make a Desktop shortcut (Windows)
78
+ ```
79
+
80
+ A tabbed, multi-device workspace:
81
+
82
+ - **Tabbed multi-device sessions** — each device opens its own tab; tabs are
83
+ closable, movable, and there's a **`+`** new-tab button. A **Split/tile** view
84
+ shows several devices at once.
85
+ - **Sidebar device manager** — saved targets **plus a LIVE `adb devices`** list
86
+ that auto-refreshes; a **Quick connect** filter box; right-click context menu
87
+ (New / Open / Edit / Duplicate / Delete). Double-click a live device to open it.
88
+ - **Ribbon toolbar** with colorful buttons: Device, Shell, Logcat, Files, Apps,
89
+ Scrcpy (mirror), Screenshot, Split, Settings, Help, Exit.
90
+ - **Per-device panels:** an interactive **Shell** terminal; a live **Logcat**
91
+ viewer (regex filter, level filter, pause/clear/save); a **Files** browser
92
+ (adb push/pull with file *and* folder pickers for both directions, progress
93
+ bar); an **Apps** manager (install via file dialog incl. split APKs, list,
94
+ uninstall, clear, start, stop); and a one-click **Mirror (scrcpy)** button.
95
+ - A dark, color-coded **log dock** at the bottom.
96
+ - **Settings** (persisted to `~/.turboadb/settings.json`, applied live):
97
+ dark/light theme, terminal font + size, default scrcpy options, adb/scrcpy
98
+ paths, logcat format. Defaults to a clean **black** dark theme; the light
99
+ theme isn't glaring.
100
+ - **Crash-proof:** a startup-failure native popup + crash log, a global
101
+ exception hook that logs and shows a non-fatal popup, and clean thread
102
+ shutdown when a tab closes.
103
+
104
+ The window/app icon is an automotive **speedometer** fused with the **Android
105
+ robot** and a terminal prompt.
106
+
107
+ ---
108
+
109
+ ## Quick start (API)
110
+
111
+ ```python
112
+ from turboadb import ADBHandler, ADBConfig
113
+
114
+ # USB: the only attached device (or pass serial="..." to pick one)
115
+ with ADBHandler() as dev:
116
+ print(dev.device_info().value if False else dev.shell("getprop ro.build.version.release").text)
117
+ dev.push("app.apk", "/data/local/tmp/app.apk")
118
+ dev.install("app.apk", grant_perms=True)
119
+
120
+ # live logs, with a regex match + tee to a file
121
+ dev.logcat(tag="ActivityManager", match=r"ANR|FATAL",
122
+ on_line=print, save_to="boot.log", stop_on_match=True)
123
+
124
+ dev.screenshot("shot.png")
125
+ dev.mirror(max_size=1280) # launch scrcpy
126
+ ```
127
+
128
+ The raw adb path is always available at `dev.adb_path`, and any adb command is a
129
+ call away: `dev.adb("shell", "wm", "size")`.
130
+
131
+ ---
132
+
133
+ ## Connecting — USB vs network (Wi-Fi / Ethernet)
134
+
135
+ **USB (local):**
136
+
137
+ ```python
138
+ from turboadb import ADBHandler, ADBConfig, list_devices
139
+
140
+ for d in list_devices():
141
+ print(d) # serial, state, model…
142
+
143
+ with ADBHandler(ADBConfig(serial="emulator-5554")) as dev:
144
+ ...
145
+ ```
146
+
147
+ **Network (remote head unit / IVI on the bench LAN):**
148
+
149
+ ```python
150
+ # Enable TCP mode once while on USB, then connect wirelessly:
151
+ with ADBHandler() as usb:
152
+ usb.tcpip(5555)
153
+
154
+ with ADBHandler(ADBConfig(host="192.168.1.50", port=5555)) as hu:
155
+ print(hu.shell("getprop ro.build.characteristics").text)
156
+ ```
157
+
158
+ **Android 11+ wireless pairing:**
159
+
160
+ ```python
161
+ dev = ADBHandler(ADBConfig(host="192.168.1.50", port=5555))
162
+ dev.pair("192.168.1.50", 37123, "482913") # host:pairing_port + code
163
+ dev.connect()
164
+ ```
165
+
166
+ `connect()` auto-runs `adb connect host:port` for network targets, waits for the
167
+ device, and verifies it's `device` (not `unauthorized`/`offline`) with a clear,
168
+ actionable error if not.
169
+
170
+ ---
171
+
172
+ ## Running shell commands
173
+
174
+ ```python
175
+ res = dev.shell("pm list packages -3")
176
+ print(res.ok, res.exit_code, res.duration)
177
+ for line in res.lines:
178
+ print(line)
179
+
180
+ dev.shell("settings put global development_settings_enabled 1", check=True)
181
+ dev.shell("svc power stayon true", su=True) # wrap in su -c for rooted devices
182
+
183
+ # an interactive shell session (used by the GUI terminal, usable in scripts)
184
+ sh = dev.open_shell()
185
+ sh.send_line("top -n 1")
186
+ import time; time.sleep(1)
187
+ print(sh.read().decode(errors="replace"))
188
+ sh.close()
189
+ ```
190
+
191
+ ---
192
+
193
+ ## Live logcat (continuous logs)
194
+
195
+ Stream logcat **live, line by line**, cleanly formatted, with regex matching,
196
+ match callbacks, stop-on-match, and tee-to-file — plus tag/priority filters and
197
+ buffer selection.
198
+
199
+ ```python
200
+ # tag + minimum priority, collect ANR/FATAL, save everything, stop on first hit
201
+ res = dev.logcat(tag="ActivityManager", priority="I",
202
+ match=r"ANR|FATAL", on_match=lambda l: print("HIT:", l),
203
+ save_to="session.log", stop_on_match=True,
204
+ buffers=["main", "system", "crash"], clear_first=True)
205
+ print(res.lines, "lines,", len(res.matches), "matches")
206
+
207
+ # arbitrary streaming command + a stop event from another thread
208
+ import threading
209
+ stop = threading.Event()
210
+ dev.logcat(on_line=print, stop_event=stop) # stop.set() to end
211
+
212
+ dev.logcat_clear() # adb logcat -c
213
+ ```
214
+
215
+ `fmt=` sets the `-v` format (default `threadtime`); `dump=True` does `-d`
216
+ (dump current buffer and exit); `filterspecs=[...]` passes explicit `TAG:LEVEL`
217
+ specs. Lines are ANSI/control-char cleaned by default (`clean=True`).
218
+
219
+ ---
220
+
221
+ ## File transfer (push / pull)
222
+
223
+ Files **and** folders, with a live percent callback (the GUI shows a progress
224
+ bar):
225
+
226
+ ```python
227
+ dev.push("local_dir/", "/sdcard/local_dir", on_progress=lambda p: print(p, "%"))
228
+ dev.pull("/sdcard/Download", "out/", on_progress=print)
229
+
230
+ tr = dev.push("big.bin", "/data/local/tmp/big.bin")
231
+ print(tr.human_size, tr.human_speed, tr.duration) # 12.0MB 48.0MB/s 0.25
232
+ ```
233
+
234
+ ---
235
+
236
+ ## App management
237
+
238
+ ```python
239
+ dev.install("app.apk", replace=True, grant_perms=True) # adb install -r -g
240
+ dev.install_multiple(["base.apk", "split_config.en.apk"]) # split APKs
241
+ dev.uninstall("com.example.app", keep_data=False)
242
+
243
+ dev.list_packages(third_party=True) # -> ["com.foo", ...]
244
+ dev.clear_app("com.example.app") # pm clear
245
+ dev.start_app("com.example.app") # launcher intent
246
+ dev.start_activity("com.example.app/.MainActivity")
247
+ dev.stop_app("com.example.app") # am force-stop
248
+ dev.grant("com.example.app", "android.permission.CAMERA")
249
+ dev.revoke("com.example.app", "android.permission.CAMERA")
250
+ print(dev.current_activity()) # foreground component
251
+ ```
252
+
253
+ ---
254
+
255
+ ## Media — screenshot & screen record
256
+
257
+ ```python
258
+ dev.screenshot("shot.png") # exec-out screencap -p
259
+ png_bytes = dev.screenshot() # raw PNG bytes if no path
260
+
261
+ # record on-device, then pull it (stop early with a stop_event)
262
+ dev.screen_record("clip.mp4", time_limit=20, size="1280x720", bit_rate="8M")
263
+ ```
264
+
265
+ ---
266
+
267
+ ## Port forwarding (forward / reverse)
268
+
269
+ Stoppable handles (`adb forward` / `adb reverse`):
270
+
271
+ ```python
272
+ fwd = dev.forward("tcp:9222", "localabstract:chrome_devtools_remote")
273
+ # ... use 127.0.0.1:9222 on the host ...
274
+ fwd.close() # adb forward --remove
275
+
276
+ with dev.reverse("tcp:8000", "tcp:8000"): # device reaches your PC:8000
277
+ ...
278
+ print(dev.list_forwards())
279
+ ```
280
+
281
+ ---
282
+
283
+ ## scrcpy mirroring (the visual session)
284
+
285
+ Launch real-time screen mirroring + control — the visual analog of opening a
286
+ remote desktop for a host:
287
+
288
+ ```python
289
+ from turboadb import ScrcpyOptions
290
+
291
+ sess = dev.mirror(max_size=1280, bit_rate="8M", stay_awake=True)
292
+ # ... a scrcpy window is now mirroring & controlling the device ...
293
+ sess.stop()
294
+
295
+ # full control via ScrcpyOptions (crop is great for IVI displays):
296
+ opts = ScrcpyOptions(crop="1920x1080:0:0", display_id=0,
297
+ record="drive.mp4", turn_screen_off=True)
298
+ dev.mirror(opts)
299
+ ```
300
+
301
+ ---
302
+
303
+ ## Automotive (Android Automotive OS / IVI)
304
+
305
+ ```python
306
+ with ADBHandler(ADBConfig(host="192.168.1.50")) as hu:
307
+ info = hu.device_info()
308
+ print(info["manufacturer"], info["model"], "Android", info["android_version"])
309
+ if hu.is_automotive():
310
+ print("This is an Android Automotive OS head unit.")
311
+ # multi-display head units: mirror a specific display
312
+ hu.mirror(display_id=1, crop="1280x720:0:0")
313
+ ```
314
+
315
+ `device_info()` includes an `automotive` flag (from the automotive hardware
316
+ feature / build characteristics), so you can branch IVI-specific flows.
317
+
318
+ ---
319
+
320
+ ## Result objects & error handling
321
+
322
+ Every operation returns a structured result:
323
+
324
+ | Result | Key fields / props |
325
+ |-----------------|----------------------------------------------------------------|
326
+ | `CommandResult` | `.ok`, `.exit_code`, `.stdout`, `.stderr`, `.duration`, `.text`, `.lines` |
327
+ | `TransferResult`| `.size_bytes`, `.duration`, `.files`, `.human_size`, `.human_speed` |
328
+ | `StreamResult` | `.lines`, `.matches`, `.matched`, `.saved_to` |
329
+ | `OperationResult`| `.success`/`bool`, `.value`, `.error`, `.unwrap()` (safe mode) |
330
+
331
+ **Raise mode (default)** — great for test automation:
332
+
333
+ ```python
334
+ try:
335
+ dev.shell("false", check=True)
336
+ except ADBError as exc:
337
+ print("failed:", exc)
338
+ ```
339
+
340
+ **Safe mode** — great for GUIs (never throws):
341
+
342
+ ```python
343
+ dev = ADBHandler(cfg, safe=True)
344
+ res = dev.install("app.apk")
345
+ if res: # OperationResult is falsy on failure
346
+ print("ok:", res.value)
347
+ else:
348
+ print("error:", res.error)
349
+ ```
350
+
351
+ Exception hierarchy (catch `ADBError` for everything):
352
+ `ADBNotFoundError`, `ADBConnectionError`, `ADBTimeoutError`,
353
+ `ADBNotConnectedError`, `ADBCommandError`, `ADBTransferError`,
354
+ `ADBInstallError`, `ScrcpyError`.
355
+
356
+ ---
357
+
358
+ ## CLI reference
359
+
360
+ ```
361
+ turboadb doctor # is adb / scrcpy installed?
362
+ turboadb devices # list attached/known devices
363
+ turboadb info [-s S] # device identity / build / automotive flag
364
+ turboadb shell [-s S] -- CMD… # one-shot adb shell (use --su to wrap in su -c)
365
+ turboadb logcat [-s S] [--tag T --priority I --match RE --save F --stop-on-match --clear --dump]
366
+ turboadb logcat-clear[-s S]
367
+ turboadb push [-s S] LOCAL REMOTE
368
+ turboadb pull [-s S] REMOTE LOCAL
369
+ turboadb install [-s S] APK [APK…] [--grant --downgrade --no-replace]
370
+ turboadb uninstall [-s S] PKG [--keep-data]
371
+ turboadb packages [-s S] [--third-party --system] [FILTER]
372
+ turboadb clear [-s S] PKG
373
+ turboadb start|stop [-s S] PKG
374
+ turboadb screenshot [-s S] PATH
375
+ turboadb record [-s S] PATH [--time-limit 30 --size 1280x720 --bit-rate 8M]
376
+ turboadb forward [-s S] LOCAL REMOTE # stays until Ctrl+C
377
+ turboadb reverse [-s S] REMOTE LOCAL
378
+ turboadb scrcpy [-s S] [--max-size 1280 --bit-rate 8M --record F --turn-screen-off --no-control --wait]
379
+ turboadb connect HOST:PORT
380
+ turboadb disconnect [HOST:PORT]
381
+ turboadb tcpip [-s S] [PORT]
382
+ turboadb pair HOST:PAIRPORT CODE
383
+ turboadb reboot [-s S] [recovery|bootloader|sideload]
384
+ turboadb root [-s S]
385
+ turboadb gui # launch the desktop GUI
386
+ ```
387
+
388
+ `-s` accepts a USB serial **or** a `host:port` network target. Add `--json` to
389
+ most commands for machine-readable output. Examples:
390
+
391
+ ```bash
392
+ turboadb -s 192.168.1.50:5555 shell -- dumpsys power | findstr mWakefulness
393
+ turboadb logcat --tag ActivityManager --priority I --match "ANR|FATAL" --save boot.log
394
+ turboadb install base.apk split_config.en.apk --grant
395
+ turboadb screenshot dash.png
396
+ turboadb scrcpy --max-size 1280 --bit-rate 8M
397
+ ```
398
+
399
+ Other console scripts: `turboadb-gui`, `turboadb-docs`, `turboadb-shortcut`.
400
+
401
+ ---
402
+
403
+ ## API map
404
+
405
+ ```
406
+ turboadb
407
+ ├── ADBHandler(config=ADBConfig|None, *, serial=, safe=, quiet=, log_callback=)
408
+ │ ├── connect() / disconnect() / is_connected / get_state() / wait_for_device()
409
+ │ ├── tcpip(port) / connect_tcp(host, port) / pair(host, port, code)
410
+ │ ├── reboot(mode) / root() / unroot() / remount()
411
+ │ ├── getprop(name?) / device_info() / is_automotive()
412
+ │ ├── shell(cmd, su=, check=) / shell_many() / open_shell() -> ShellSession
413
+ │ ├── iter_lines(args) / stream(args, …) / logcat(…) / logcat_clear()
414
+ │ ├── push(local, remote, on_progress=) / pull(remote, local, on_progress=)
415
+ │ ├── install() / install_multiple() / uninstall() / list_packages()
416
+ │ ├── clear_app() / start_app() / start_activity() / stop_app()
417
+ │ ├── grant() / revoke() / current_activity()
418
+ │ ├── screenshot(path?) / screen_record(path, …)
419
+ │ ├── forward(local, remote) / reverse(remote, local) -> ForwardHandle
420
+ │ ├── list_forwards() / remove_all_forwards()
421
+ │ ├── mirror(options?|**opts) -> ScrcpySession
422
+ │ └── adb(*args) · adb_path · serial
423
+ ├── ADBConfig / ScrcpyOptions
424
+ ├── Device / list_devices() / first_online()
425
+ ├── launch_scrcpy() / ScrcpySession
426
+ ├── find_adb() / find_scrcpy() / adb_available() / scrcpy_available() / diagnose()
427
+ ├── CommandResult / TransferResult / StreamResult / OperationResult / strip_ansi
428
+ └── ADBError + ADBNotFoundError / ADBConnectionError / ADBTimeoutError /
429
+ ADBNotConnectedError / ADBCommandError / ADBTransferError /
430
+ ADBInstallError / ScrcpyError
431
+ ```
432
+
433
+ ---
434
+
435
+ ## Building the GUI exe & releasing
436
+
437
+ ```bash
438
+ python scripts/make_icon.py # (re)generate the icon
439
+ python scripts/build_exe.py # -> dist/turboadb-gui.exe (PyQt5 baked in)
440
+ # copy the exe into turboadb/bin/ so the wheel ships it, then:
441
+ python scripts/release.py patch # bump -> test -> build -> twine check -> upload
442
+ ```
443
+
444
+ `turboadb-gui` runs the bundled exe when present and falls back to running from
445
+ source (`turboadb[gui]`) otherwise. The release helper reads the PyPI token from
446
+ `TWINE_PASSWORD` (never hard-coded) and supports `--wheel-only` if a flaky
447
+ network makes the sdist+wheel upload hang.
448
+
449
+ ---
450
+
451
+ ## License
452
+
453
+ MIT — see [LICENSE](LICENSE).
turboadb/__init__.py ADDED
@@ -0,0 +1,65 @@
1
+ """
2
+ TurboADB
3
+ ========
4
+
5
+ An Android **ADB + scrcpy** device toolkit for automotive/embedded Android
6
+ (Android Automotive OS / IVI head units) and general Android work. Wraps Google's
7
+ ``adb`` and ``scrcpy`` behind one robust, structured API — usable as a Python
8
+ library, a CLI (``turboadb``), or a full PyQt5 desktop GUI (``turboadb-gui``).
9
+
10
+ Quick start
11
+ -----------
12
+ from turboadb import ADBHandler, ADBConfig
13
+
14
+ # USB (only device) — or serial="..." to pick one of several
15
+ with ADBHandler() as dev:
16
+ print(dev.shell("getprop ro.build.version.release").text)
17
+ dev.push("app.apk", "/data/local/tmp/app.apk")
18
+ dev.install("app.apk", grant_perms=True)
19
+ dev.logcat(tag="ActivityManager", match=r"ANR|FATAL", on_line=print)
20
+
21
+ # Network / Wi-Fi head unit
22
+ with ADBHandler(ADBConfig(host="192.168.1.50", port=5555)) as hu:
23
+ hu.mirror(max_size=1280) # launch scrcpy
24
+
25
+ Every action returns a structured result (``CommandResult`` / ``TransferResult``
26
+ / ``StreamResult``). Pass ``safe=True`` to get an ``OperationResult`` instead of
27
+ exceptions (ideal for GUIs). The raw adb path is always at ``dev.adb_path``.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ __version__ = "0.1.0"
33
+
34
+ from .config import ADBConfig, ScrcpyOptions
35
+ from .core import ADBHandler, ADBDevice, ShellSession, ForwardHandle
36
+ from .devices import Device, list_devices, first_online
37
+ from .scrcpy import launch_scrcpy, ScrcpySession
38
+ from .tools import (find_adb, find_scrcpy, adb_available, scrcpy_available,
39
+ adb_version, diagnose, ADB_DOWNLOAD, SCRCPY_DOWNLOAD)
40
+ from .results import (CommandResult, TransferResult, StreamResult,
41
+ OperationResult, strip_ansi)
42
+ from .exceptions import (
43
+ ADBError,
44
+ ADBNotFoundError,
45
+ ADBConnectionError,
46
+ ADBTimeoutError,
47
+ ADBNotConnectedError,
48
+ ADBCommandError,
49
+ ADBTransferError,
50
+ ADBInstallError,
51
+ ScrcpyError,
52
+ )
53
+
54
+ __all__ = [
55
+ "ADBHandler", "ADBDevice", "ADBConfig", "ScrcpyOptions", "ShellSession",
56
+ "ForwardHandle", "Device", "list_devices", "first_online",
57
+ "launch_scrcpy", "ScrcpySession",
58
+ "find_adb", "find_scrcpy", "adb_available", "scrcpy_available",
59
+ "adb_version", "diagnose", "ADB_DOWNLOAD", "SCRCPY_DOWNLOAD",
60
+ "CommandResult", "TransferResult", "StreamResult", "OperationResult",
61
+ "strip_ansi",
62
+ "ADBError", "ADBNotFoundError", "ADBConnectionError", "ADBTimeoutError",
63
+ "ADBNotConnectedError", "ADBCommandError", "ADBTransferError",
64
+ "ADBInstallError", "ScrcpyError", "__version__",
65
+ ]
turboadb/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Enable ``python -m turboadb`` to run the CLI."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
Binary file
Binary file
Binary file