Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/workflows/dependabot-uv-lock-commit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: "Dependabot: Commit uv.lock"
# Privileged companion to dependabot-uv-lock.yml.
# Triggered only after the unprivileged lock workflow succeeds. Downloads the
# artifact produced there and commits it to the PR branch. No untrusted code
# from the PR is executed here — only git operations on a known-good artifact.
permissions:
contents: read
on:
workflow_run:
workflows: ["Dependabot: Update uv.lock"]
types: [completed]
jobs:
commit:
runs-on: ubuntu-latest
if: >
github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.actor.login == 'dependabot[bot]'

permissions:
contents: write
steps:
# Checkout by exact SHA so we commit on top of what the lock job saw.
- name: Checkout PR branch at workflow HEAD SHA
uses: actions/checkout@v6
with:
ref: ${{ github.event.workflow_run.head_sha }}
token: ${{ secrets.DEPENDABOT_PAT }}
# Download after checkout so the artifact lands directly in the workspace,
# overwriting the existing uv.lock with the freshly-generated one.
# Push target is passed via env to avoid shell-injection from branch names.
- name: Download uv.lock artifact
Comment thread
dcode marked this conversation as resolved.
Dismissed
uses: actions/download-artifact@v7
with:
name: uv-lock
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Commit and push changes
env:
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"

git add uv.lock

if ! git diff --staged --quiet; then
git commit -m "chore(dependabot): synchronize uv.lock"
git push origin "HEAD:${HEAD_BRANCH}"
else
echo "uv.lock is already up to date."
fi
34 changes: 4 additions & 30 deletions .github/workflows/dependabot-uv-lock.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
name: "Dependabot: Update uv.lock"
# Runs in the unprivileged pull_request context — no secrets, no write access.
# The generated uv.lock is uploaded as an artifact for the companion
# dependabot-uv-lock-commit workflow to consume once this run completes.
permissions:
contents: read
on:
pull_request_target:
pull_request:
types: [opened, synchronize]
jobs:
lock:
Expand All @@ -24,32 +27,3 @@ jobs:
with:
name: uv-lock
path: uv.lock
commit:
needs: lock
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
permissions:
contents: write
steps:
- name: Checkout the PR branch
uses: actions/checkout@v6
with:
ref: ${{ github.head_ref }}
token: ${{ secrets.DEPENDABOT_PAT }}
- name: Download uv.lock
uses: actions/download-artifact@v7
with:
name: uv-lock
- name: Commit and push changes
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"

git add uv.lock

if ! git diff --staged --quiet; then
git commit -m "chore(dependabot): synchronize uv.lock"
git push
else
echo "uv.lock is already up to date."
fi
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ repos:
- id: yamlfmt
exclude: ^mkdocs\.yml$
- repo: https://github.com/rhysd/actionlint
rev: v1.7.10
rev: v1.7.11
hooks:
- id: actionlint
69 changes: 69 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.0] - 2026-02-23

### Breaking Changes

- Removed deprecated shim methods from `PrusaConnectClient` that were present in
the alpha releases. The service-based API is now the sole public interface:

| Removed method | Replacement |
| -------------------------------- | ----------------------------------------- |
| `client.get_printers()` | `client.printers.list_printers()` |
| `client.get_printer(uuid)` | `client.printers.get(uuid)` |
| `client.get_cameras()` | `client.cameras.list()` |
| `client.get_teams()` | `client.teams.list_teams()` |
| `client.get_team(id)` | `client.teams.get(id)` |
| `client.send_command(uuid, cmd)` | `client.printers.send_command(uuid, cmd)` |

### Added

