Skip to content

Commit b6d33e5

Browse files
committed
add templated batch create support
1 parent 0296d71 commit b6d33e5

7 files changed

Lines changed: 135 additions & 2 deletions

File tree

‎README.md‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ result = client.create_image_batch(
101101
)
102102
```
103103

104-
Only HTML/CSS and URL requests can be batched. Empty `html` or `url` values in variations are omitted so they can inherit from `default_options`. An empty variation list returns a successful empty result without sending an HTTP request. Options unsupported by the batch API, such as `dedupe_duration_s`, are not serialized. See the [batch API documentation](https://docs.htmlcsstoimage.com/getting-started/using-the-api/#batch-image-creation).
104+
`create_image_batch` accepts HTML/CSS and URL requests; use `create_templated_image_batch` for templates. Empty `html` or `url` values in variations are omitted so they can inherit from `default_options`. An empty variation list returns a successful empty result without sending an HTTP request. Options unsupported by the batch API, such as `dedupe_duration_s`, are not serialized. See the [batch API documentation](https://docs.htmlcsstoimage.com/getting-started/using-the-api/#batch-image-creation).
105105

106106
## Signed URLs
107107

@@ -243,6 +243,7 @@ else:
243243
| `from_env(...)` | `HtmlCssToImageClient` | Reads credentials from the environment. |
244244
| `create_image(request)` | `CreateImageResponse` | Sends `POST /v1/image`. |
245245
| `create_image_batch(variations, default_options=None)` | `CreateImageBatchResponse` | Sends `POST /v1/image/batch`, unless the list is empty. |
246+
| `create_templated_image_batch(variations, default_options=None)` | `CreateImageBatchResponse` | Sends `POST /v1/image/batch/templated`, unless the list is empty. |
246247
| `delete_image(image_id)` | `DeleteImageResponse` | Sends `DELETE /v1/image/{id}`. |
247248
| `delete_image_batch(image_ids)` | `DeleteImageResponse` | Sends `DELETE /v1/image/batch`. |
248249
| `image_url(image_id, render_options=None)` | `str` | Builds an existing-image URL locally. |
@@ -268,3 +269,23 @@ python -m build
268269
## License
269270

270271
MIT
272+
273+
## Templated image batches
274+
275+
Create images from one or more templates with shared defaults and ordered variations:
276+
277+
```python
278+
from html_css_to_image import TemplatedBatchImageOptions
279+
280+
result = client.create_templated_image_batch(
281+
[
282+
TemplatedBatchImageOptions(template_values={"title": "First"}),
283+
TemplatedBatchImageOptions(template_id="t-other", template_values={"title": "Second"}),
284+
],
285+
TemplatedBatchImageOptions(template_id="t-card", template_version=3, format="webp"),
286+
)
287+
```
288+
289+
Omitted fields inherit defaults. Supplying a template ID resets the inherited version; omit its version to use latest. Template value objects merge recursively on the API; arrays, scalars, and explicit null values replace defaults. Results preserve input order and identical images reuse existing assets. Each merged values object must be nonempty and satisfy its template's required variables.
290+
291+
See the [API reference](https://docs.htmlcsstoimage.com/getting-started/using-the-api/#batch-templated-image-creation) for plan limits and examples.

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "html-css-to-image"
7-
version = "0.3.0"
7+
version = "0.4.0"
88
description = "Official Python client for the HTML/CSS to Image API"
99
readme = "README.md"
1010
requires-python = ">=3.10"

‎src/html_css_to_image/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
RequestOverride,
3838
RequestOverrideAction,
3939
RequestOverrideResourceType,
40+
TemplatedBatchImageOptions,
4041
ValidationError,
4142
)
4243

@@ -52,6 +53,7 @@
5253
"CreateImageResponse",
5354
"CreateImageSuccessResponse",
5455
"CreateTemplatedImageRequest",
56+
"TemplatedBatchImageOptions",
5557
"CreateUrlImageRequest",
5658
"DeleteImageResponse",
5759
"DeleteImageSuccessResponse",

‎src/html_css_to_image/_request_mapper.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
PDFValueInput,
1717
PDFValueWithUnits,
1818
RequestOverride,
19+
TemplatedBatchImageOptions,
1920
)
2021

2122

@@ -58,6 +59,22 @@ def map_batch_request(
5859
raise TypeError("Batch requests must contain HTML/CSS or URL requests")
5960
return cls.map_request(request, in_batch=True)
6061

62+
@classmethod
63+
def map_templated_batch_options(
64+
cls, request: TemplatedBatchImageOptions,
65+
) -> dict[str, Any]:
66+
if not isinstance(request, TemplatedBatchImageOptions):
67+
raise TypeError("Template batches require TemplatedBatchImageOptions")
68+
return cls.without_none({
69+
"template_id": request.template_id,
70+
"template_version": request.template_version,
71+
"template_values": (
72+
dict(request.template_values)
73+
if request.template_values is not None else None
74+
),
75+
"format": request.format,
76+
})
77+
6178
@classmethod
6279
def common_payload(
6380
cls,

‎src/html_css_to_image/client.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
CreateUrlImageRequest,
2424
DeleteImageResponse,
2525
RenderImageOptions,
26+
TemplatedBatchImageOptions,
2627
)
2728

2829
_ClientT = TypeVar("_ClientT", bound="HtmlCssToImageClient")
@@ -165,6 +166,31 @@ def create_image_batch(
165166
response = self._transport.post("/v1/image/batch", payload)
166167
return ResponseMapper.map_batch_response(response)
167168

169+
def create_templated_image_batch(
170+
self,
171+
variations: Sequence[TemplatedBatchImageOptions],
172+
default_options: TemplatedBatchImageOptions | None = None,
173+
) -> CreateImageBatchResponse:
174+
"""Create a template batch with shared defaults and ordered results.
175+
176+
Merging and template resolution happen on the API. An empty variation
177+
list succeeds locally. API errors use the existing batch response type.
178+
"""
179+
if not variations:
180+
return CreateImageBatchSuccessResponse(images=())
181+
payload: dict[str, Any] = {
182+
"variations": [
183+
RequestMapper.map_templated_batch_options(item)
184+
for item in variations
185+
],
186+
}
187+
if default_options is not None:
188+
payload["default_options"] = RequestMapper.map_templated_batch_options(
189+
default_options
190+
)
191+
response = self._transport.post("/v1/image/batch/templated", payload)
192+
return ResponseMapper.map_batch_response(response)
193+
168194
def delete_image(self, image_id: str) -> DeleteImageResponse:
169195
"""Delete one generated image.
170196

‎src/html_css_to_image/models.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,21 @@ class CreateTemplatedImageRequest:
269269
format: ImageFormat | None = None
270270

271271

272+
@dataclass(slots=True, kw_only=True)
273+
class TemplatedBatchImageOptions:
274+
"""Shared defaults or one template batch variation.
275+
276+
Supplying template_id resets the inherited version. Objects in
277+
template_values merge recursively; arrays, scalars and explicit None
278+
values replace defaults. Omitted fields inherit defaults.
279+
"""
280+
281+
template_id: str | None = None
282+
template_version: int | None = None
283+
template_values: Mapping[str, Any] | None = None
284+
format: ImageFormat | None = None
285+
286+
272287
CreateImageRequest: TypeAlias = (
273288
CreateHtmlCssImageRequest | CreateUrlImageRequest | CreateTemplatedImageRequest
274289
)

‎tests/test_client.py‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
ApiErrorResponse,
1515
CreateHtmlCssImageRequest,
1616
CreateTemplatedImageRequest,
17+
TemplatedBatchImageOptions,
1718
CreateUrlImageRequest,
1819
DeleteImageSuccessResponse,
1920
HtmlCssToImageClient,
@@ -48,6 +49,57 @@ def make_client(self, handler):
4849
http_client=http_client,
4950
)
5051

52+
def test_template_batch_preserves_inheritance_and_nested_null(self):
53+
defaults = TemplatedBatchImageOptions(
54+
template_id="t-card", template_version=3, format="webp",
55+
template_values={"brand": {"name": "Acme", "color": "red"}},
56+
)
57+
variation = TemplatedBatchImageOptions(
58+
template_id="t-other",
59+
template_values={"brand": {"color": None}, "tags": [], "active": False},
60+
)
61+
62+
def handler(request):
63+
self.assertEqual(str(request.url), "https://hcti.io/v1/image/batch/templated")
64+
self.assertEqual(request.method, "POST")
65+
self.assertEqual(json.loads(request.content), {
66+
"default_options": {
67+
"template_id": "t-card", "template_version": 3, "format": "webp",
68+
"template_values": {"brand": {"name": "Acme", "color": "red"}},
69+
},
70+
"variations": [{}, {
71+
"template_id": "t-other",
72+
"template_values": {"brand": {"color": None}, "tags": [], "active": False},
73+
}],
74+
})
75+
return httpx.Response(200, json={"images": [
76+
{"id": "two", "url": "two"}, {"id": "one", "url": "one"},
77+
]})
78+
79+
client = self.make_client(handler)
80+
result = client.create_templated_image_batch([TemplatedBatchImageOptions(), variation], defaults)
81+
self.assertTrue(result.success)
82+
self.assertEqual([image.id for image in result.images], ["two", "one"])
83+
self.assertEqual(variation.template_version, None)
84+
self.assertEqual(defaults.template_values["brand"]["color"], "red")
85+
86+
def test_template_batch_empty_and_error(self):
87+
calls = []
88+
89+
def handler(request):
90+
calls.append(request)
91+
self.assertNotIn("default_options", json.loads(request.content))
92+
return httpx.Response(400, json={"error": "Bad Request", "message": "Invalid template"})
93+
94+
client = self.make_client(handler)
95+
self.assertTrue(client.create_templated_image_batch([]).success)
96+
self.assertEqual(calls, [])
97+
result = client.create_templated_image_batch([TemplatedBatchImageOptions(template_id="t-missing")])
98+
self.assertFalse(result.success)
99+
self.assertEqual(result.message, "Invalid template")
100+
with self.assertRaises(TypeError):
101+
client.create_templated_image_batch([CreateUrlImageRequest(url="https://example.com")])
102+
51103
def test_request_overrides_serialize_enums_and_stay_out_of_signed_urls(self):
52104
rules = [RequestOverride(
53105
action=RequestOverrideAction.BLOCK,

0 commit comments

Comments
 (0)