galley-render 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- galley_render-0.1.0/.gitignore +19 -0
- galley_render-0.1.0/LICENSE +21 -0
- galley_render-0.1.0/PKG-INFO +252 -0
- galley_render-0.1.0/README.md +216 -0
- galley_render-0.1.0/pyproject.toml +63 -0
- galley_render-0.1.0/src/galley_render/__init__.py +57 -0
- galley_render-0.1.0/src/galley_render/_client.py +494 -0
- galley_render-0.1.0/src/galley_render/_errors.py +167 -0
- galley_render-0.1.0/src/galley_render/_models.py +347 -0
- galley_render-0.1.0/src/galley_render/_transport.py +251 -0
- galley_render-0.1.0/src/galley_render/_trial.py +164 -0
- galley_render-0.1.0/src/galley_render/py.typed +0 -0
- galley_render-0.1.0/tests/conftest.py +109 -0
- galley_render-0.1.0/tests/test_async_client.py +66 -0
- galley_render-0.1.0/tests/test_client.py +206 -0
- galley_render-0.1.0/tests/test_transport.py +169 -0
- galley_render-0.1.0/tests/test_trial.py +95 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
.env
|
|
2
|
+
.env.*
|
|
3
|
+
!.env.example
|
|
4
|
+
*.swp
|
|
5
|
+
notes
|
|
6
|
+
node_modules/
|
|
7
|
+
dist/
|
|
8
|
+
.turbo/
|
|
9
|
+
coverage/
|
|
10
|
+
.dev-storage/
|
|
11
|
+
test-output/
|
|
12
|
+
*.tsbuildinfo
|
|
13
|
+
__pycache__/
|
|
14
|
+
.venv/
|
|
15
|
+
*.egg-info/
|
|
16
|
+
*.pyc
|
|
17
|
+
|
|
18
|
+
# MCP registry Ed25519 private key material (never commit)
|
|
19
|
+
ops/listings/.secrets/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Galley Render
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: galley-render
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Galley Render — JSON in, PDF out. Official Python client for the document API agents can sign themselves up for.
|
|
5
|
+
Project-URL: Homepage, https://galleyrender.com
|
|
6
|
+
Project-URL: Documentation, https://galleyrender.com/docs/sdks/python
|
|
7
|
+
Project-URL: Source, https://github.com/mattmueller/galley
|
|
8
|
+
Project-URL: Changelog, https://galleyrender.com/docs/sdks/python/changelog
|
|
9
|
+
Project-URL: Issues, https://galleyrender.com/support
|
|
10
|
+
Author-email: Galley Render <hello@galleyrender.com>
|
|
11
|
+
Maintainer-email: Galley Render <support@galleyrender.com>
|
|
12
|
+
License-Expression: MIT
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Keywords: agents,certificate,document-api,html-to-pdf,invoice,mcp,og-image,pdf,pdf-generation,png
|
|
15
|
+
Classifier: Development Status :: 4 - Beta
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
|
|
25
|
+
Classifier: Topic :: Printing
|
|
26
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
|
+
Classifier: Typing :: Typed
|
|
28
|
+
Requires-Python: >=3.9
|
|
29
|
+
Requires-Dist: httpx>=0.27
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
34
|
+
Requires-Dist: twine>=5; extra == 'dev'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# galley-render
|
|
38
|
+
|
|
39
|
+
**JSON in, PDF out.** The official Python client for [Galley Render](https://galleyrender.com) — a
|
|
40
|
+
document API for agents and the programs they write. A template plus a JSON payload becomes a PDF,
|
|
41
|
+
PNG or JPG behind a signed URL, deterministically and cached, so the same input always returns the
|
|
42
|
+
same file and an identical repeat call is free.
|
|
43
|
+
|
|
44
|
+
- Sync and async clients with the same surface.
|
|
45
|
+
- One dependency: `httpx`.
|
|
46
|
+
- Typed responses from the API's own [OpenAPI spec](https://api.galleyrender.com/openapi.json),
|
|
47
|
+
with `.raw` kept intact and `py.typed` shipped.
|
|
48
|
+
- Retries 429 and 5xx with exponential backoff and full jitter.
|
|
49
|
+
- Downloads signed URLs, and re-signs them when they expire.
|
|
50
|
+
- Starts a **50-render keyless trial** with no signup and no card.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install galley-render
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Python 3.9 or newer.
|
|
57
|
+
|
|
58
|
+
## Quickstart
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import os
|
|
62
|
+
from galley_render import Galley
|
|
63
|
+
|
|
64
|
+
galley = Galley(api_key=os.environ["GALLEY_API_KEY"])
|
|
65
|
+
|
|
66
|
+
render = galley.render(
|
|
67
|
+
"invoice@1",
|
|
68
|
+
format="pdf",
|
|
69
|
+
data={
|
|
70
|
+
"invoice_number": "INV-1042",
|
|
71
|
+
"customer": {"name": "Acme Corp"},
|
|
72
|
+
"line_items": [{"description": "Consulting", "quantity": 12, "unit_price": 150}],
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
print(render.url) # signed, short-lived
|
|
77
|
+
galley.download(render, to_file="invoice.pdf")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Async is the same thing with `await`:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from galley_render import AsyncGalley
|
|
84
|
+
|
|
85
|
+
async with AsyncGalley() as galley: # reads GALLEY_API_KEY
|
|
86
|
+
render = await galley.render("invoice@1", format="pdf", data=payload)
|
|
87
|
+
pdf = await galley.download(render)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### No key yet
|
|
91
|
+
|
|
92
|
+
`start_trial()` mints a real 50-render account through Galley's MCP server and hands back a key that
|
|
93
|
+
works everywhere in this package and against the REST API.
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from galley_render import Galley, start_trial
|
|
97
|
+
|
|
98
|
+
trial = start_trial(client_id="my-app") # reuse client_id to keep the same trial
|
|
99
|
+
print(trial.renders_remaining) # 50
|
|
100
|
+
|
|
101
|
+
galley = Galley(api_key=trial.api_key)
|
|
102
|
+
render = galley.render("og-card", format="png", data={"title": "Hello"})
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`async_start_trial()` is the awaitable form. To lift the limit, call the `create_account` tool on
|
|
106
|
+
[the MCP server](https://mcp.galleyrender.com/mcp) with an email, or sign up at
|
|
107
|
+
[galleyrender.com](https://galleyrender.com). The trial upgrades in place — nothing it made is lost.
|
|
108
|
+
|
|
109
|
+
## Configuration
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
Galley(
|
|
113
|
+
api_key=None, # default: $GALLEY_API_KEY
|
|
114
|
+
base_url=None, # default: $GALLEY_BASE_URL, then the public API
|
|
115
|
+
timeout=60.0, # seconds, per attempt
|
|
116
|
+
max_retries=3, # extra attempts on 429/5xx and connection failures
|
|
117
|
+
headers=None, # merged into every request
|
|
118
|
+
http_client=None, # bring your own httpx.Client
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Both clients are context managers, and close the `httpx` client they created:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
with Galley() as galley:
|
|
126
|
+
...
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Rendering
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
# Small jobs finish inside the call.
|
|
133
|
+
render = galley.render(
|
|
134
|
+
"certificate@2", # pin the version in anything you ship
|
|
135
|
+
format="pdf", # pdf | png | jpg
|
|
136
|
+
data={"recipient": "Dana Lee", "course": "Rope Access L1"},
|
|
137
|
+
options={"page_size": "Letter", "margin": "18mm", "landscape": True},
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# A webhook, async_=True or a large payload queues the job instead.
|
|
141
|
+
queued = galley.render("report", data=data, async_=True)
|
|
142
|
+
done = galley.renders.wait(queued.id) # polls with backoff
|
|
143
|
+
|
|
144
|
+
# Or do both in one call, whichever path the API takes.
|
|
145
|
+
finished = galley.render_and_wait("report", data=data)
|
|
146
|
+
|
|
147
|
+
# Up to 50 at a time. A bad item fails alone; the rest still run.
|
|
148
|
+
batch = galley.renders.batch(
|
|
149
|
+
[{"template": "statement@4", "data": c} for c in customers],
|
|
150
|
+
webhook_url="https://example.com/hooks/galley",
|
|
151
|
+
)
|
|
152
|
+
batch.succeeded # the Render objects
|
|
153
|
+
batch.failed # the error envelopes, each with its index
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`render.cached` is `True` when the deterministic cache answered: the same template version, data,
|
|
157
|
+
options and format were rendered before, and this call cost nothing.
|
|
158
|
+
|
|
159
|
+
## Downloading
|
|
160
|
+
|
|
161
|
+
Signed URLs are short-lived; the stored file is not. `download()` takes a render, a render id or a
|
|
162
|
+
URL, and quietly re-signs an expired one.
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
data = galley.download(render) # bytes
|
|
166
|
+
galley.download(render, to_file="out/invoice.pdf")
|
|
167
|
+
galley.download("rnd_7hq2m4x8k1bv", to_file="a.png")
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Templates
|
|
171
|
+
|
|
172
|
+
Templates are code: one self-contained HTML document with inline CSS and Liquid expressions, plus a
|
|
173
|
+
JSON Schema that is the contract for `data`. Versions are immutable and content-addressed.
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
galley.templates.list()
|
|
177
|
+
invoice = galley.templates.get("invoice@3")
|
|
178
|
+
invoice.schema # JSON Schema for `data`
|
|
179
|
+
invoice.example # a payload that renders
|
|
180
|
+
|
|
181
|
+
galley.templates.create(
|
|
182
|
+
"welcome-card",
|
|
183
|
+
engine="satori", # fast PNG path for simple flexbox cards
|
|
184
|
+
source="<div style='display:flex'>{{ name }}</div>",
|
|
185
|
+
schema={"type": "object", "required": ["name"], "properties": {"name": {"type": "string"}}},
|
|
186
|
+
example={"name": "Dana"},
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
galley.templates.publish("welcome-card", source="…", message="tighter kerning")
|
|
190
|
+
galley.templates.versions("welcome-card")
|
|
191
|
+
|
|
192
|
+
# Free, renders nothing, and returns the same field errors a render would.
|
|
193
|
+
check = galley.templates.validate("welcome-card", {"name": 42})
|
|
194
|
+
if not check:
|
|
195
|
+
for field in check.errors:
|
|
196
|
+
print(field) # name: must be string (expected string, got number)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Usage
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
usage = galley.usage()
|
|
203
|
+
usage.billable_units # 1 per PNG/JPG, 1 per PDF page; cache hits are free
|
|
204
|
+
usage.free_renders_remaining
|
|
205
|
+
usage.spend_remaining_usd
|
|
206
|
+
usage.trial # not None only on a keyless trial
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Errors
|
|
210
|
+
|
|
211
|
+
Every failure is a `GalleyError` carrying the API's own envelope: a stable `type`, a `docs_url` and,
|
|
212
|
+
for validation, the field path, the expected type, what arrived and a value that would be accepted.
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
from galley_render import GalleyError, GalleyConnectionError, GalleyTimeoutError
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
galley.render("invoice", data={})
|
|
219
|
+
except GalleyError as err:
|
|
220
|
+
err.type # "validation_error"
|
|
221
|
+
err.status # 422
|
|
222
|
+
err.retryable # False
|
|
223
|
+
err.request_id # quote this in a support mail
|
|
224
|
+
for field in err.errors:
|
|
225
|
+
print(field.path, field.message, field.expected, field.received, field.example)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
| Type | Status | What to do |
|
|
229
|
+
|---|---|---|
|
|
230
|
+
| `validation_error` | 422 | Fix the named fields. `err.errors` says exactly which. |
|
|
231
|
+
| `invalid_request` | 400 | The request shape is wrong, not the data. |
|
|
232
|
+
| `authentication_error` | 401 | Missing or bad key. |
|
|
233
|
+
| `not_found` | 404 | No such template, version or render on this account. |
|
|
234
|
+
| `quota_exceeded` | 402 | Trial or free tier spent. |
|
|
235
|
+
| `spend_cap_exceeded` | 402 | The account's monthly cap. Raise it in the dashboard. |
|
|
236
|
+
| `rate_limited` | 429 | Retried for you. |
|
|
237
|
+
| `asset_blocked` | 400 | An image or font URL failed the SSRF policy; use a public https URL. |
|
|
238
|
+
| `render_failed` | 500 | The template threw. `err.body` has the detail. |
|
|
239
|
+
|
|
240
|
+
`GalleyConnectionError` means no HTTP response at all — DNS, TLS, timeout. `GalleyTimeoutError`
|
|
241
|
+
means `wait()` gave up while the render was still queued; the render is not lost, so poll again or
|
|
242
|
+
take the webhook.
|
|
243
|
+
|
|
244
|
+
## Also
|
|
245
|
+
|
|
246
|
+
- [Node SDK](https://www.npmjs.com/package/galley-render) — same surface, zero dependencies.
|
|
247
|
+
- [MCP server](https://mcp.galleyrender.com/mcp) — the same operations as agent tools, no key needed
|
|
248
|
+
to start.
|
|
249
|
+
- [Docs](https://galleyrender.com/docs) · [API reference](https://galleyrender.com/docs/api) ·
|
|
250
|
+
[Template language](https://galleyrender.com/docs/templates)
|
|
251
|
+
|
|
252
|
+
MIT licensed. Support: [support@galleyrender.com](mailto:support@galleyrender.com).
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# galley-render
|
|
2
|
+
|
|
3
|
+
**JSON in, PDF out.** The official Python client for [Galley Render](https://galleyrender.com) — a
|
|
4
|
+
document API for agents and the programs they write. A template plus a JSON payload becomes a PDF,
|
|
5
|
+
PNG or JPG behind a signed URL, deterministically and cached, so the same input always returns the
|
|
6
|
+
same file and an identical repeat call is free.
|
|
7
|
+
|
|
8
|
+
- Sync and async clients with the same surface.
|
|
9
|
+
- One dependency: `httpx`.
|
|
10
|
+
- Typed responses from the API's own [OpenAPI spec](https://api.galleyrender.com/openapi.json),
|
|
11
|
+
with `.raw` kept intact and `py.typed` shipped.
|
|
12
|
+
- Retries 429 and 5xx with exponential backoff and full jitter.
|
|
13
|
+
- Downloads signed URLs, and re-signs them when they expire.
|
|
14
|
+
- Starts a **50-render keyless trial** with no signup and no card.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install galley-render
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Python 3.9 or newer.
|
|
21
|
+
|
|
22
|
+
## Quickstart
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import os
|
|
26
|
+
from galley_render import Galley
|
|
27
|
+
|
|
28
|
+
galley = Galley(api_key=os.environ["GALLEY_API_KEY"])
|
|
29
|
+
|
|
30
|
+
render = galley.render(
|
|
31
|
+
"invoice@1",
|
|
32
|
+
format="pdf",
|
|
33
|
+
data={
|
|
34
|
+
"invoice_number": "INV-1042",
|
|
35
|
+
"customer": {"name": "Acme Corp"},
|
|
36
|
+
"line_items": [{"description": "Consulting", "quantity": 12, "unit_price": 150}],
|
|
37
|
+
},
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
print(render.url) # signed, short-lived
|
|
41
|
+
galley.download(render, to_file="invoice.pdf")
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Async is the same thing with `await`:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from galley_render import AsyncGalley
|
|
48
|
+
|
|
49
|
+
async with AsyncGalley() as galley: # reads GALLEY_API_KEY
|
|
50
|
+
render = await galley.render("invoice@1", format="pdf", data=payload)
|
|
51
|
+
pdf = await galley.download(render)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### No key yet
|
|
55
|
+
|
|
56
|
+
`start_trial()` mints a real 50-render account through Galley's MCP server and hands back a key that
|
|
57
|
+
works everywhere in this package and against the REST API.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from galley_render import Galley, start_trial
|
|
61
|
+
|
|
62
|
+
trial = start_trial(client_id="my-app") # reuse client_id to keep the same trial
|
|
63
|
+
print(trial.renders_remaining) # 50
|
|
64
|
+
|
|
65
|
+
galley = Galley(api_key=trial.api_key)
|
|
66
|
+
render = galley.render("og-card", format="png", data={"title": "Hello"})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`async_start_trial()` is the awaitable form. To lift the limit, call the `create_account` tool on
|
|
70
|
+
[the MCP server](https://mcp.galleyrender.com/mcp) with an email, or sign up at
|
|
71
|
+
[galleyrender.com](https://galleyrender.com). The trial upgrades in place — nothing it made is lost.
|
|
72
|
+
|
|
73
|
+
## Configuration
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
Galley(
|
|
77
|
+
api_key=None, # default: $GALLEY_API_KEY
|
|
78
|
+
base_url=None, # default: $GALLEY_BASE_URL, then the public API
|
|
79
|
+
timeout=60.0, # seconds, per attempt
|
|
80
|
+
max_retries=3, # extra attempts on 429/5xx and connection failures
|
|
81
|
+
headers=None, # merged into every request
|
|
82
|
+
http_client=None, # bring your own httpx.Client
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Both clients are context managers, and close the `httpx` client they created:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
with Galley() as galley:
|
|
90
|
+
...
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Rendering
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
# Small jobs finish inside the call.
|
|
97
|
+
render = galley.render(
|
|
98
|
+
"certificate@2", # pin the version in anything you ship
|
|
99
|
+
format="pdf", # pdf | png | jpg
|
|
100
|
+
data={"recipient": "Dana Lee", "course": "Rope Access L1"},
|
|
101
|
+
options={"page_size": "Letter", "margin": "18mm", "landscape": True},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# A webhook, async_=True or a large payload queues the job instead.
|
|
105
|
+
queued = galley.render("report", data=data, async_=True)
|
|
106
|
+
done = galley.renders.wait(queued.id) # polls with backoff
|
|
107
|
+
|
|
108
|
+
# Or do both in one call, whichever path the API takes.
|
|
109
|
+
finished = galley.render_and_wait("report", data=data)
|
|
110
|
+
|
|
111
|
+
# Up to 50 at a time. A bad item fails alone; the rest still run.
|
|
112
|
+
batch = galley.renders.batch(
|
|
113
|
+
[{"template": "statement@4", "data": c} for c in customers],
|
|
114
|
+
webhook_url="https://example.com/hooks/galley",
|
|
115
|
+
)
|
|
116
|
+
batch.succeeded # the Render objects
|
|
117
|
+
batch.failed # the error envelopes, each with its index
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`render.cached` is `True` when the deterministic cache answered: the same template version, data,
|
|
121
|
+
options and format were rendered before, and this call cost nothing.
|
|
122
|
+
|
|
123
|
+
## Downloading
|
|
124
|
+
|
|
125
|
+
Signed URLs are short-lived; the stored file is not. `download()` takes a render, a render id or a
|
|
126
|
+
URL, and quietly re-signs an expired one.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
data = galley.download(render) # bytes
|
|
130
|
+
galley.download(render, to_file="out/invoice.pdf")
|
|
131
|
+
galley.download("rnd_7hq2m4x8k1bv", to_file="a.png")
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Templates
|
|
135
|
+
|
|
136
|
+
Templates are code: one self-contained HTML document with inline CSS and Liquid expressions, plus a
|
|
137
|
+
JSON Schema that is the contract for `data`. Versions are immutable and content-addressed.
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
galley.templates.list()
|
|
141
|
+
invoice = galley.templates.get("invoice@3")
|
|
142
|
+
invoice.schema # JSON Schema for `data`
|
|
143
|
+
invoice.example # a payload that renders
|
|
144
|
+
|
|
145
|
+
galley.templates.create(
|
|
146
|
+
"welcome-card",
|
|
147
|
+
engine="satori", # fast PNG path for simple flexbox cards
|
|
148
|
+
source="<div style='display:flex'>{{ name }}</div>",
|
|
149
|
+
schema={"type": "object", "required": ["name"], "properties": {"name": {"type": "string"}}},
|
|
150
|
+
example={"name": "Dana"},
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
galley.templates.publish("welcome-card", source="…", message="tighter kerning")
|
|
154
|
+
galley.templates.versions("welcome-card")
|
|
155
|
+
|
|
156
|
+
# Free, renders nothing, and returns the same field errors a render would.
|
|
157
|
+
check = galley.templates.validate("welcome-card", {"name": 42})
|
|
158
|
+
if not check:
|
|
159
|
+
for field in check.errors:
|
|
160
|
+
print(field) # name: must be string (expected string, got number)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Usage
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
usage = galley.usage()
|
|
167
|
+
usage.billable_units # 1 per PNG/JPG, 1 per PDF page; cache hits are free
|
|
168
|
+
usage.free_renders_remaining
|
|
169
|
+
usage.spend_remaining_usd
|
|
170
|
+
usage.trial # not None only on a keyless trial
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Errors
|
|
174
|
+
|
|
175
|
+
Every failure is a `GalleyError` carrying the API's own envelope: a stable `type`, a `docs_url` and,
|
|
176
|
+
for validation, the field path, the expected type, what arrived and a value that would be accepted.
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from galley_render import GalleyError, GalleyConnectionError, GalleyTimeoutError
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
galley.render("invoice", data={})
|
|
183
|
+
except GalleyError as err:
|
|
184
|
+
err.type # "validation_error"
|
|
185
|
+
err.status # 422
|
|
186
|
+
err.retryable # False
|
|
187
|
+
err.request_id # quote this in a support mail
|
|
188
|
+
for field in err.errors:
|
|
189
|
+
print(field.path, field.message, field.expected, field.received, field.example)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
| Type | Status | What to do |
|
|
193
|
+
|---|---|---|
|
|
194
|
+
| `validation_error` | 422 | Fix the named fields. `err.errors` says exactly which. |
|
|
195
|
+
| `invalid_request` | 400 | The request shape is wrong, not the data. |
|
|
196
|
+
| `authentication_error` | 401 | Missing or bad key. |
|
|
197
|
+
| `not_found` | 404 | No such template, version or render on this account. |
|
|
198
|
+
| `quota_exceeded` | 402 | Trial or free tier spent. |
|
|
199
|
+
| `spend_cap_exceeded` | 402 | The account's monthly cap. Raise it in the dashboard. |
|
|
200
|
+
| `rate_limited` | 429 | Retried for you. |
|
|
201
|
+
| `asset_blocked` | 400 | An image or font URL failed the SSRF policy; use a public https URL. |
|
|
202
|
+
| `render_failed` | 500 | The template threw. `err.body` has the detail. |
|
|
203
|
+
|
|
204
|
+
`GalleyConnectionError` means no HTTP response at all — DNS, TLS, timeout. `GalleyTimeoutError`
|
|
205
|
+
means `wait()` gave up while the render was still queued; the render is not lost, so poll again or
|
|
206
|
+
take the webhook.
|
|
207
|
+
|
|
208
|
+
## Also
|
|
209
|
+
|
|
210
|
+
- [Node SDK](https://www.npmjs.com/package/galley-render) — same surface, zero dependencies.
|
|
211
|
+
- [MCP server](https://mcp.galleyrender.com/mcp) — the same operations as agent tools, no key needed
|
|
212
|
+
to start.
|
|
213
|
+
- [Docs](https://galleyrender.com/docs) · [API reference](https://galleyrender.com/docs/api) ·
|
|
214
|
+
[Template language](https://galleyrender.com/docs/templates)
|
|
215
|
+
|
|
216
|
+
MIT licensed. Support: [support@galleyrender.com](mailto:support@galleyrender.com).
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "galley-render"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Galley Render — JSON in, PDF out. Official Python client for the document API agents can sign themselves up for."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Galley Render", email = "hello@galleyrender.com" }]
|
|
14
|
+
maintainers = [{ name = "Galley Render", email = "support@galleyrender.com" }]
|
|
15
|
+
keywords = [
|
|
16
|
+
"pdf",
|
|
17
|
+
"pdf-generation",
|
|
18
|
+
"html-to-pdf",
|
|
19
|
+
"png",
|
|
20
|
+
"invoice",
|
|
21
|
+
"certificate",
|
|
22
|
+
"og-image",
|
|
23
|
+
"document-api",
|
|
24
|
+
"mcp",
|
|
25
|
+
"agents",
|
|
26
|
+
]
|
|
27
|
+
classifiers = [
|
|
28
|
+
"Development Status :: 4 - Beta",
|
|
29
|
+
"Intended Audience :: Developers",
|
|
30
|
+
"Programming Language :: Python :: 3",
|
|
31
|
+
"Programming Language :: Python :: 3.9",
|
|
32
|
+
"Programming Language :: Python :: 3.10",
|
|
33
|
+
"Programming Language :: Python :: 3.11",
|
|
34
|
+
"Programming Language :: Python :: 3.12",
|
|
35
|
+
"Programming Language :: Python :: 3.13",
|
|
36
|
+
"Programming Language :: Python :: 3.14",
|
|
37
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
38
|
+
"Topic :: Multimedia :: Graphics :: Graphics Conversion",
|
|
39
|
+
"Topic :: Printing",
|
|
40
|
+
"Typing :: Typed",
|
|
41
|
+
]
|
|
42
|
+
dependencies = ["httpx>=0.27"]
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Homepage = "https://galleyrender.com"
|
|
46
|
+
Documentation = "https://galleyrender.com/docs/sdks/python"
|
|
47
|
+
Source = "https://github.com/mattmueller/galley"
|
|
48
|
+
Changelog = "https://galleyrender.com/docs/sdks/python/changelog"
|
|
49
|
+
Issues = "https://galleyrender.com/support"
|
|
50
|
+
|
|
51
|
+
[project.optional-dependencies]
|
|
52
|
+
dev = ["pytest>=8", "pytest-asyncio>=0.24", "build>=1.2", "twine>=5"]
|
|
53
|
+
|
|
54
|
+
[tool.hatch.build.targets.wheel]
|
|
55
|
+
packages = ["src/galley_render"]
|
|
56
|
+
|
|
57
|
+
[tool.hatch.build.targets.sdist]
|
|
58
|
+
include = ["src/galley_render", "README.md", "LICENSE", "tests"]
|
|
59
|
+
|
|
60
|
+
[tool.pytest.ini_options]
|
|
61
|
+
testpaths = ["tests"]
|
|
62
|
+
asyncio_mode = "auto"
|
|
63
|
+
filterwarnings = ["error"]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""galley-render — JSON in, PDF out.
|
|
2
|
+
|
|
3
|
+
::
|
|
4
|
+
|
|
5
|
+
from galley_render import Galley
|
|
6
|
+
|
|
7
|
+
galley = Galley(api_key=os.environ["GALLEY_API_KEY"])
|
|
8
|
+
render = galley.render("invoice@1", format="pdf", data={"invoice_number": "INV-1042"})
|
|
9
|
+
galley.download(render, to_file="invoice.pdf")
|
|
10
|
+
|
|
11
|
+
With no key at all, :func:`start_trial` mints a 50-render account through the
|
|
12
|
+
MCP server and hands back a key that works everywhere in this package.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from ._client import DEFAULT_BASE_URL, AsyncGalley, Galley, __version__
|
|
16
|
+
from ._errors import (
|
|
17
|
+
FieldError,
|
|
18
|
+
GalleyConnectionError,
|
|
19
|
+
GalleyError,
|
|
20
|
+
GalleyTimeoutError,
|
|
21
|
+
)
|
|
22
|
+
from ._models import (
|
|
23
|
+
Account,
|
|
24
|
+
Batch,
|
|
25
|
+
DeletedTemplate,
|
|
26
|
+
Render,
|
|
27
|
+
Template,
|
|
28
|
+
TemplateVersion,
|
|
29
|
+
Usage,
|
|
30
|
+
Validation,
|
|
31
|
+
WebhookSecret,
|
|
32
|
+
)
|
|
33
|
+
from ._trial import DEFAULT_MCP_URL, Trial, async_start_trial, start_trial
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"Galley",
|
|
37
|
+
"AsyncGalley",
|
|
38
|
+
"start_trial",
|
|
39
|
+
"async_start_trial",
|
|
40
|
+
"Trial",
|
|
41
|
+
"GalleyError",
|
|
42
|
+
"GalleyConnectionError",
|
|
43
|
+
"GalleyTimeoutError",
|
|
44
|
+
"FieldError",
|
|
45
|
+
"Render",
|
|
46
|
+
"Batch",
|
|
47
|
+
"Template",
|
|
48
|
+
"TemplateVersion",
|
|
49
|
+
"Validation",
|
|
50
|
+
"Usage",
|
|
51
|
+
"Account",
|
|
52
|
+
"WebhookSecret",
|
|
53
|
+
"DeletedTemplate",
|
|
54
|
+
"DEFAULT_BASE_URL",
|
|
55
|
+
"DEFAULT_MCP_URL",
|
|
56
|
+
"__version__",
|
|
57
|
+
]
|