Metadata-Version: 2.4
Name: pyprusalink
Version: 3.0.0
Summary: Library to interact with PrusaLink v2
Author-email: The Home Assistant Authors <hello@home-assistant.io>
License: Apache-2.0
Platform: any
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Environment :: Console
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Home Automation
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx
Provides-Extra: test
Requires-Dist: pytest>=9.0.3; extra == "test"
Requires-Dist: pytest-asyncio>=1.3.0; extra == "test"
Requires-Dist: respx>=0.23.1; extra == "test"
Provides-Extra: lint
Requires-Dist: black==26.3.1; extra == "lint"
Requires-Dist: flake8==7.3.0; extra == "lint"
Requires-Dist: isort==8.0.1; extra == "lint"
Requires-Dist: mypy==2.0.0; extra == "lint"
Dynamic: license-file

# pyprusalink

[![PyPI](https://img.shields.io/pypi/v/pyprusalink.svg)](https://pypi.org/project/pyprusalink/)
[![Python](https://img.shields.io/pypi/pyversions/pyprusalink.svg)](https://pypi.org/project/pyprusalink/)
[![License](https://img.shields.io/pypi/l/pyprusalink.svg)](https://github.com/home-assistant-libs/pyprusalink/blob/main/LICENSE)

Async Python client for the [PrusaLink HTTP API](https://github.com/prusa3d/Prusa-Link-Web/blob/master/spec/openapi.yaml). Covers both the current `/api/v1/...` endpoints and a few legacy paths (`/api/version`, `/api/printer`).

The primary consumer is the [Home Assistant `prusalink` integration](https://www.home-assistant.io/integrations/prusalink/), and API shape decisions are weighted toward serving that integration. Today the library does not perform runtime validation, retry, or caching at the boundary — consumers handle those — but the shape may evolve as the integration's needs do.

## Requirements

- Python 3.11+
- A PrusaLink-enabled Prusa printer (bundled with Prusa-Firmware-Buddy on Core One, MK4, MK3.9, MK3.5, MINI, XL; or standalone PrusaLink on an RPi)

The library is async-only and is built on [`httpx`](https://www.python-httpx.org/). Digest authentication is handled by `httpx.DigestAuth` under the hood.

## Installation

```bash
pip install pyprusalink
```

## Quickstart

```python
import asyncio

import httpx
from pyprusalink import PrusaLink


async def main() -> None:
    async with httpx.AsyncClient() as client:
        api = PrusaLink(client, "http://prusa.local", "maker", "<password>")

        info = await api.get_info()
        print(info.get("name"), info.get("serial"))

        status = await api.get_status()
        print(status["printer"]["state"])

        job = await api.get_job()
        if job is not None:
            print(f"{job['progress']:.0f}% — {job['file']['display_name']}")


asyncio.run(main())
```

The username on bundled-firmware printers is `maker`. The password is the API key shown under **Settings → PrusaLink** on the printer.

## Public API

| Method | Returns | Notes |
| --- | --- | --- |
| `get_version()` | `VersionInfo` | `/api/version` — firmware, hostname, API version |
| `get_info()` | `PrinterInfo` | `/api/v1/info` — serial, model, location, capabilities |
| `get_status()` | `PrinterStatus` | `/api/v1/status` — printer state plus embedded job/storage/transfer/camera |
| `get_job()` | `JobInfo \| None` | `/api/v1/job` — `None` when no job is running |
| `get_storage()` | `list[Storage]` | `/api/v1/storage` — available storage devices |
| `get_transfer()` | `Transfer \| None` | `/api/v1/transfer` — `None` when no transfer is in progress |
| `cancel_transfer(transfer_id)` | `None` | Cancel an active upload |
| `cancel_job(job_id)` | `None` | Cancel a print |
| `pause_job(job_id)` | `None` | Pause a running print |
| `resume_job(job_id)` | `None` | Resume a paused print |
| `continue_job(job_id)` | `None` | Continue after the printer enters the `ATTENTION` state (e.g. timelapse capture) |
| `get_legacy_printer()` | `LegacyPrinterStatus` | `/api/printer` — legacy endpoint, used for `material` |
| `get_file(path)` | `bytes` | Fetch raw resources such as thumbnails referenced from `JobFilePrint.refs` |

### Errors

All HTTP errors map to subclasses of `PrusaLinkError`:

| Exception | When |
| --- | --- |
| `InvalidAuth` | 401 — wrong credentials |
| `NotFound` | 404 — resource missing |
| `Conflict` | 409 — action conflicts with current printer state (e.g. cancel while idle) |

```python
from pyprusalink.types import Conflict

try:
    await api.cancel_job(42)
except Conflict:
    ...  # printer wasn't in a cancellable state
```

## Type contract

Return types are `TypedDict`s declared in [`pyprusalink/types.py`](pyprusalink/types.py). Two conventions worth knowing:

1. **`NotRequired[T]` for optional fields.** The PrusaLink API omits absent fields rather than returning `null`, so optional fields are `NotRequired[T]` and consumers must use `.get(...)` or membership checks. Indexing a missing key raises `KeyError`.

2. **`T | None` for return types when the resource may be absent.** `get_job()` and `get_transfer()` return `None` (not an empty `dict`) when there's no active job/transfer.

The library does not perform runtime validation. `response.json()` results are wrapped in `typing.cast(...)` against the declared `TypedDict`. This is a deliberate choice — for a thin wrapper, the runtime overhead and dependency footprint of pydantic/msgspec is not worth it. If you need runtime validation, layer it on top.

## Versioning

[Semantic versioning](https://semver.org/). Changes to `TypedDict` shapes that affect strict-typed consumers are counted as breaking and require a major version bump.

## Development

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[test,lint]"

# unit tests (default — integration tests are excluded)
pytest tests/

# lint
black --check pyprusalink/ tests/
flake8 pyprusalink/ tests/
isort --check pyprusalink/ tests/
mypy
```

### Integration tests

Integration tests live in `tests/test_integration.py` and run against a real printer. They are opt-in via the `integration` pytest marker:

```bash
PRUSALINK_HOST=http://prusa.local \
PRUSALINK_USERNAME=maker \
PRUSALINK_PASSWORD=<api-key> \
pytest tests/test_integration.py -m integration
```

## License

[Apache-2.0](LICENSE).
