Skip to main content

bucketbag

Experimental. A small, toolz-style library for batch-processing large collections of files in Hugging Face buckets — an experiment in what bucket-scale batch work wants as an interface:

  • Bounded scratch — download a batch, process, delete, repeat. Peak footprint ≈ (prefetch + 1) × max_bytes, never the corpus.
  • Resume — re-runs skip work already done (long HF Jobs can stop and restart).
  • Zero cleanup code — temp files removed as the loop advances, even on exceptions.
  • Throughput knobs — prefetch overlaps download with compute; boost() raises xet's small-file concurrency.

Mounting a bucket in a Job (hf jobs run -v hf://buckets/...) is the simplest way to use one; bucketbag explores the explicit API path for batch workloads where footprint, resume, and concurrency should be under your control. Not faster than the raw API (same transport), and not a framework.

Install

# /// script
# dependencies = ["bucketbag==0.4.0"]
# ///

Or pip install bucketbag. Pin a version: bucketbag is experimental and 0.x releases break. CHANGELOG.md says how to move from 0.3.

The read loop

from bucketbag import batched_files

for batch in batched_files("ns/my-bucket", include="**/*.jp2", max_bytes=4 * 2**30):
    for it in batch:                 # LoadedItem, already on local disk
        work(it.path)                # or it.bytes / it.image / it.text() / it.json()
    # ↑ this batch's files are deleted as the loop advances

Default scratch dir is /dev/shm (RAM tmpfs, not disk) — size (prefetch + 1) × max_bytes against memory, or pass dir=. One rule: don't keep a LoadedItem/.path past its batch.

Writing + resume

from bucketbag import iter_keys, batched_files, put_files

done = {k.removesuffix(".md") for k in iter_keys(OUT, include="**/*.md")}
keys = [k for k in iter_keys(SRC, include="**/*.jp2", objects=True) if k.path not in done]
for batch in batched_files(SRC, keys=keys, max_bytes=4 * 2**30):
    put_files([(it.key + ".md", ocr(it)) for it in batch], OUT)   # one API call per batch

iter_keys(..., objects=True) yields BucketFile objects (with .size) instead of key strings — pass those to batched_files(keys=…, max_bytes=…) so the byte bound can be honored. Bare string keys carry no size, so max_bytes + string keys raises ValueError (fail fast rather than silently run unbounded against the default RAM-tmpfs scratch — an OOMKill, not a disk-full). Let batched_files do the listing (omit keys=) and it keeps the sizes for you.

Parquet outputs are your tool's job (polars / pyarrow straight to the bucket); completed_keys(OUT) reads a done-set back from a __source_key column (pyarrow in your deps).

Bag — the loop as a lazy plan (experimental)

from bucketbag import Bag

bag = (Bag.from_bucket(SRC, include="**/*.jp2")
         .map_batches(ocr_batch, batch_size=32, setup=load_model)   # fn(items[, ctx]) -> [(key, content)]
         .to_bucket(OUT, done=".md"))                                  # input k is done once k + ".md" exists
bag.take(3)        # dev peek: outputs only, nothing written
stats = bag.compute()  # list -> skip done -> download -> fn -> put_files; kill + re-run safe
print(stats)  # for example:
# 476 processed (+24 skipped as done) in 15 batches -> 952 outputs, 3712.4s
# time: download wait 1%, map 98%, upload 1%; bound by map. The map function is the limit: ...

stats.bound_by ("download", "map" or "upload") and stats.hint tell a person or an agent what to tune next. compute() also logs its settings, a progress line after batches (at most every 30 s) and the same report at the end. They print to stderr as [bucketbag] … lines, so they show up in hf jobs logs, until your script configures logging; then your handlers get them.

Resume: done=".md" names the output that marks an input done, and the lookup is exact (pages/p10.md never marks pages/p1 done). compute() runs in-process today; fanning the same plan across HF Jobs (one shard per job) is the next layer.

To follow a bucket that another job is still filling, re-run the plan. Each pass lists both buckets again and skips what is done; setup() runs only when a pass has work:

import time

while producer_running():
    bag.compute()
    time.sleep(60)
bag.compute()  # once more: files may have landed during the last sleep

CONTRACT.md states what bucketbag promises; the tests check each rule.

API

batched_files(bucket, *, keys, include, exclude, n=20, max_bytes, dir, prefetch=2, ...) download batches → yield list[LoadedItem] → auto-delete
iter_keys(bucket, *, prefix, include, exclude, start_after, limit, token) list + glob-filter + sort keys (no download)
put_files(pairs, out_bucket) / put_bytes / put_text write raw objects; put_files batches many per call
completed_keys(out_bucket, *, prefix, column="__source_key") done-set from parquet outputs
boost(*, file_concurrency=32) raise xet download concurrency (~2.5× on small files)
LoadedItem .key .path + lazy .bytes .image .text() .json()
Bag.from_bucket(...).map_batches(fn).to_bucket(out, done=".md") + .take(n) / .compute() the loop as a lazy plan

bucket = "ns/bucket", "ns/bucket/prefix", "hf://buckets/ns/bucket/prefix", or a Hub web URL. prefix= joins below a prefix inside bucket, as a directory. Globs follow fsspec's rules (as in HfFileSystem.glob): * within a path segment, ** across /. prefix is a string prefix and the trailing slash matters: prefix="a/b/" lists the directory a/b only; prefix="a/b" also matches siblings like a/bc/y. The prefix is sent verbatim to the Hub. Bucketbag also checks each returned key against it as a cheap safeguard against over-matching listings, including pagination regressions.

Performance

Cold + disjoint, replicated (examples/bench.py, l4x1, ~0.8 MB jp2; ±10–20%):

default xet + boost()
bucketbag (prefetch 2–4) ~85–110 img/s ~200–270 img/s
raw download_bucket_files ~85–105 ~210–260
HfFileSystem (32 threads) ~90–110 ~110 (bypasses xet)
FUSE mount (same workload) ~22 —

Competitive with the raw API by design; the win is the interface. The one real throughput lever is xet's concurrent-file cap (default 8) — boost() raises it, ~2.5× on small files. Skip it for large files (over-subscription).

Decode, not transport

If a pipeline is slow, time the download and the decode separately before tuning transport (examples/decode_probe.py does this). The transport is usually not the gate: on ~0.7 MB page images, batched_files moves 40 files in ~1 s, and decoding those 40 takes ~70 s. Three things to check in the decode step:

  • Threads may not help. Some Pillow decoders hold the GIL (JPEG 2000 does): 8 threads gave the same speed as 1. A process pool did scale.
  • Decode smaller. Formats with resolution levels (JPEG 2000: im.reduce; JPEG: im.draft()) can decode at 1/4 or 1/8 the pixels for a fraction of the time — usually all a classifier needs.
  • Check the reduced size loads. Pillow's JPEG 2000 reduce>=2 fails with "broken data stream" when its size rounding disagrees with OpenJPEG's; safe_reduce() in the example picks the largest level that loads.

Not in scope

DAGs, shuffles, custom formats, return values to a driver. Stages couple through bucket keys; a global reduce is a one-off polars job. Jobs fan-out is planned as a thin layer on Bag.

Metadata

Release files for bucketbag 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for bucketbag 0.4.0
File Size Uploaded Provenance Actions
bucketbag-0.4.0.tar.gz 47.4 kB Without provenance Details for bucketbag-0.4.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for bucketbag 0.4.0
File Interpreter ABI Platform Actions
bucketbag-0.4.0-py3-none-any.whl Python 3 none any Details for bucketbag-0.4.0-py3-none-any.whl

Total release size: 71.3 kB

Release files / bucketbag-0.4.0.tar.gz

Download URL bucketbag-0.4.0.tar.gz
Size 47.4 kB
Tags Source
SHA-256 checksum
How to use checksums
ee307ff2a8db69e1476f72b98f91f5081489271fe9eb269453ae35b33cc0a98d
BLAKE2b-256 checksum
How to use checksums
c0508387ed874fdcd19ef607e3d506f25570adfafcc10be56a7a1e6048a42c53
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.7 {"installer":{"name":"uv","version":"0.13.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / bucketbag-0.4.0-py3-none-any.whl

Download URL bucketbag-0.4.0-py3-none-any.whl
Size 23.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f65930c0962065a146113928588eb8977403828be46aacd843056f2fe4527e7c
BLAKE2b-256 checksum
How to use checksums
37775e3bade6352d1f420db33f04a2986b8f20d512d77590fc1b2aaa03f65660
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.7 {"installer":{"name":"uv","version":"0.13.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for bucketbag 0.4.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page