- **Service-based API:** Resources are now accessed through dedicated service
objects on the client — `client.printers`, `client.cameras`, `client.teams`,
`client.files`, `client.jobs`, and `client.stats`.
- **`prusactl` CLI** with full subcommand coverage: `printer`, `camera`, `team`,
`job`, `file`, `stats`, and `auth`.
- **Statistics service** (`client.stats`) for per-printer material usage, print
time, planned tasks, and job success metrics.
- **Printer command discovery** — `client.printers.get_supported_commands(uuid)`
with optional disk caching and TTL.
- **Validated command execution** — `client.execute_printer_command()` validates
arguments against the printer's reported command schema before sending.
- **Camera Signal.IO signaling client** (`PrusaCameraClient`) for pan/tilt
control and image adjustment via the Prusa signaling protocol.
- **G-code metadata parser** (`client.validate_gcode(path)`) for pre-flight
checks before uploading.
- **Persistent credential caching** — CLI credentials are stored in the platform
config directory and auto-loaded by the SDK.
- **CLI output format control** — the `--format` global flag selects between
`rich` (coloured tables; default when stdout is a TTY), `plain`
(tab-separated text with no ANSI escapes or table borders; default when
stdout is not a TTY), and `json` (JSON array per table to stdout, all status
messages to stderr).
- The active format can also be set via the `output_format` key in `config.json`
or the `PRUSACTL_OUTPUT_FORMAT` environment variable; the priority is: CLI
flag → env var → config file → TTY auto-detect.

### Changed

