Metadata-Version: 2.4
Name: pymenderio
Version: 0.1.1
Summary: Python client library for the mender.io API
License: MIT
License-File: LICENSE
Keywords: mender,iot,ota,deployment,embedded
Author: boeboe
Requires-Python: >=3.9,<4.0
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Software Distribution
Classifier: Typing :: Typed
Requires-Dist: httpx (>=0.27,<0.28)
Requires-Dist: pydantic (>=2.0,<3.0)
Project-URL: Documentation, https://pymenderio.readthedocs.io
Project-URL: Repository, https://github.com/boeboe/pymenderio
Description-Content-Type: text/markdown

# pymenderio

[![CI](https://github.com/boeboe/pymenderio/actions/workflows/ci.yml/badge.svg)](https://github.com/boeboe/pymenderio/actions/workflows/ci.yml)
[![PyPI release workflow](https://github.com/boeboe/pymenderio/actions/workflows/release.yml/badge.svg)](https://github.com/boeboe/pymenderio/actions/workflows/release.yml)
[![Coverage](https://codecov.io/gh/boeboe/pymenderio/graph/badge.svg)](https://codecov.io/gh/boeboe/pymenderio)
[![PyPI version](https://img.shields.io/pypi/v/pymenderio?logo=pypi&logoColor=white)](https://pypi.org/project/pymenderio/)
[![Python versions](https://img.shields.io/pypi/pyversions/pymenderio?logo=python&logoColor=white)](https://pypi.org/project/pymenderio/)
[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

Python client library for the [Mender](https://mender.io) API.

## Table of Contents

- [pymenderio](#pymenderio)
  - [Table of Contents](#table-of-contents)
  - [Features](#features)
  - [Installation](#installation)
  - [Quick Start](#quick-start)
    - [Synchronous Usage](#synchronous-usage)
    - [Asynchronous Usage](#asynchronous-usage)
  - [API Coverage](#api-coverage)
  - [Major Functionality Blocks](#major-functionality-blocks)
    - [Devices (`client.devices`)](#devices-clientdevices)
    - [Deployments (`client.deployments`)](#deployments-clientdeployments)
    - [Artifacts (`client.artifacts`)](#artifacts-clientartifacts)
    - [Releases (`client.releases`)](#releases-clientreleases)
    - [Inventory (`client.inventory`)](#inventory-clientinventory)
      - [Groups (`client.inventory.groups`)](#groups-clientinventorygroups)
      - [Tags (`client.inventory.tags`)](#tags-clientinventorytags)
    - [Inventory Filters v2 (`client.filters`)](#inventory-filters-v2-clientfilters)
    - [User Administration (`client.useradm`)](#user-administration-clientuseradm)
    - [Tenant Administration (`client.tenantadm`)](#tenant-administration-clienttenantadm)
    - [Device Connect (`client.deviceconnect`)](#device-connect-clientdeviceconnect)
    - [Device Configure (`client.deviceconfigure`)](#device-configure-clientdeviceconfigure)
    - [Device Monitor (`client.devicemonitor`)](#device-monitor-clientdevicemonitor)
    - [IoT Manager (`client.iot`)](#iot-manager-clientiot)
  - [Authentication](#authentication)
    - [Token File](#token-file)
    - [mender-cli Compatible Config](#mender-cli-compatible-config)
    - [Direct Token](#direct-token)
    - [Login](#login)
  - [Examples](#examples)
  - [API Reference](#api-reference)
  - [Error Handling](#error-handling)
  - [Development](#development)
  - [Release](#release)
  - [License](#license)

## Features

- **Type-safe**: Full type hints and Pydantic models for all API objects
- **Async/Sync**: Both synchronous and asynchronous clients
- **Transparent pagination**: All list operations return complete results
- **Clean API**: Pythonic interface mirroring the Mender CLI

## Installation

```bash
pip install pymenderio
```

Or with Poetry:

```bash
poetry add pymenderio
```

## Quick Start

### Synchronous Usage

```python
from pymenderio import MenderClient

# Using a token file (default: ~/.cache/mender/authtoken)
with MenderClient.from_token_file() as client:
    # List all accepted devices
    devices = client.devices.list(status="accepted")
    for device in devices:
        print(f"{device.id}: {device.identity}")

    # Create a deployment
    deployment_id = client.deployments.create(
        name="Production rollout",
        artifact_name="myapp-v2.0",
        devices=["device-id-1", "device-id-2"],
    )
    print(f"Created deployment: {deployment_id}")

# Using a token directly
with MenderClient(server="https://hosted.mender.io", token="...") as client:
    releases = client.releases.list()
```

### Asynchronous Usage

```python
import asyncio
from pymenderio import AsyncMenderClient

async def main():
    async with AsyncMenderClient.from_token_file() as client:
        # List devices
        devices = await client.devices.list(status="accepted")
        
        # Get deployment stats
        stats = await client.deployments.stats("deployment-id")
        print(f"Success: {stats.success}, Pending: {stats.pending}")

asyncio.run(main())
```

## API Coverage

Every API method documented below is available in both variants:

- Sync: `MenderClient` method call
- Async: same method name on `AsyncMenderClient`, awaited

Example pattern:

```python
# sync
from pymenderio import MenderClient

with MenderClient.from_mender_cli() as client:
        devices = client.devices.list(status="accepted")
```

```python
# async
from pymenderio import AsyncMenderClient

async with AsyncMenderClient.from_mender_cli() as client:
        devices = await client.devices.list(status="accepted")
```

Function parity reference by API family:

- Devices methods (`list`, `search`, `get`, `count`, `preauthorize`, `accept`, `reject`, `set_auth_status`, `get_auth_status`, `remove_auth_set`, `decommission`, `revoke_token`, `limits`, `license`, `auto_auth`)
    - Sync form: `client.devices.<method>(...)`
    - Async form: `await client.devices.<method>(...)`
- Deployments methods (`list`, `get`, `create`, `stats`, `stats_list`, `devices`, `log`, `try_log`, `abort`, `device_history`, `abort_device`)
    - Sync form: `client.deployments.<method>(...)`
    - Async form: `await client.deployments.<method>(...)`
- Artifacts methods (`list`, `get`, `upload`, `download`, `download_to_file`, `delete`)
    - Sync form: `client.artifacts.<method>(...)`
    - Async form: `await client.artifacts.<method>(...)`
- Releases methods (`list`, `get`, `delete`, `update`, `set_tags`, `list_tags`, `list_update_types`, `list_delta_jobs`, `get_delta_job`)
    - Sync form: `client.releases.<method>(...)`
    - Async form: `await client.releases.<method>(...)`
- Inventory methods (`list`, `get`, `count`, `get_device_group`, `set_device_group`, `clear_device_group`)
    - Sync form: `client.inventory.<method>(...)`
    - Async form: `await client.inventory.<method>(...)`
- Inventory groups methods (`list`, `devices`, `add_devices`, `remove_devices`, `delete`)
    - Sync form: `client.inventory.groups.<method>(...)`
    - Async form: `await client.inventory.groups.<method>(...)`
- Inventory tags methods (`list`, `set`, `delete`)
    - Sync form: `client.inventory.tags.<method>(...)`
    - Async form: `await client.inventory.tags.<method>(...)`
- Inventory filters v2 methods (`attributes`, `search`, `list`, `create`, `get`, `update`, `delete`, `execute`, `statistics`)
    - Sync form: `client.filters.<method>(...)`
    - Async form: `await client.filters.<method>(...)`
- User administration methods (`list_users`, `create_user`, `user_exists`, `get_user`, `update_user`, `delete_user`, `me`, `update_me`, `enable_2fa`, `disable_2fa`, `settings`, `set_settings`, `my_settings`, `set_my_settings`, `list_personal_access_tokens`, `create_personal_access_token`, `revoke_personal_access_token`, `list_roles`, `create_role`, `get_role`, `update_role`, `delete_role`, `list_permission_sets`, `create_permission_set`, `get_permission_set`, `update_permission_set`, `delete_permission_set`)
    - Sync form: `client.useradm.<method>(...)`
    - Async form: `await client.useradm.<method>(...)`
- Tenant administration methods (`list_tenants`, `create_tenant`, `me`, `delete_inactive_tenant`, `cancel_tenant`, `update_child_tenant`, `update_plan`, `init_tenant_removal`, `set_tenant_status`, `billing_products`, `billing_info`, `init_card_update`, `confirm_card_update`, `register_billing_profile`, `billing_profile`, `update_billing_profile`, `change_subscription`, `subscription`, `preview_invoice`, `stripe_secret`, `contact_support`)
    - Sync form: `client.tenantadm.<method>(...)`
    - Async form: `await client.tenantadm.<method>(...)`
- Device connect methods (`get_device`, `connect`, `check_update`, `send_inventory`, `playback`, `download`, `upload`)
    - Sync form: `client.deviceconnect.<method>(...)`
    - Async form: `await client.deviceconnect.<method>(...)`
- Device configure methods (`get`, `set`, `deploy`)
    - Sync form: `client.deviceconfigure.<method>(...)`
    - Async form: `await client.deviceconfigure.<method>(...)`
- Device monitor methods (`alerts`, `latest_alerts`, `config`, `set_alert_channel_status`)
    - Sync form: `client.devicemonitor.<method>(...)`
    - Async form: `await client.devicemonitor.<method>(...)`
- IoT manager methods (`list_integrations`, `register_integration`, `remove_integration`, `set_integration_credentials`, `unregister_device_integrations`, `device_states`, `device_state`, `set_device_state`, `events`)
    - Sync form: `client.iot.<method>(...)`
    - Async form: `await client.iot.<method>(...)`

Authentication helper parity:

- Sync: `login(...)`, `login_and_save_session(...)`
- Async: `await async_login(...)`, `await async_login_and_save_session(...)`

## Major Functionality Blocks

- Device Identity and Lifecycle Management:
    - Device listing/search, auth-set lifecycle, preauthorization, token revocation, auto-auth, decommission, and limits/license helpers.
- OTA and Release Operations:
    - Deployments lifecycle, stats and logs, release metadata/tag management, artifact operations, and server-side delta generation visibility.
- Inventory and Fleet Querying:
    - Inventory list/get/count, groups/tags management, and inventory v2 filters/search/statistics with saved filter workflows.
- Administrative and Tenant Operations:
    - User administration (users, PATs, settings, RBAC) and tenant administration (tenant lifecycle, billing/profile/subscription, support).
- Device Service Operations:
    - Device Connect, Device Configure, Device Monitor, and IoT Manager families for operational actions beyond OTA.
- Typed Ergonomics and Compatibility:
    - Pydantic models for typed payloads/queries, sync+async parity, and compatibility fallbacks for evolving backend query parameters.

### Devices (`client.devices`)

| Method | Description |
|--------|-------------|
| `list(status=...)` | List all devices |
| `search(status=..., ids=...)` | Search devices by status and IDs |
| `get(device_id)` | Get a single device |
| `count(status=...)` | Count devices |
| `preauthorize(preauth)` | Submit a preauthorized device identity |
| `accept(device_id, auth_set_id)` | Accept a device |
| `reject(device_id, auth_set_id)` | Reject a device |
| `set_auth_status(device_id, auth_set_id, status)` | Set auth set status |
| `get_auth_status(device_id, auth_set_id)` | Get auth set status |
| `remove_auth_set(device_id, auth_set_id)` | Remove auth set |
| `decommission(device_id)` | Decommission a device |
| `revoke_token(token_id)` | Revoke device API token |
| `limits()` | Get accepted device limits per tier |
| `license()` | Get device license data (CSV) |
| `auto_auth(request, signature=...)` | Automatically authenticate a device |

### Deployments (`client.deployments`)

| Method | Description |
|--------|-------------|
| `list(status=..., type=...)` | List all deployments |
| `get(deployment_id)` | Get a single deployment |
| `create(name=..., artifact_name=..., devices=...)` | Create a deployment |
| `stats(deployment_id)` | Get deployment statistics |
| `stats_list(deployment_ids)` | Get statistics for multiple deployments |
| `devices(deployment_id, status=...)` | List devices in a deployment |
| `log(deployment_id, device_id)` | Get device deployment log |
| `try_log(deployment_id, device_id)` | Get deployment log or `None` if unavailable |
| `abort(deployment_id)` | Abort a deployment |
| `device_history(device_id, status=...)` | List deployment history for a device |
| `abort_device(device_id)` | Abort active/pending deployments for a device |

### Artifacts (`client.artifacts`)

| Method | Description |
|--------|-------------|
| `list(name=..., device_type=...)` | List all artifacts |
| `get(artifact_id)` | Get a single artifact |
| `upload(file, description=...)` | Upload an artifact |
| `download(artifact_id)` | Download an artifact |
| `download_to_file(artifact_id, path)` | Download to a file |
| `delete(artifact_id)` | Delete an artifact |

### Releases (`client.releases`)

| Method | Description |
|--------|-------------|
| `list(name=..., tag=...)` | List all releases |
| `get(name)` | Get a single release |
| `delete(name or [name,...])` | Delete one or more releases |
| `update(name, update)` | Update release fields (for example notes) |
| `set_tags(name, tags)` | Replace tags for a release |
| `list_tags()` | List all release tags |
| `list_update_types()` | List all release update types |
| `list_delta_jobs(sort=...)` | List server-side delta generation jobs |
| `get_delta_job(job_id)` | Get delta generation job details |

### Inventory (`client.inventory`)

| Method | Description |
|--------|-------------|
| `list(group=..., filters=...)` | List all inventory devices |
| `get(device_id)` | Get device inventory |
| `count(group=..., filters=...)` | Count devices |
| `get_device_group(device_id)` | Get device's group |
| `set_device_group(device_id, group)` | Set device's group |
| `clear_device_group(device_id)` | Remove from group |

#### Groups (`client.inventory.groups`)

| Method | Description |
|--------|-------------|
| `list()` | List all group names |
| `devices(name)` | List device IDs in a group |
| `add_devices(name, device_ids)` | Add devices to a group |
| `remove_devices(name, device_ids)` | Remove devices from a group |
| `delete(name)` | Delete a group |

#### Tags (`client.inventory.tags`)

| Method | Description |
|--------|-------------|
| `list(device_id)` | Get device tags |
| `set(device_id, name, value)` | Set a tag |
| `delete(device_id, name)` | Delete a tag |

### Inventory Filters v2 (`client.filters`)

| Method | Description |
|--------|-------------|
| `attributes()` | List filterable inventory attributes |
| `search(params=...)` | Search devices with filter predicates |
| `list()` | List saved filters |
| `create(definition)` | Create a saved filter |
| `get(filter_id)` | Get a saved filter definition |
| `update(filter_id, definition)` | Update a saved filter |
| `delete(filter_id)` | Delete a saved filter |
| `execute(filter_id)` | Search devices using a saved filter |
| `statistics()` | Get inventory statistics |

### User Administration (`client.useradm`)

| Method | Description |
|--------|-------------|
| `list_users()` | List users in the current tenant |
| `create_user(user)` | Create a user |
| `user_exists(email)` | Check whether a user exists |
| `get_user(user_id)` | Get user by ID |
| `update_user(user_id, user)` | Update user by ID |
| `delete_user(user_id)` | Delete user by ID |
| `me()` | Get current user information |
| `update_me(user)` | Update current user information |
| `enable_2fa(user_id="me")` | Enable 2FA for a user |
| `disable_2fa(user_id="me")` | Disable 2FA for a user |
| `settings()` | Get global settings and ETag |
| `set_settings(settings, if_match=...)` | Set global settings |
| `my_settings()` | Get current user settings and ETag |
| `set_my_settings(settings, if_match=...)` | Set current user settings |
| `list_personal_access_tokens()` | List personal access tokens |
| `create_personal_access_token(request)` | Create a personal access token |
| `revoke_personal_access_token(token_id)` | Revoke a personal access token |
| `list_roles()` | List RBAC roles (v2) |
| `create_role(role)` | Create RBAC role (v2) |
| `get_role(role_id)` | Get RBAC role (v2) |
| `update_role(role_id, role)` | Update RBAC role (v2) |
| `delete_role(role_id)` | Delete RBAC role (v2) |
| `list_permission_sets()` | List permission sets (v2) |
| `create_permission_set(permission_set)` | Create permission set (v2) |
| `get_permission_set(permission_set_id)` | Get permission set (v2) |
| `update_permission_set(permission_set_id, permission_set)` | Update permission set (v2) |
| `delete_permission_set(permission_set_id)` | Delete permission set (v2) |

### Tenant Administration (`client.tenantadm`)

| Method | Description |
|--------|-------------|
| `list_tenants()` | List child tenants |
| `create_tenant(tenant)` | Create a child tenant |
| `me()` | Get current tenant |
| `delete_inactive_tenant(tenant_id)` | Remove inactive tenant |
| `cancel_tenant(tenant_id, request)` | Request tenant cancellation |
| `update_child_tenant(tenant_id, tenant)` | Update child tenant |
| `update_plan(tenant_id, request)` | Request plan/add-on change |
| `init_tenant_removal(tenant_id)` | Start asynchronous tenant removal |
| `set_tenant_status(tenant_id, status)` | Set tenant status |
| `billing_products()` | Get billing product info |
| `billing_info()` | Get billing info summary |
| `init_card_update()` | Initialize card update flow |
| `confirm_card_update(intent_id)` | Confirm card update |
| `register_billing_profile(profile)` | Register billing profile |
| `billing_profile()` | Get billing profile |
| `update_billing_profile(profile)` | Update billing profile |
| `change_subscription(request)` | Request subscription change |
| `subscription()` | Get current subscription |
| `preview_invoice(request)` | Preview upcoming invoice |
| `stripe_secret()` | Get Stripe client secret |
| `contact_support(request)` | Send message to support |

### Device Connect (`client.deviceconnect`)

| Method | Description |
|--------|-------------|
| `get_device(device_id)` | Get current device connection state |
| `connect(device_id, headers=...)` | Initiate websocket upgrade handshake for interactive session |
| `check_update(device_id)` | Trigger check-update on device |
| `send_inventory(device_id)` | Trigger send-inventory on device |
| `playback(session_id, sleep_ms=..., headers=...)` | Initiate websocket upgrade handshake for session playback |
| `download(device_id, path=...)` | Download file content and metadata from device |
| `upload(device_id, path=..., fileobj=...)` | Upload file to device |

Typed helper support:

- `upload(..., request=UploadFileRequest(...))` for structured upload metadata and optional inline bytes content.

### Device Configure (`client.deviceconfigure`)

| Method | Description |
|--------|-------------|
| `get(device_id)` | Get device configuration |
| `set(device_id, configuration)` | Replace device configuration |
| `deploy(device_id, request=...)` | Trigger configuration deployment |

### Device Monitor (`client.devicemonitor`)

| Method | Description |
|--------|-------------|
| `alerts(device_id, ...)` | List alerts for a device |
| `latest_alerts(device_id, ...)` | List latest alerts for a device |
| `config(device_id)` | List monitor check configuration for a device |
| `set_alert_channel_status(name, enabled=...)` | Enable/disable a global alert channel |

Typed helper support:

- `alerts(..., query=AlertsQuery(...))`
- `latest_alerts(..., query=LatestAlertsQuery(...))`

### IoT Manager (`client.iot`)

| Method | Description |
|--------|-------------|
| `list_integrations()` | List configured cloud integrations |
| `register_integration(integration)` | Register a new integration |
| `remove_integration(integration_id)` | Remove a configured integration |
| `set_integration_credentials(integration_id, credentials)` | Replace integration credentials |
| `unregister_device_integrations(device_id)` | Remove all integrations from a device |
| `device_states(device_id)` | Get states for all integrations for a device |
| `device_state(device_id, integration_id)` | Get state for one integration |
| `set_device_state(device_id, integration_id, state)` | Replace desired state for one integration |
| `events(integration_id=...)` | List integration events |

Typed helper support:

- `events(..., query=EventsQuery(...))` for typed `integration_id`, `page`, and `per_page`
- `list_integrations(query=IntegrationsQuery(...))` for typed `page` and `per_page`

`IntegrationsQuery(...)` also supports additive client-side filters:

- `provider`
- `scope`
- `description_contains`

Compatibility fallback strategy for future backend query params:

- Use `future_params={...}` to pass through potential future server query params.
- If backend rejects unknown params with HTTP 400/422, pymenderio retries with stable params when `allow_unsupported_params_fallback=True` (default), then applies client-side typed filters.

Credential variants for `client.iot.register_integration(...)` and `client.iot.set_integration_credentials(...)`:

- HTTP webhook credentials (`type="http"`)
- Azure IoT Hub shared access secret (`type="sas"`)
- AWS credentials (`type="aws"`)

Typed helper support for integration registration:

- `register_integration(IntegrationCreateRequest(provider=..., credentials=..., scopes=...))`

IoT event payload parsing for `client.iot.events(...)`:

- `device-provisioned`, `device-decommissioned`, `device-status-changed` map `event.data` to `DeviceAuthEvent`
- `device-inventory-changed` maps `event.data` to `DeviceInventoryEvent`

Typed helper support for state updates:

- `set_device_state(..., DeviceStateUpdate(desired=...))`

## Authentication

### Token File

By default, pymenderio reads the JWT token from the same cache path used by mender-cli:
`~/.cache/mender/authtoken`.

```python
client = MenderClient.from_token_file()
# or specify a custom token file path
client = MenderClient.from_token_file("/path/to/authtoken")
```

### mender-cli Compatible Config

pymenderio can also read mender-cli configuration from `~/.mender-clirc`
(JSON containing `server` and `username`) and combine it with the default token path:

```python
client = MenderClient.from_mender_cli()
```

### Direct Token

```python
client = MenderClient(
    server="https://hosted.mender.io",
    token="your-jwt-token",
)
```

### Login

```python
from pymenderio.auth import login, write_token_file

token = login(
    server="https://hosted.mender.io",
    email="user@example.com",
    password="password",
    totp_code="123456",  # Optional 2FA code
)

# Optionally save for future use
write_token_file(token)
```

To persist server and username in mender-cli config format:

```python
from pymenderio.auth import write_config_file

write_config_file(
    server="https://eu.hosted.mender.io",
    username="user@example.com",
)
```

For a single call that logs in and saves both config and token in mender-cli
compatible locations:

```python
from pymenderio.auth import login_and_save_session

token = login_and_save_session(
    server="https://eu.hosted.mender.io",
    email="user@example.com",
    password="password",
)
```

## Examples

The repository includes runnable sync/async script pairs under [examples](examples).

Parity script pairs:

- Device list: [examples/sync_list_devices.py](examples/sync_list_devices.py) and [examples/async_list_devices.py](examples/async_list_devices.py)
- Inventory list: [examples/sync_list_inventory.py](examples/sync_list_inventory.py) and [examples/async_list_inventory.py](examples/async_list_inventory.py)
- Artifacts list: [examples/sync_list_artifacts.py](examples/sync_list_artifacts.py) and [examples/async_list_artifacts.py](examples/async_list_artifacts.py)
- Releases list: [examples/sync_list_releases.py](examples/sync_list_releases.py) and [examples/async_list_releases.py](examples/async_list_releases.py)
- Deployments list: [examples/sync_list_deployments.py](examples/sync_list_deployments.py) and [examples/async_list_deployments.py](examples/async_list_deployments.py)
- Get resource by kind: [examples/sync_get_resource.py](examples/sync_get_resource.py) and [examples/async_get_resource.py](examples/async_get_resource.py)
- Deployment details: [examples/sync_deployment_details.py](examples/sync_deployment_details.py) and [examples/async_deployment_details.py](examples/async_deployment_details.py)
- Inventory filters v2: [examples/sync_inventory_v2_filters.py](examples/sync_inventory_v2_filters.py) and [examples/async_inventory_v2_filters.py](examples/async_inventory_v2_filters.py)
- Deployment delta jobs: [examples/sync_deployment_delta_jobs.py](examples/sync_deployment_delta_jobs.py) and [examples/async_deployment_delta_jobs.py](examples/async_deployment_delta_jobs.py)
- Device auth search/preauthorize: [examples/sync_devauth_search_and_preauth.py](examples/sync_devauth_search_and_preauth.py) and [examples/async_devauth_search_and_preauth.py](examples/async_devauth_search_and_preauth.py)
- User administration basics: [examples/sync_user_admin_basics.py](examples/sync_user_admin_basics.py) and [examples/async_user_admin_basics.py](examples/async_user_admin_basics.py)
- Tenant administration basics: [examples/sync_tenant_admin_basics.py](examples/sync_tenant_admin_basics.py) and [examples/async_tenant_admin_basics.py](examples/async_tenant_admin_basics.py)
- Device services overview: [examples/sync_device_services_overview.py](examples/sync_device_services_overview.py) and [examples/async_device_services_overview.py](examples/async_device_services_overview.py)
- Login and save session: [examples/sync_login_and_save_session.py](examples/sync_login_and_save_session.py) and [examples/async_login_and_save_session.py](examples/async_login_and_save_session.py)
- Real API smoke (combined sync + async probe): [examples/real_api_smoke.py](examples/real_api_smoke.py)

See [EXAMPLES.md](EXAMPLES.md) for:

- script-by-script usage
- live API validation results
- model compatibility notes

See [API.md](API.md) for the full public SDK reference:

- all public methods
- all public classes
- all public errors

## API Reference

For a complete surface inventory, see [API.md](API.md).

## Error Handling

```python
from pymenderio import MenderClient
from pymenderio.exceptions import (
    AuthenticationError,
    NotFoundError,
    ValidationError,
)

with MenderClient.from_token_file() as client:
    try:
        device = client.devices.get("nonexistent-id")
    except NotFoundError:
        print("Device not found")
    except AuthenticationError:
        print("Invalid or expired token")
```

## Development

```bash
# Show available targets
make help

# Create local virtual environment and install dependencies
make venv

# Run tests in Docker
make test

# Lint and type-check in Docker
make lint
make typecheck

# Run all checks (lint, typecheck, test)
make check

# Remove local virtual environment
make venv-clean
```

If you prefer Poetry directly:

```bash
# Install dependencies
poetry install

# Run tests
poetry run pytest

# Type checking
poetry run mypy src/

# Linting
poetry run ruff check src/
```

## Release

GitHub Actions workflows in [.github/workflows/release-testpypi.yml](.github/workflows/release-testpypi.yml)
and [.github/workflows/release-pypi.yml](.github/workflows/release-pypi.yml)
handle package publishing.

One-time setup:

- Configure a Trusted Publisher in TestPyPI for
    [.github/workflows/release-testpypi.yml](.github/workflows/release-testpypi.yml).
- Configure a Trusted Publisher in PyPI for
    [.github/workflows/release-pypi.yml](.github/workflows/release-pypi.yml).

Release flow:

- Publish a prerelease to TestPyPI with a prerelease tag, for example:

```bash
git tag v0.1.1-rc1
git push origin v0.1.1-rc1
```

- Publish a final release to PyPI with a stable tag:

```bash
git tag v0.1.1
git push origin v0.1.1
```

The PyPI workflow validates that the tag version matches the package version in
[pyproject.toml](pyproject.toml).

## License

MIT License - see [LICENSE](LICENSE) for details.

