Batch
Bulk move, copy, delete, and update across many images in one round-trip.
Bulk image operations on a single dataset. Each call accepts a list of image IDs and returns a BatchResult with per-item failure context — partial success does not raise.
from pictograph import Client
client = Client()
move
Move images to a different virtual folder within the same dataset.
result = client.batch.move(
dataset_name="my-dataset",
image_ids=["img-1", "img-2", "img-3"],
target_folder_path="/sorted/cars",
)
print(result.succeeded, result.failed_count, result.failures)
Storage paths are immutable — “move” updates virtual_folder_path; the underlying image bytes don’t move.
copy
Copy images to a different folder. Server-side copy of the underlying bytes (instant, zero data transfer).
result = client.batch.copy(
dataset_name="my-dataset",
image_ids=["img-1", "img-2"],
target_folder_path="/cars-copy",
duplicate_handling="rename", # collision policy in the destination
copy_annotations=False, # destination images start without annotations
)
| Arg | Type | Default | Notes |
|---|---|---|---|
dataset_name | str | required | |
image_ids | Sequence[str] | required | |
target_folder_path | str | "/" | Destination virtual folder |
duplicate_handling | Literal["rename", "skip", "overwrite"] | "rename" | How to handle filename collisions |
copy_annotations | bool | False | When True, copy annotations_json and status too |
delete
Soft-archive by default; permanent on request.
result = client.batch.delete(
dataset_name="my-dataset",
image_ids=["img-1", "img-2", "img-3"],
permanent=False, # archive (recoverable)
)
permanent=True purges the stored bytes — irreversible. Requires admin+ role.
update
Update metadata fields on a batch of images. Pass exactly the fields you want to change — None is omitted from the request.
result = client.batch.update(
dataset_name="my-dataset",
image_ids=["img-1", "img-2"],
status="complete",
is_archived=False,
)
| Arg | Type | Default | Notes |
|---|---|---|---|
dataset_name | str | required | |
image_ids | Sequence[str] | required | |
status | str | None | None | "new", "annotate", "review", "complete" |
display_name | str | None | None | Display override |
is_archived | bool | None | None | True archives; False restores |
ValidationError if every field is None (the update would be a no-op).
BatchResult
| Attribute | Type | Notes |
|---|---|---|
succeeded | list[str] | IDs the op completed for |
failed_count | int | len(failures) |
failures | list[BatchFailure] | {image_id, reason} per failure |
success | bool (property) | failed_count == 0 |
Errors
| Status | Exception | Cause |
|---|---|---|
| 403 | ForbiddenError | permanent=True requires admin+ role |
| 404 | NotFoundError | Dataset missing, or every image_id invalid |
| 422 | ValidationError | Invalid field value or empty update |
Why batch over loops
Reorganizing 10K images is one round-trip with batch.move() versus 10K with images.update(). Bulk operations are implemented server-side as single statements, not loops.