Metadata-Version: 2.5
Name: galley-render
Version: 0.1.0
Summary: Galley Render — JSON in, PDF out. Official Python client for the document API agents can sign themselves up for.
Project-URL: Homepage, https://galleyrender.com
Project-URL: Documentation, https://galleyrender.com/docs/sdks/python
Project-URL: Source, https://github.com/mattmueller/galley
Project-URL: Changelog, https://galleyrender.com/docs/sdks/python/changelog
Project-URL: Issues, https://galleyrender.com/support
Author-email: Galley Render <hello@galleyrender.com>
Maintainer-email: Galley Render <support@galleyrender.com>
License-Expression: MIT
License-File: LICENSE
Keywords: agents,certificate,document-api,html-to-pdf,invoice,mcp,og-image,pdf,pdf-generation,png
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
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 :: Multimedia :: Graphics :: Graphics Conversion
Classifier: Topic :: Printing
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx>=0.27
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: twine>=5; extra == 'dev'
Description-Content-Type: text/markdown

# galley-render

**JSON in, PDF out.** The official Python client for [Galley Render](https://galleyrender.com) — a
document API for agents and the programs they write. A template plus a JSON payload becomes a PDF,
PNG or JPG behind a signed URL, deterministically and cached, so the same input always returns the
same file and an identical repeat call is free.

- Sync and async clients with the same surface.
- One dependency: `httpx`.
- Typed responses from the API's own [OpenAPI spec](https://api.galleyrender.com/openapi.json),
  with `.raw` kept intact and `py.typed` shipped.
- Retries 429 and 5xx with exponential backoff and full jitter.
- Downloads signed URLs, and re-signs them when they expire.
- Starts a **50-render keyless trial** with no signup and no card.

```bash
pip install galley-render
```

Python 3.9 or newer.

## Quickstart

```python
import os
from galley_render import Galley

galley = Galley(api_key=os.environ["GALLEY_API_KEY"])

render = galley.render(
    "invoice@1",
    format="pdf",
    data={
        "invoice_number": "INV-1042",
        "customer": {"name": "Acme Corp"},
        "line_items": [{"description": "Consulting", "quantity": 12, "unit_price": 150}],
    },
)

print(render.url)                       # signed, short-lived
galley.download(render, to_file="invoice.pdf")
```

Async is the same thing with `await`:

```python
from galley_render import AsyncGalley

async with AsyncGalley() as galley:                 # reads GALLEY_API_KEY
    render = await galley.render("invoice@1", format="pdf", data=payload)
    pdf = await galley.download(render)
```

### No key yet

`start_trial()` mints a real 50-render account through Galley's MCP server and hands back a key that
works everywhere in this package and against the REST API.

```python
from galley_render import Galley, start_trial

trial = start_trial(client_id="my-app")     # reuse client_id to keep the same trial
print(trial.renders_remaining)              # 50

galley = Galley(api_key=trial.api_key)
render = galley.render("og-card", format="png", data={"title": "Hello"})
```

`async_start_trial()` is the awaitable form. To lift the limit, call the `create_account` tool on
[the MCP server](https://mcp.galleyrender.com/mcp) with an email, or sign up at
[galleyrender.com](https://galleyrender.com). The trial upgrades in place — nothing it made is lost.

## Configuration

```python
Galley(
    api_key=None,                              # default: $GALLEY_API_KEY
    base_url=None,                             # default: $GALLEY_BASE_URL, then the public API
    timeout=60.0,                              # seconds, per attempt
    max_retries=3,                             # extra attempts on 429/5xx and connection failures
    headers=None,                              # merged into every request
    http_client=None,                          # bring your own httpx.Client
)
```

Both clients are context managers, and close the `httpx` client they created:

```python
with Galley() as galley:
    ...
```

## Rendering

```python
# Small jobs finish inside the call.
render = galley.render(
    "certificate@2",                  # pin the version in anything you ship
    format="pdf",                      # pdf | png | jpg
    data={"recipient": "Dana Lee", "course": "Rope Access L1"},
    options={"page_size": "Letter", "margin": "18mm", "landscape": True},
)

# A webhook, async_=True or a large payload queues the job instead.
queued = galley.render("report", data=data, async_=True)
done = galley.renders.wait(queued.id)                     # polls with backoff

# Or do both in one call, whichever path the API takes.
finished = galley.render_and_wait("report", data=data)

# Up to 50 at a time. A bad item fails alone; the rest still run.
batch = galley.renders.batch(
    [{"template": "statement@4", "data": c} for c in customers],
    webhook_url="https://example.com/hooks/galley",
)
batch.succeeded     # the Render objects
batch.failed        # the error envelopes, each with its index
```

`render.cached` is `True` when the deterministic cache answered: the same template version, data,
options and format were rendered before, and this call cost nothing.

## Downloading

Signed URLs are short-lived; the stored file is not. `download()` takes a render, a render id or a
URL, and quietly re-signs an expired one.

```python
data = galley.download(render)                            # bytes
galley.download(render, to_file="out/invoice.pdf")
galley.download("rnd_7hq2m4x8k1bv", to_file="a.png")
```

## Templates

Templates are code: one self-contained HTML document with inline CSS and Liquid expressions, plus a
JSON Schema that is the contract for `data`. Versions are immutable and content-addressed.

```python
galley.templates.list()
invoice = galley.templates.get("invoice@3")
invoice.schema       # JSON Schema for `data`
invoice.example      # a payload that renders

galley.templates.create(
    "welcome-card",
    engine="satori",                                     # fast PNG path for simple flexbox cards
    source="<div style='display:flex'>{{ name }}</div>",
    schema={"type": "object", "required": ["name"], "properties": {"name": {"type": "string"}}},
    example={"name": "Dana"},
)

galley.templates.publish("welcome-card", source="…", message="tighter kerning")
galley.templates.versions("welcome-card")

# Free, renders nothing, and returns the same field errors a render would.
check = galley.templates.validate("welcome-card", {"name": 42})
if not check:
    for field in check.errors:
        print(field)          # name: must be string (expected string, got number)
```

## Usage

```python
usage = galley.usage()
usage.billable_units              # 1 per PNG/JPG, 1 per PDF page; cache hits are free
usage.free_renders_remaining
usage.spend_remaining_usd
usage.trial                       # not None only on a keyless trial
```

## Errors

Every failure is a `GalleyError` carrying the API's own envelope: a stable `type`, a `docs_url` and,
for validation, the field path, the expected type, what arrived and a value that would be accepted.

```python
from galley_render import GalleyError, GalleyConnectionError, GalleyTimeoutError

try:
    galley.render("invoice", data={})
except GalleyError as err:
    err.type          # "validation_error"
    err.status        # 422
    err.retryable     # False
    err.request_id    # quote this in a support mail
    for field in err.errors:
        print(field.path, field.message, field.expected, field.received, field.example)
```

| Type | Status | What to do |
|---|---|---|
| `validation_error` | 422 | Fix the named fields. `err.errors` says exactly which. |
| `invalid_request` | 400 | The request shape is wrong, not the data. |
| `authentication_error` | 401 | Missing or bad key. |
| `not_found` | 404 | No such template, version or render on this account. |
| `quota_exceeded` | 402 | Trial or free tier spent. |
| `spend_cap_exceeded` | 402 | The account's monthly cap. Raise it in the dashboard. |
| `rate_limited` | 429 | Retried for you. |
| `asset_blocked` | 400 | An image or font URL failed the SSRF policy; use a public https URL. |
| `render_failed` | 500 | The template threw. `err.body` has the detail. |

`GalleyConnectionError` means no HTTP response at all — DNS, TLS, timeout. `GalleyTimeoutError`
means `wait()` gave up while the render was still queued; the render is not lost, so poll again or
take the webhook.

## Also

- [Node SDK](https://www.npmjs.com/package/galley-render) — same surface, zero dependencies.
- [MCP server](https://mcp.galleyrender.com/mcp) — the same operations as agent tools, no key needed
  to start.
- [Docs](https://galleyrender.com/docs) · [API reference](https://galleyrender.com/docs/api) ·
  [Template language](https://galleyrender.com/docs/templates)

MIT licensed. Support: [support@galleyrender.com](mailto:support@galleyrender.com).
