# Client and connections

`TapClient` finds the running server; `TapConnection` attaches devices and holds them. The
server's device list and info types, and starting the server from code. Guide:
[The tap server](../../guide/server.md), [Python + pytest](../../sdk/python.md#without-pytest).

## <kbd>class</kbd> `TapClient`
One gRPC channel to a host server. Cheap to create; share one per process. 

:meth:`create` is the usual entry point: it resolves the endpoint (see :func:`resolve_endpoint`) and, when discovering from ``daemon.json``, checks that the server answers ``Info``. The constructor takes an already-resolved :class:`Endpoint` and does no I/O. Use it as a context manager, or call :meth:`close` when done. 

### <kbd>method</kbd> `TapClient.__init__`

```python
__init__(endpoint: 'Endpoint')
```

---

### <kbd>method</kbd> `TapClient.close`

```python
close() → None
```

Closes the channel. Close every ``TapConnection`` first. 

---

### <kbd>method</kbd> `TapClient.connect`

```python
connect(
    name: 'str',
    first_event_timeout: 'float' = 30.0,
    hold: 'float | None' = None
) → TapConnection
```

Open a ``TapConnection`` named ``name``. 

By default the connection is *observed*: its liveness stream is established before this returns (the server's first event, ``observing``, is awaited for up to ``first_event_timeout`` seconds, else ``TapError`` and the new id is disconnected), so every later ``attach_device`` belongs to a live connection: if this process dies, the server detaches every device the connection owns. A later ``closing`` event (the daemon reaped the connection or is shutting down) makes it unusable with the daemon's reason. 

With ``hold`` (seconds, 1 to 86400) the connection is *held* instead: it has no liveness stream and outlives this process, so a later process can pick it up with :meth:`resume`. Every call naming it restarts its idle clock; it ends after ``hold`` seconds without one, on ``close`` from any process, or when the daemon stops. Its name must be unique among held connections (else ``ServerError``, reason ``DAEMON_PRECONDITION``). Tests should keep the default: a crashed test run would otherwise hold its devices until the timeout. ``hold`` is experimental (the agent surface): it may change in any release. 

Use the result as a context manager, or call ``close`` when done. 

---

### <kbd>method</kbd> `TapClient.connections`

```python
connections() → list[ConnectionEntry]
```

Every live client connection, held or observed, with the devices attached to it. 

---

### <kbd>classmethod</kbd> `TapClient.create`

```python
create(address: 'str | None' = None, token: 'str | None' = None) → TapClient
```

A client for ``address`` (token: ``token`` or ``TAP_TOKEN``), or for ``TAP_SERVER``, or for the live server ``daemon.json`` records. Raises ``TapError`` telling you to run ``tap start`` when none is running. Never starts a server. 

---

### <kbd>method</kbd> `TapClient.devices`

```python
devices() → list[DeviceEntry]
```

Every device ADB lists, with its state (``FREE``, ``LEASED``, ``QUARANTINED``, ``OFFLINE``, ``UNAUTHORIZED``); only ``FREE`` and ``LEASED`` devices can be attached. 

---

### <kbd>method</kbd> `TapClient.info`

```python
info() → ServerInfo
```

Daemon version and pid, protocol version, ADB executable, state dir, whether the daemon carries a driver to install (``driver_available``) and its default timeouts. 

---

### <kbd>method</kbd> `TapClient.resume`

```python
resume(name: 'str') → TapConnection
```

The held connection named ``name`` (see ``connect(..., hold=...)``), for this process to use; ``attached_devices()`` returns its devices. Raises ``TapError`` when no held connection has that name (it was closed, or it expired). 

Experimental: not covered by the compatibility promise; it may change in any release. 

## <kbd>class</kbd> `TapConnection`
An identity at the server that owns attached devices, returned by ``TapClient.connect``. 

An observed connection (the default) has its liveness stream already running on a background thread. If this process dies, the server detaches every device owned by the connection. If the stream ends unexpectedly (server restart, network loss) or the daemon sends ``closing``, the connection becomes unusable: ``attach_device`` and every call on its devices raise ``TapError`` (``broken`` holds the cause). 

A held connection (``hold`` is its idle timeout in seconds) has no stream: it lives in the daemon until ``close``, its idle timeout, or daemon shutdown, and other processes reach it with ``TapClient.resume``. Dropping the object without ``close`` leaves it for them. 

### <kbd>method</kbd> `TapConnection.__init__`

```python
__init__(
    client: 'TapClient',
    client_connection_id: 'str',
    name: 'str | None' = None,
    hold: 'float | None' = None
)
```

---

#### <kbd>property</kbd> TapConnection.broken

Why the liveness stream ended unexpectedly, or None while it is healthy. 

---

#### <kbd>property</kbd> TapConnection.events

The most recent liveness-stream events (at most ``MAX_EVENTS``), oldest first: ``observing`` and ``closing: <reason>``; heartbeats are not recorded. 

---

#### <kbd>property</kbd> TapConnection.usable

True while the connection can attach devices: not closed and its stream is alive. 

---

### <kbd>method</kbd> `TapConnection.attach_device`

```python
attach_device(
    serial: 'str',
    timeouts: 'Timeouts | None' = None,
    skip_driver_install: 'bool' = False,
    wait_for_device: 'float' = 0
) → Device
```

Attach ``serial``: install and start the driver (unless ``skip_driver_install``) and take the device's session. ``timeouts`` are this device's defaults; ``wait_for_device`` > 0 waits that many seconds for another session to release it. Raises ``TapError`` without an RPC once the connection is closed or broken. 

---

### <kbd>method</kbd> `TapConnection.attached_devices`

```python
attached_devices(timeouts: 'Timeouts | None' = None) → list[Device]
```

The devices attached to this connection now, as ``Device`` objects — including ones another process attached to a held connection. Nothing is re-attached: each keeps its session and generation. ``timeouts`` are this process's defaults for them. Raises ``TapError`` when the connection no longer exists at the server. 

---

### <kbd>method</kbd> `TapConnection.available_serials`

```python
available_serials() → list[str]
```

Serials a test can use, from ``TapClient.devices``: online and not quarantined, free ones first, then ones another session holds (``attach_device`` then waits for them when ``wait_for_device`` is set). Exclusive use is enforced by the session itself, so there is nothing to acquire beforehand. 

---

### <kbd>method</kbd> `TapConnection.close`

```python
close() → None
```

Disconnects (the server detaches this connection's devices), then drops the liveness stream. For a held connection this ends it for every process. Idempotent: later calls do nothing. 

---

### <kbd>method</kbd> `TapConnection.ensure_usable`

```python
ensure_usable(operation: 'str') → None
```

Raises ``TapError`` when the connection is closed or its liveness stream ended. 

---

### <kbd>method</kbd> `TapConnection.event_log`

```python
event_log(after_seq: 'int' = 0) → EventLog
```

The device calls this connection made, as the server logged them: every command except the diagnostic ``device_info`` / ``dump_hierarchy``, and the app calls that change the device (install, uninstall, force-stop, clear-data, grant, launch, cold launch). Only events with ``seq > after_seq``. A selector appears as sent, so a snapshot ref shows as the selector it named. The log is data, for turning a session into a test in any language; nothing renders code from it. Typed text is recorded verbatim, passwords included. 

Experimental: not covered by the compatibility promise; it may change in any release. 

## <kbd>class</kbd> `Endpoint`
Where a server listens and the bearer token its RPCs need (None: send no token). 

### <kbd>method</kbd> `Endpoint.__init__`

```python
__init__(address: 'str', token: 'str | None' = None) → None
```

## <kbd>function</kbd> `start_daemon`

```python
start_daemon(
    binary: 'str | None' = None,
    directory: 'pathlib.Path | None' = None,
    adb: 'str | None' = None,
    timeout: 'float' = 45.0
) → DaemonStartResult
```

``tap start``: starts the daemon in the background unless one is already running in the state dir. The executable picks the port, detaches the process and waits for ``Info`` to answer; nothing is parsed from the daemon process itself. 

## <kbd>function</kbd> `stop_daemon`

```python
stop_daemon(
    binary: 'str | None' = None,
    directory: 'pathlib.Path | None' = None,
    timeout: 'float' = 30.0
) → None
```

``tap stop``: stops the daemon recorded in the state dir; a no-op when none is running. 

## <kbd>class</kbd> `DaemonStartResult`
``address`` of the server; ``started`` is False when it was already running. 

### <kbd>method</kbd> `DaemonStartResult.__init__`

```python
__init__(address: 'str', started: 'bool') → None
```

## <kbd>class</kbd> `DeviceEntry`
One device ADB lists (``TapClient.devices()``). ``client_connection_id`` is the holding connection when this daemon holds the device; ``quarantine_reason`` says why it is ``QUARANTINED``. 

### <kbd>method</kbd> `DeviceEntry.__init__`

```python
__init__(
    serial: 'str',
    state: 'DeviceState',
    client_connection_id: 'str | None',
    quarantine_reason: 'str | None'
) → None
```

## <kbd>class</kbd> `DeviceState`
A device's state in ``TapClient.devices()``. Only ``FREE`` and ``LEASED`` devices can be attached; ``UNKNOWN`` is a state this client version does not know. 

## <kbd>class</kbd> `ServerInfo`
What the server reports about itself (``TapClient.info()``). 

### <kbd>method</kbd> `ServerInfo.__init__`

```python
__init__(
    daemon_version: 'str',
    host_build_id: 'str',
    protocol_version: 'str',
    adb_executable: 'str',
    state_dir: 'str',
    driver_available: 'bool',
    pid: 'int',
    defaults: 'ServerDefaults'
) → None
```

## <kbd>class</kbd> `ServerDefaults`
The daemon's default timeouts in seconds (``ServerInfo.defaults``). 

### <kbd>method</kbd> `ServerDefaults.__init__`

```python
__init__(
    action: 'float',
    wait: 'float',
    lifecycle: 'float',
    idle_stable: 'float',
    acquire: 'float'
) → None
```

## <kbd>class</kbd> `ConnectionEntry`
A live client connection (``TapClient.connections()``). ``hold`` is the idle timeout in seconds of a held connection, None for an observed one; ``idle`` is how long, in seconds, no call has named it (0 while one is running). 

### <kbd>method</kbd> `ConnectionEntry.__init__`

```python
__init__(
    id: 'str',
    name: 'str',
    hold: 'float | None',
    idle: 'float',
    attached_devices: 'tuple[AttachedDeviceEntry, ...]'
) → None
```

## <kbd>class</kbd> `AttachedDeviceEntry`
A device attached to a connection (``ConnectionEntry.attached_devices``). 

### <kbd>method</kbd> `AttachedDeviceEntry.__init__`

```python
__init__(attached_device_id: 'str', serial: 'str', generation: 'int') → None
```