- CLI rewritten with [Cyclopts](https://github.com/BrianPugh/cyclopts) for
richer help output and `--verbose` / `--debug` global flags.
- Pydantic models split into focused submodules under
`prusa.connect.client.models`.
- `AppConfig` is now fetched at client init time to validate that the server
supports the `PRUSA_AUTH` backend.

## [1.0.0a2] - 2025-01-13

### Changed

- Refactored CLI to use Cyclopts; split monolithic models into service modules;
added stats commands and documentation site.

## [1.0.0a0] - Initial alpha release
42 changes: 38 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ provides a frictionless, strongly-typed interface for the Prusa Connect API.
>
> This SDK is not an officially supported or endorsed product of Prusa Research.
> It is developed and maintained by an independent developer and is not
> affiliated with Prusa Research. See [Motivation & Design](#motivation-design)
> for more information.
> affiliated with Prusa Research. See
> [Motivation & Design](#motivation-and-design) for more information.

**Features:**

- **Zero-Config Authentication:** Log in once via CLI, use everywhere in Python.
- **Strong Typing:** Full Pydantic models for printers, jobs, cameras, and
files.
- **Batteries Included:** Retries, timeouts, and error handling out of the box.
- **CLI Tool:** Managing printers from the terminal.
- **CLI Tool:** Manage printers from the terminal with `prusactl`.

## Installation

Expand All @@ -36,7 +36,31 @@ Or install the lightweight library only:
pip install prusa-connect-sdk-client
```

## Motivation & Design
## Quick Start

```python
from prusa.connect.client import PrusaConnectClient

# Credentials are automatically loaded from the CLI session
# (run `prusactl auth login` first)
client = PrusaConnectClient()

for printer in client.printers.list_printers():
status = printer.printer_state or "UNKNOWN"
print(f"- {printer.name} ({status})")
```

Resources are grouped by service — `client.printers`, `client.cameras`,
`client.teams`, `client.files`, `client.jobs`, and `client.stats`.

## Documentation

Full documentation including the CLI reference, SDK quickstart, and API
reference is available at:

**<https://dcode.github.io/python-prusa-connect-sdk-client/>**

## Motivation and Design

My motivation to create this library is to provide a frictionless,
strongly-typed interface for the Prusa Connect API. I want to be able to monitor
Expand All @@ -56,3 +80,13 @@ gladly accept Prusameters towards a new Core-generation printer. 😉

My Printables Profile: :simple-printables:
[dcode](https://www.printables.com/@dcode_3006269)

## Contributing

Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for
development setup, testing, and pull request guidelines.

## License

This project is licensed under the
[GNU Affero General Public License v3.0 or later](LICENSE).
File renamed without changes.
1 change: 1 addition & 0 deletions docs/LICENSE
26 changes: 24 additions & 2 deletions docs/cli/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,23 @@ prusactl printer --help
prusactl stats --help
```

## Step 10: Change Output Format

By default, `prusactl` uses `rich` output (colored tables and text) when running
in a terminal, and `plain` output (tab-separated) when piped or redirected. You
can explicitly set the output format using the `--format` flag:

```bash
# JSON output (useful for jq or other scripts)
prusactl printer list --format json

# Plain text (tab-separated)
prusactl printer list --format plain

# Force rich output even when redirected
prusactl printer list --format rich
```

## Configuration File

Settings like default printer, team, and camera IDs are stored in a JSON file in
Expand All @@ -128,8 +145,13 @@ You can edit this file directly. Supported keys:
{
"default_printer_id": "your-printer-uuid",
"default_team_id": 12345,
"default_camera_id": "your-camera-id"
"default_camera_id": "your-camera-id",
"output_format": "json"
}
```

Environment variables (e.g. `DEFAULT_PRINTER_ID`) override file values.
Environment variables override file values:

- `PRUSACTL_OUTPUT_FORMAT`: Set to `rich`, `plain`, or `json`.
- `DEFAULT_PRINTER_ID`: Override the default printer UUID.
- `DEFAULT_TEAM_ID`: Override the default team ID.
10 changes: 5 additions & 5 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ from prusa.connect.client import PrusaConnectClient

client = PrusaConnectClient()

# Get your printer's UUID (e.g., from client.get_printers())
# Get your printer's UUID (e.g., from client.printers.list_printers())
printer_uuid = "c0ffee-uuid-1234"

# Pause the print
client.send_command(printer_uuid, "PAUSE_PRINT")
client.pause_print(printer_uuid)
print("Printer paused.")
```

Expand All @@ -32,7 +32,7 @@ Fetch the latest snapshot from your printer's camera.
from prusa.connect.client import PrusaConnectClient

client = PrusaConnectClient()
cameras = client.get_cameras()
cameras = client.cameras.list()

if cameras:
cam = cameras[0]
Expand All @@ -54,10 +54,10 @@ List files on your team's storage.
from prusa.connect.client import PrusaConnectClient

client = PrusaConnectClient()
teams = client.get_teams()
teams = client.teams.list_teams()
if teams:
my_team_id = teams[0].id
files = client.get_file_list(my_team_id)
files = client.files.list(my_team_id)

for file in files:
print(f"{file.name} ({file.size.human_readable() if file.size else 'N/A'})")
Expand Down
4 changes: 2 additions & 2 deletions docs/sdk/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ from prusa.connect.client import PrusaConnectClient
client = PrusaConnectClient()

print("My Printers:")
for printer in client.get_printers():
for printer in client.printers.list_printers():
status = printer.printer_state or "UNKNOWN"
print(f"- {printer.name} ({status})")

Expand All @@ -86,7 +86,7 @@ from prusa.connect.client.exceptions import PrusaApiError, PrusaNetworkError
client = PrusaConnectClient()

try:
printers = client.get_printers()
printers = client.printers.list_printers()
except PrusaApiError as e:
# HTTP error from the Prusa Connect API (4xx / 5xx)
print(f"API error {e.status_code}: {e}")
Expand Down
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ nav:
- Client: api/client.md
- Models: api/models.md
- Development Resources:
- Contributing: contributing.md
- Contributing: CONTRIBUTING.md
- Architecture Notes: internal_architecture.md
not_in_nav: |
/api/index.md
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
[project]
name = "prusa-connect-sdk-client" # Normalized per https://packaging.python.org/en/latest/specifications/name-normalization/
name = "prusa-connect-sdk-client"
description = "Unoriginal Prusa Connect API client for Python and CLI"
readme = "README.md"
authors = [
{ name = "Derek Ditch", email = "dcode@users.noreply.github.com"}
]
classifiers = [
"Development Status :: 4 - Beta",
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
"Programming Language :: Python :: 3",
Expand Down Expand Up @@ -108,7 +108,7 @@ packages = ["src/prusa"]

[tool.hatch.envs.default]
installer = "uv"
env-vars = { "UV_DEFAULT_INDEX" = "https://pypi.org/simple/" }
env-vars = { "UV_DEFAULT_INDEX" = "https://pypi.org/simple" }
features = ["cli"]

[tool.hatch.build.hooks.protobuf]
Expand Down
2 changes: 1 addition & 1 deletion src/prusa/connect/client/__version__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@
```
"""

__version__ = "1.0.0a2"
__version__ = "1.0.0"
Loading