Metadata-Version: 2.4
Name: zpljet
Version: 2.0.0
Summary: Official Python SDK for the ZPLJet API — fast ZPL to PDF/PNG conversion
Project-URL: Homepage, https://zpljet.com/docs
Project-URL: Documentation, https://zpljet.com/docs/sdks
Project-URL: Repository, https://github.com/zpljet/zpljet-python
Project-URL: Changelog, https://github.com/zpljet/zpljet-python/blob/main/CHANGELOG.md
Author-email: ZPLJet <support@zpljet.com>
License-Expression: MIT
License-File: LICENSE
Keywords: barcode,label,pdf,png,printing,shipping-label,zebra,zpl,zpljet
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
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: Typing :: Typed
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: build>=1; extra == 'dev'
Requires-Dist: mypy>=1.8; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Description-Content-Type: text/markdown

# zpljet

Official Python SDK for the [ZPLJet](https://zpljet.com) API — fast ZPL → PDF/PNG conversion.

[![PyPI version](https://img.shields.io/pypi/v/zpljet.svg)](https://pypi.org/project/zpljet/)
[![CI](https://github.com/zpljet/zpljet-python/actions/workflows/ci.yml/badge.svg)](https://github.com/zpljet/zpljet-python/actions/workflows/ci.yml)
[![license](https://img.shields.io/pypi/l/zpljet.svg)](https://github.com/zpljet/zpljet-python/blob/main/LICENSE)

- **Zero dependencies** — a single small client on top of the stdlib
- **Fully typed** (`py.typed`) — parameters, results, and every API error code
- **Reliable by default** — automatic retries with exponential backoff (honoring `Retry-After`), per-request timeouts, typed exceptions
- **Sync and async** — `ZplJet` for scripts and servers, `AsyncZplJet` for asyncio code
- Python ≥ 3.10; tested through 3.14

## Installation

Choose one:

```sh
pip install zpljet
uv add zpljet
poetry add zpljet
```

## Quickstart

Create an API key in the [dashboard](https://zpljet.com/dashboard) (keys look like `zpl_…`), then:

```python
import os
from pathlib import Path

from zpljet import ZplJet

zpljet = ZplJet(api_key=os.environ["ZPLJET_API_KEY"])

label = zpljet.convert(zpl="^XA^FO50,50^A0N,50,50^FDHello^FS^XZ")

Path("label.pdf").write_bytes(label.data)
```

> **Keep your API key server-side.** Anyone with the key can spend your quota.

## Usage

### Convert to PDF or PNG

`convert()` accepts every parameter of [`POST /v1/convert`](https://zpljet.com/docs/api-reference):

```python
label = zpljet.convert(
    zpl="^XA^FO50,50^A0N,50,50^FDHello^FS^XZ",
    format="png",     # "pdf" (default) | "png"
    dpmm=12,          # 6 | 8 (default, 203 dpi) | 12 (300 dpi) | 24 (600 dpi)
    width_mm=101.6,   # label width, default 4 in
    height_mm=152.4,  # label height, default 6 in
)

label.data          # bytes — the file
label.content_type  # "application/pdf" | "image/png"
label.id            # conversion id (shows up in your dashboard)
```

### Hosted URLs (paid plans)

Pass `output="url"` to have ZPLJet host the file and return a public link
instead of the bytes. Files are retained for your account's retention window
(a dashboard setting, up to your plan's maximum).

```python
hosted = zpljet.convert(zpl="^XA^FO50,50^A0N,50,50^FDHello^FS^XZ", output="url")

hosted.url             # public URL to the PDF (works until the file is deleted)
hosted.pages           # pages rendered (one per ^XA…^XZ block)
hosted.retention_days  # how long the file is kept
hosted.expires_at      # when the file is deleted and the URL stops working (ISO 8601, UTC)
```

The return type narrows automatically: `output="url"` gives a `HostedLabel`,
everything else a `LabelData`.

### Async

`AsyncZplJet` has the identical interface for asyncio code:

```python
from zpljet import AsyncZplJet

zpljet = AsyncZplJet(api_key=os.environ["ZPLJET_API_KEY"])
label = await zpljet.convert(zpl="^XA^FO50,50^A0N,50,50^FDHello^FS^XZ")
```

The async client runs the dependency-free transport in a worker thread. Bound
large batches with a semaphore; see
[`examples/05_async_batch.py`](https://github.com/zpljet/zpljet-python/blob/main/examples/05_async_batch.py).

### Error handling

Every API error code maps to a dedicated exception, so you branch with
`except` — no string matching:

```python
from zpljet import (
    ZplJet,
    APIConnectionError,
    BadRequestError,
    ConversionFailedError,
    QuotaExceededError,
    RateLimitError,
)

try:
    label = zpljet.convert(zpl=zpl)
except BadRequestError as err:
    print(f"Invalid request ({err.param}): {err.message}")
except QuotaExceededError as err:
    print(f"Quota used up ({err.used}/{err.quota}), resets {err.resets_at}")
except RateLimitError as err:
    print(f"Rate limited — retry after {err.retry_after}s")
except ConversionFailedError as err:
    print(f"Engine rejected the ZPL (conversion {err.conversion_id})")
except APIConnectionError as err:
    print(f"Network problem: {err}")
```

| Exception | Status | `error.code` | Extra fields |
| --- | --- | --- | --- |
| `BadRequestError` | 400 | `invalid_request` | `param` |
| `AuthenticationError` | 401 | `missing_api_key` · `invalid_api_key` | — |
| `QuotaExceededError` | 402 | `quota_exceeded` | `plan`, `quota`, `used`, `resets_at` |
| `PermissionDeniedError` | 403 | `hosting_not_allowed` · `no_retention_enforced` | — |
| `PayloadTooLargeError` | 413 | `payload_too_large` | — |
| `RateLimitError` | 429 | `rate_limit_exceeded` | `retry_after`, `retry_at` |
| `ConversionFailedError` | 502 | `conversion_failed` | `conversion_id` |
| `ServiceUnavailableError` | 503 | `service_unavailable` | `retry_after` |
| `APIError` | any | anything else | `status`, `code`, `raw` |
| `APITimeoutError` | — | (an attempt timed out) | — |
| `APIConnectionError` | — | (request never got a response) | — |

All of these extend `ZplJetError`, and every HTTP error carries `status`,
`code`, `doc_url`, and the raw error payload in `raw`. Full code reference:
[zpljet.com/docs/errors](https://zpljet.com/docs/errors).

### Retries

Rate limits, transient 5xx responses, timeouts, and network failures retry up
to twice by default. Retries use exponential backoff and honor `Retry-After`.
`conversion_failed` is never retried.

```python
# Client-wide
zpljet = ZplJet(api_key=key, max_retries=5)

# Or per request
zpljet.convert(zpl=zpl, max_retries=0)  # fail fast
```

### Timeouts

Each attempt has a 60-second timeout by default:

```python
zpljet = ZplJet(api_key=key, timeout=10.0)

# Per request:
zpljet.convert(zpl=zpl, timeout=5.0)
```

A timed-out attempt raises `APITimeoutError` (after retries).

### Configuration

```python
zpljet = ZplJet(
    api_key="zpl_…",                     # required
    base_url="https://api.zpljet.com",   # default
    timeout=60.0,                        # per-attempt timeout, seconds
    max_retries=2,                       # automatic retries
    transport=my_transport,              # custom transport (proxies, tests)
)
```

The default transport uses stdlib `urllib`. For connection pooling, inject any
callable matching `(url, body, headers, timeout) -> TransportResponse`.

## Examples

Runnable scripts live in [`examples/`](https://github.com/zpljet/zpljet-python/tree/main/examples):

```sh
ZPLJET_API_KEY=zpl_… python examples/01_convert_to_pdf.py
```

## Contributing & development

```sh
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"

ruff check .
mypy
pytest

ZPLJET_API_KEY=zpl_… pytest tests/test_e2e.py
```

## License

[MIT](https://github.com/zpljet/zpljet-python/blob/main/LICENSE)
