Skip to main content

ZeroBucket

PyPI version Python versions License: MIT CI

Your database. Your images. Zero buckets.

ZeroBucket is a database-native image storage library. It lets you store and retrieve images using the PostgreSQL database you already have, instead of standing up a separate object-storage service like S3.

from zerobucket import ZeroBucket

images = ZeroBucket(database_url="postgresql://...")

image_id = images.put("avatar.jpg")

image = images.get(image_id)
print(image.mime_type)   # "image/jpeg"
print(image.size_bytes)  # 1116478
print(image.data)        # raw bytes, ready to serve

Full documentation, architecture notes, and benchmark results live in the GitHub repository.

Installation

pip install zerobucket

Requires Python 3.10+ and PostgreSQL 13+ (uses gen_random_uuid(), built in since Postgres 13).

Quick reference

Method What it does
images.put(image, filename=None, optimize=False, max_width=None, format=None, quality=None) Validates, optionally optimizes, checksums, and stores an image. Accepts a file path, raw bytes, or a file-like object (including framework upload objects). Returns the new image's id.
images.get(image_id) Returns an Image (data, mime_type, filename, width, height, size_bytes, checksum_sha256). Raises ImageNotFoundError if missing.
images.metadata(image_id) Same fields as get() but without the raw bytes -- cheap existence/info check.
images.exists(image_id) Returns True/False.
images.delete(image_id) Deletes the image. Returns True if it existed.
images.close() Releases database connections. ZeroBucket also works as a context manager.
from zerobucket import ZeroBucket, ImageNotFoundError, ImageValidationError

images = ZeroBucket(
    database_url="postgresql://user:pass@localhost/mydb",
    max_bytes=8 * 1024 * 1024,  # default: 8MB
)

image_id = images.put("photo.jpg")
image_id = images.put(open("photo.jpg", "rb"))
image_id = images.put(request.files["avatar"])  # framework upload objects

image = images.get(image_id)
info = images.metadata(image_id)

try:
    images.get("nonexistent-id")
except ImageNotFoundError:
    ...

try:
    images.put("not-actually-an-image.txt")
except ImageValidationError:
    ...

Serving from a web API

from flask import Flask, Response

app = Flask(__name__)
images = ZeroBucket(database_url=DATABASE_URL)

@app.route("/images/<image_id>")
def serve_image(image_id):
    image = images.get(image_id)
    return Response(image.data, mimetype=image.mime_type)

Optimizing images (compression)

Off by default -- put() stores your exact input bytes unless you opt in:

image_id = images.put(
    "photo.jpg",
    optimize=True,
    max_width=1600,      # downscale if wider, aspect ratio preserved
    format="webp",       # optional re-encode target: "jpeg", "png", "webp"
    quality=90,           # 1-100, JPEG/WebP only; omit for data-backed defaults
)

Quality defaults (JPEG=90, WebP=88) are backed by measured SSIM data across multiple content types -- typical photos see 70-95% size reduction with no visible quality loss. One thing this data caught: don't target format="jpeg" for flat/graphic content (screenshots, logos) -- it can make them larger, not smaller. See COMPRESSION_RESULTS.md on GitHub for the full methodology.

What it validates

  • Format: JPEG, PNG, WebP built in, plus HEIC/HEIF (iPhone photos) via the optional pip install zerobucket[heic] extra -- detected from actual file content, never from filename extension or a client-supplied Content-Type header.
  • Corruption: truncated or malformed images are decoded and rejected before they reach the database.
  • Decompression bombs: a tiny compressed file that decodes to an enormous pixel grid is rejected, not silently allocated.
  • Size: configurable via max_bytes (default 8MB) -- see the benchmark results for why.

Transactions

By default, put()/get()/delete() each use their own independent database connection -- not your application's own transaction, even against the same database. Pass your own open psycopg connection via connection= to make a write participate in your transaction (e.g. "user

  • avatar, atomically, or neither"). See the Transactions section on GitHub for a worked example -- this was verified by direct experiment during development, not assumed.

Batch operations

results = images.put_many([open("a.jpg", "rb"), open("b.jpg", "rb")])
fetched = images.get_many([id1, id2])
deleted = images.delete_many([id1, id2])

Best-effort, not all-or-nothing -- check .success/.error per item. get_many/delete_many are genuine single-query batch operations; see the full docs on GitHub for what's actually batched vs. still per-item.

Retry behavior

Transient errors (connection drops, deadlocks, serialization failures) are automatically retried with exponential backoff (max_retries=3 by default). Important: passing your own connection= disables automatic retry for that call -- see the full explanation on GitHub for why that's a deliberate safety rule, not an oversight.

Custom content types (PDFs and beyond)

from zerobucket.validators.pdf import PDFValidator

doc_id = images.put(pdf_bytes, validator=PDFValidator())
doc = images.get(doc_id)  # no special handling needed -- ever

Everything (transactions, retry, batch ops) works identically regardless of which validator produced a row. See the full explanation on GitHub for why this is a pluggable hook rather than native PDF support built into the core.

CLI

zerobucket init      # create the schema if missing
zerobucket info      # image count, total size, breakdown by format
zerobucket verify    # re-checksum every image to detect corruption

Takes --database-url or reads ZEROBUCKET_DATABASE_URL from the environment. verify exits non-zero on any mismatch, so it's usable in cron/CI. See the full CLI docs on GitHub.

Deduplication

images = ZeroBucket(database_url=DATABASE_URL, dedup=True)
id1 = images.put("photo.jpg")
id2 = images.put("photo.jpg")  # identical content -- stored exactly once, referenced twice

Opt-in (dedup=True), uses separate tables from classic mode, so it's safe to add later without touching existing data. See the full explanation on GitHub, including the migration path for existing classic-mode data.

Limitations (read before using in production)

  • Not built for large files or high-volume media. Full images are read into memory on both ends of every request -- no streaming, no range requests, no CDN.
  • Deduplication is opt-in, not automatic. Default dedup=False stores every upload as a separate row; pass dedup=True for content-addressed, reference-counted storage (see above).
  • PostgreSQL only, for now. The storage layer is abstracted for future adapters, but only Postgres exists today.

See the full README and roadmap on GitHub for more detail.

License

MIT -- see LICENSE.

Release files for zerobucket 0.9.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 zerobucket 0.9.0
File Size Uploaded
zerobucket-0.9.0.tar.gz 49.3 kB Details

Built distribution (wheel)

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

Total release size: 85.3 kB

Release files / zerobucket-0.9.0.tar.gz

Download URL zerobucket-0.9.0.tar.gz
Size 49.3 kB
Tags Source
SHA-256 checksum
How to use checksums
e9ed0ac25812a4ac9a450f9003fdf9d21df9b68cc340ef5889a60ff814d16a5d
BLAKE2b-256 checksum
How to use checksums
01b1a3fdef34aafdb732b952a303e433f8eb4ffbcad94dd98916c3cf423eb32c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.9

Release files / zerobucket-0.9.0-py3-none-any.whl

Download URL zerobucket-0.9.0-py3-none-any.whl
Size 36.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
93e6b58705e171a0951af17c5937f7fa0ec46e0a3b8f523e77ae03c02ab5497a
BLAKE2b-256 checksum
How to use checksums
bfc8c6cbbed7f721bdfa4c96e1aae22e0e78dc047f344a60e31238903ced49d3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.9

Release history Release notifications | RSS feed

0.21.0

2 release files

0.20.0

2 release files

0.18.0

2 release files

0.17.0

2 release files

0.16.0

2 release files

This release

0.9.0 This release

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

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