Metadata-Version: 2.4
Name: wisent-wire-sdk
Version: 0.5.1
Summary: Python client library for the Wisent Wire hardware testing platform API
Author-email: Wisent Wire <info@wisent-wire.com>
License-Expression: MIT
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.28
Requires-Dist: tenacity>=8.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: responses>=0.23; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Requires-Dist: mypy>=1.10; extra == "dev"
Requires-Dist: types-requests>=2.28; extra == "dev"
Dynamic: license-file

# Wisent Wire Python SDK

Python client library for the [Wisent Wire](https://wisent-wire.com) hardware
testing platform. A single typed client wraps the whole REST API — device
control, firmware flashing, on-target debugging, serial and GPIO I/O,
telemetry, reservations, and organization management — with automatic auth,
typed responses, typed errors, and convenience pollers for asynchronous
operations.

## Installation

```bash
pip install wisent-wire-sdk
```

## Quick start

```python
from wisentwire import WisentWireClient

# API key (recommended — scripts, CI, MCP)
client = WisentWireClient(
    base_url="https://app.wisent-wire.com",
    api_key="wwk_...",   # or set the WISENTWIRE_API_KEY env var and omit api_key
)

# Cognito (email + password)
client = WisentWireClient(
    base_url="https://app.wisent-wire.com",
    email="user@company.com",
    password="secret",
)

# Pre-existing bearer token
client = WisentWireClient(
    base_url="https://app.wisent-wire.com",
    token="eyJhbGciOi...",
)
```

Create an API key from the web app under **Settings → API keys**. Keys are
shown once — store them securely. Default expiry is 1 year; up to 10 active
keys per user. Auth resolution precedence: explicit `api_key` >
`WISENTWIRE_API_KEY` env > `token` > `email` + `password`.

## Features

- **Wires** — list, inspect, create, update, delete wires; check availability.
- **Power supply** — set state / voltage / current, read the device shadow and
  lab tools, toggle and read power telemetry.
- **Firmware & flashing** — upload firmware, dispatch a flash job, poll its
  status by id, stream flasher logs.
- **Flasher catalog** — list programmers, targets, interfaces, toolchains, and
  the valid launch-command combinations.
- **Debug (GDB)** — start / stop an on-target GDB server, read live session
  state, poll the dispatched job by id.
- **UART / RS485** — send hex or ASCII, read the console (per-protocol,
  shared across backend instances), wait for an RX match.
- **GPIO** — read the shadow, drive outputs, set input pull policy, name pins,
  toggle and read input telemetry.
- **Device registry** — list registered and connected USB devices, register
  and label devices, trigger a device scan.
- **Frame definitions** — CRUD for protocol frame definitions and their
  commands.
- **Reservations** — list, create, and delete calendar reservations.
- **Organization** — read the org, manage members and roles, invitations, and
  membership requests.
- **Typed errors & pollers** — every non-2xx maps to a `WisentWireError`
  subclass; `wait_*` helpers poll asynchronous jobs with a configurable timeout.

## Usage

### Wires

```python
wires = client.list_wisentwires()
ww = client.get_wisentwire(ww_id=1)
print(ww.name, ww.is_virtual)
```

### Power supply

```python
client.set_power_supply(ww_id=1, state="ON", voltage=5.0, current=2.0)
shadow = client.wait_shadow_state(ww_id=1, expected_state="ON")
print(shadow.reported.voltage_setpoint)

client.set_power_supply_telemetry(ww_id=1, enabled=True)
readings = client.get_power_telemetry(ww_id=1)
```

### Firmware & flashing

```python
# Upload firmware (one call: presigned URL -> S3 -> confirm)
fw = client.upload_firmware("app.bin", firmware_bytes)

# Resolve a valid (target, programmer, interface, toolchain) combination
cmd = client.list_launch_commands()[0]

# Dispatch a flash job and wait for it by job id
dispatch = client.flash(
    ww_id=1,
    firmware_id=fw.id,
    target_id=cmd.target_id,
    programmer_id=cmd.programmer_id,
    interface_id=cmd.interface_id,
    toolchain_id=cmd.toolchain_id,
)
status = client.wait_flash_complete(ww_id=1, job_id=dispatch.job_id)
print(status.status)

for entry in client.get_flasher_logs(ww_id=1):
    print(entry.source, entry.data)
```

### Debug (GDB)

```python
cmd = client.list_launch_commands()[0]
dispatch = client.start_debug(
    ww_id=1,
    target_id=cmd.target_id,
    programmer_id=cmd.programmer_id,
    interface_id=cmd.interface_id,
    toolchain_id=cmd.toolchain_id,
    port=3333,
)
state = client.get_debug_state(ww_id=1)
print(state.active, state.local_ip, state.port)
client.stop_debug(ww_id=1)
```

### UART / RS485

```python
client.send_uart_ascii(ww_id=1, text="HELLO")
msg = client.wait_for_uart_rx(ww_id=1, match_hex="48454C4C4F")
print(msg.direction, msg.protocol, msg.bytes_hex)
```

### GPIO

```python
from wisentwire import GpioPullPolicy

shadow = client.get_gpio_shadow(ww_id=1)
client.set_gpio_output(ww_id=1, pin=0, is_high=True)
client.set_gpio_input_pull_policy(ww_id=1, pin=0, pull_policy=GpioPullPolicy.UP)

# Friendly pin labels
names = client.get_gpio_names(ww_id=1)
names.outputs[0] = "LED"
client.update_gpio_names(ww_id=1, names=names)

# Input telemetry
client.set_gpio_telemetry(ww_id=1, enabled=True)
for r in client.get_gpio_telemetry(ww_id=1):
    print(r.pin, r.high, r.timestamp_micros)
```

### Device registry

```python
devices = client.list_registered_devices()
connected = client.list_connected_devices(ww_id=1)
client.trigger_device_scan(ww_id=1)
```

### Frame definitions

```python
frames = client.list_frame_definitions()
commands = client.list_commands(frame_def_id=frames[0].id)
```

### Reservations

```python
reservations = client.list_reservations()
client.create_reservation(
    ww_id=1,
    start_at="2026-06-20T09:00:00Z",
    end_at="2026-06-20T10:00:00Z",
)
```

### Organization

```python
org = client.get_organization()
members = client.list_members()
```

## Error handling

Every API error raises a typed exception inheriting from `WisentWireError`:

```python
from wisentwire import NotFoundError

try:
    client.get_wisentwire(ww_id=999)
except NotFoundError as e:
    print(f"Wire not found: {e}")
```

| HTTP status | Exception                 |
|-------------|---------------------------|
| 400         | `BadRequestError`         |
| 401         | `AuthenticationError`     |
| 403         | `ForbiddenError`          |
| 404         | `NotFoundError`           |
| 409         | `ConflictError`           |
| 429         | `RateLimitError`          |
| 503         | `ServiceUnavailableError` |

## Development

```bash
pip install -e ".[dev]"
pytest tests/ -v
```

## Requirements

- Python >= 3.10
- `requests` >= 2.28
- `tenacity` >= 8.0
