Metadata-Version: 2.4
Name: axilio-mobile
Version: 0.1.0
Summary: Axilio mobile device driver — keyboard, touch, and screen capture for sandboxed mobile devices
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: black==25.1.0; extra == 'dev'
Requires-Dist: build>=1.0; extra == 'dev'
Requires-Dist: mypy==1.16.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: ruff==0.12.1; extra == 'dev'
Description-Content-Type: text/markdown

# Axilio Mobile Driver

`axilio-mobile` is the [Axilio](https://axilio.ai) mobile-device driver — the Python package that user code imports inside a sandbox to drive a paired mobile device. Tap, swipe, type, key-press, screenshot.

It's one of a family of Axilio driver packages — `axilio-mobile` (this), `axilio-browser` (future), `axilio-desktop` (future) — that share a single `axilio.*` import namespace. For managing the platform from outside the sandbox (workflows, runs, devices, usage) see [`axilio-platform`](https://github.com/axilioai/axilio-platform-python).

## Installation

```bash
pip install axilio-mobile
```

## Quick start

```python
from axilio.drivers import mobile

mobile.tap(coords={"x": 540, "y": 1200})
mobile.swipe(start={"x": 100, "y": 800}, end={"x": 100, "y": 200})
mobile.type_text(text="hello")
mobile.key_press(key=mobile.Key.HOME)

png_bytes = mobile.screenshot()
open("frame.png", "wb").write(png_bytes)
```

All primitives are **keyword-only** — pass everything by name. The driver is meant to run inside an Axilio sandbox VM — code authored in the dashboard editor, scheduled via the dashboard, or run in our hosted runtime — where the daemon listening on `/run/axilio/sdk.sock` is paired to a real device. There is no API key, no authentication step, no allocation lifecycle to manage from user code; the daemon handles all of that.

## Where to run it

The driver only works inside an Axilio sandbox VM (today). If you import it locally and call `mobile.tap(...)`, you'll get a `mobile.ConnectionError` because there's no daemon listening on the socket.

To experiment without a sandbox, you can point the driver at a custom socket:

```bash
export AXILIO_SDK_SOCKET=/tmp/axilio-test.sock
```

…and run a fake server on that socket. The `tests/conftest.py` in this repo has a reference implementation.

## Class-based API

For tests or callers who want explicit lifecycle:

```python
from axilio.drivers.mobile import Device

dev = Device(socket_path="/run/axilio/sdk.sock")
dev.tap(coords={"x": 540, "y": 1200})
dev.close()
```

## Errors

Errors raised by the driver are subclasses of `mobile.AxilioError`:

```python
from axilio.drivers import mobile

try:
    mobile.tap(coords={"x": 540, "y": 1200})
except mobile.NoAllocationError:
    print("No active allocation — daemon hasn't been paired with a device.")
except mobile.DeviceOfflineError as e:
    if e.retryable:
        # transient — try again in a moment
        ...
except mobile.AxilioError as e:
    # catch-all
    print(f"axilio: {e.code}: {e}")
```

The exception hierarchy mirrors the daemon's wire-level error codes:

| Exception | Wire `code` | Retryable? | Notes |
|---|---|---|---|
| `NoAllocationError` | `no_allocation` | no | Daemon hasn't received `SetAllocation` |
| `DeviceOfflineError` | `device_offline` | yes | Driver not bound or capturer not started |
| `UnauthorizedError` | `unauthorized` | no | Session token rejected by Atlas |
| `InvalidArgsError` | `invalid_args` | no | Programming error in args |
| `UnknownOpError` | `unknown_op` | no | SDK/daemon version skew |
| `NotConnectedError` | `not_connected` | no | Daemon couldn't reach Atlas |
| `CanceledError` | `canceled` | sometimes | Deadline exceeded |
| `InternalError` | `internal` | no | Unclassified daemon failure |
| `ConnectionError` | (n/a) | no | SDK couldn't open the Unix socket |

## Reference: primitives

All primitives are keyword-only (note the leading `*` in each signature). Future params (`query`, `region`, `wait_for`) are present in the signatures already but raise `NotImplementedError` until the device side catches up.

```python
class Coords(TypedDict):
    x: int
    y: int

class Region(TypedDict):
    x: int
    y: int
    width: int
    height: int

mobile.tap(
    *,
    coords: Coords | None = None,
    query: str | None = None,        # not yet supported
    region: Region | None = None,    # not yet supported
    wait_for: int | None = None,     # not yet supported (ms)
) -> None

mobile.swipe(
    *,
    start: Coords,
    end: Coords,
    duration_ms: int = 300,
) -> None

mobile.key_press(*, key: int) -> None

mobile.type_text(*, text: str) -> None

mobile.screenshot(
    *,
    region: Region | None = None,    # not yet supported
) -> bytes  # PNG-encoded
```

Coords are frame-space pixels (the cropped video output). `key_press` takes a 16-bit USB HID consumer-page code; common codes are exposed as `mobile.Key.HOME`, `mobile.Key.BACK`, etc.

## Environment

| Variable | Description |
|---|---|
| `AXILIO_SDK_SOCKET` | Override the daemon socket path. Default: `/run/axilio/sdk.sock` |

## What's not in this driver

- **Workflow / run / device management** — that's the `axilio-platform` SDK.
- **Allocation / `connect` lifecycle** — sandbox VMs are pre-allocated; user code doesn't manage it.
- **Local mode** (running on a laptop driving a remote device) — defer until customer demand surfaces.
- **OCR-anchored helpers** (`tap_at("Sign in")`, `wait_for_element(...)`) — defer until the underlying atlas-side element discovery lands.
- **Async API** — sync only for now.
- **Op-set extensions** (`long_press`, `drag`, `pinch`, app management, `read_screen`, `find`) — land as the underlying atlas / firmware infra exists.
- **Browser / desktop control** — different surfaces get different drivers (`axilio-browser`, `axilio-desktop` — both future).
