Skip to main content

py_storebridge

A unified, production-ready cloud storage SDK for Python.

Switch storage providers by changing a single config line — no code changes required.

Python port of storebridge (Node.js / TypeScript), with Pydantic validation and cross-provider transfer of files, folders, and entire buckets.


Features

  • 10 storage providers — one consistent API
  • Pydantic v2 — every config object and public operation is validated
  • Retry system — exponential backoff with full jitter
  • Middleware — composable upload pipeline (metadata injection, key prefixes, …)
  • Hooks — lifecycle events (before/after upload, delete, copy, move, transfer)
  • Multipart upload — high-level helper + low-level lifecycle API
  • Signed / presigned URLs — GET, PUT, DELETE
  • Streaming — upload from paths / file objects, download as streams or bytes
  • MIME auto-detection — from file extension
  • CDN / custom domain support
  • Lazy provider loading — only the SDK you use is imported
  • Cross-provider transfer — move existing files, folders, or a whole bucket from one provider to another

Provider support

Provider Extra Install
AWS S3 s3 pip install py_storebridge[s3]
Cloudflare R2 r2 pip install py_storebridge[r2]
Google Cloud Storage gcs pip install py_storebridge[gcs]
Azure Blob Storage azure pip install py_storebridge[azure]
DigitalOcean Spaces spaces pip install py_storebridge[spaces]
Backblaze B2 b2 pip install py_storebridge[b2]
MinIO minio pip install py_storebridge[minio]
Supabase Storage supabase pip install py_storebridge[supabase]
Firebase Storage firebase pip install py_storebridge[firebase]
Appwrite Storage appwrite pip install py_storebridge[appwrite]

Provider SDKs are optional extras. The core package depends only on Pydantic.

pip install py_storebridge
pip install py_storebridge[s3]
pip install py_storebridge[all]

Feature matrix

Feature S3 R2 GCS Azure Spaces B2 MinIO Supabase Firebase Appwrite
Upload yes yes yes yes yes yes yes yes yes yes
Delete yes yes yes yes yes yes yes yes yes yes
Exists yes yes yes yes yes yes yes yes yes yes
Metadata yes yes yes yes yes yes yes yes yes yes
List yes yes yes yes yes yes yes yes yes yes
Signed URLs yes yes yes yes yes yes yes yes yes limited
Copy / move yes yes yes yes yes yes yes yes yes yes
Stream upload yes yes yes yes yes yes yes yes yes yes
Multipart native native fallback native blocks native native native fallback fallback fallback
ACL / visibility yes limited yes limited yes limited limited yes yes yes
CDN / custom domain yes yes yes yes yes yes yes yes yes yes
Transfer to another provider yes yes yes yes yes yes yes yes yes yes

Quick start

from py_storebridge import create_storage

storage = create_storage({
    "provider": "s3",
    "credentials": {
        "access_key_id": "AKI…",
        "secret_access_key": "…",
        "region": "us-east-1",
        "bucket": "my-bucket",
    },
    "visibility": "private",
})

result = storage.upload(key="users/profile.png", file=image_bytes, content_type="image/png")
print(result.url)

url = storage.get_signed_url("users/profile.png", expires_in=3600)
exists = storage.exists("users/profile.png")
storage.delete(key="users/profile.png")

CamelCase credential keys from the Node.js SDK (accessKeyId, secretAccessKey, …) are accepted as well.


Configuration

All providers share the same top-level shape. Credentials differ per provider. Pydantic validates the payload before any SDK is loaded.

from py_storebridge import create_storage

storage = create_storage(
    provider="s3",
    credentials={
        "access_key_id": "…",
        "secret_access_key": "…",
        "region": "us-east-1",
        "bucket": "my-bucket",
    },
    visibility="private",          # default visibility
    retry={"max_retries": 3, "base_delay": 200, "max_delay": 10000},
    log_level="warn",              # debug | info | warn | error | silent
)

Retry delays are milliseconds, matching the Node.js package.


API

upload

result = storage.upload(
    key="images/photo.jpg",
    file=buffer,                    # bytes | str | pathlib.Path | BinaryIO
    content_type="image/jpeg",      # optional — detected from the key
    visibility="public",
    metadata={"author": "alice"},
    cache_control="max-age=31536000",
    on_progress=lambda uploaded, total: print(uploaded, total),
)
# result.key, result.url, result.etag, result.size, result.provider

storage.upload_file("local/photo.jpg", key="images/photo.jpg")

delete / exists / metadata / list

storage.delete(key="images/photo.jpg")
storage.exists("images/photo.jpg")
meta = storage.get_metadata("images/photo.jpg")

page = storage.list(prefix="images/", limit=50, cursor=None, delimiter="/")
for entry in storage.iter_files(prefix="images/"):
    print(entry.key, entry.size)

storage.delete_prefix("tmp/")

URLs

storage.get_url("public/logo.png")
storage.get_signed_url("private/report.pdf", expires_in=900)
storage.get_signed_url("uploads/raw.csv", method="PUT", expires_in=300, content_type="text/csv")

copy / move / rename

These operate inside one provider.

storage.copy(source_key="original.png", destination_key="copies/backup.png")
storage.move(source_key="temp/upload.png", destination_key="final/upload.png")
storage.rename("old-name.png", "new-name.png")

streams and buffers

stream = storage.get_stream("videos/intro.mp4")
data = storage.get_buffer("config/settings.json")
storage.download_file("config/settings.json", "settings.json")

multipart

result = storage.upload_multipart({"key": "large/dataset.parquet", "file": payload}, chunk_size=10 * 1024 * 1024)

session = storage.create_multipart_upload("huge.bin", "application/octet-stream")
parts = [
    storage.upload_part(upload_id=session.upload_id, key=session.key, part_number=i + 1, body=chunk)
    for i, chunk in enumerate(chunks)
]
storage.complete_multipart_upload(session, parts)
# storage.abort_multipart_upload(session)

Transfer between providers

This is the capability the Node.js storebridge package does not have. transfer() copies (or moves) existing objects from one provider to another: a single file, a folder prefix, an explicit key list, or an entire bucket.

from py_storebridge import create_storage, transfer

s3 = create_storage({"provider": "s3", "credentials": {…}})
gcs = create_storage({"provider": "gcs", "credentials": {…}})

# One file
transfer(s3, gcs, source_key="users/profile.png", destination_key="migrated/profile.png")

# A folder (every key under the prefix)
transfer(
    s3,
    gcs,
    prefix="images/",
    destination_prefix="backup/images/",
    concurrency=8,
    preserve_metadata=True,
    preserve_visibility=True,
)

# Entire bucket
result = s3.transfer_to(gcs)

# Move (copy then delete from the source)
transfer(s3, gcs, prefix="uploads/", delete_source=True)

# Dry run
plan = transfer(s3, gcs, prefix="images/", dry_run=True)
print(plan.planned, plan.items)

# Skip objects that already exist on the destination
transfer(s3, gcs, prefix="images/", skip_existing=True, continue_on_error=True)

TransferResult includes transferred, skipped, failed, bytes_transferred, and per-item status. Set continue_on_error=False to stop on the first failure (TransferError then carries the partial result).


Middleware

Middleware wraps every upload() call (onion model).

def inject_metadata(ctx, nxt):
    ctx.options.metadata = {
        **(ctx.options.metadata or {}),
        "uploaded_by": "user-123",
    }
    return nxt()

def force_prefix(ctx, nxt):
    if not ctx.options.key.startswith("uploads/"):
        ctx.options.key = f"uploads/{ctx.options.key}"
    return nxt()

storage.use(inject_metadata)
storage.use(force_prefix)

Hooks

storage.on("after:upload", lambda result: print(result.key, result.provider))
storage.on("error", lambda payload: log(payload["operation"], payload["error"]))
storage.on("after:transfer", lambda result: print(result.transferred, result.failed))

storage.off("after:upload", handler)

Events: before:upload, after:upload, before:delete, after:delete, before:copy, after:copy, before:move, after:move, before:list, after:list, before:getSignedUrl, after:getSignedUrl, before:transfer, after:transfer, error.


Errors

All errors extend StoreBridgeError (StorixError is an alias).

from py_storebridge import (
    StoreBridgeError,
    FileNotFoundError,
    PermissionError,
    MaxRetriesExceededError,
    ValidationError,
    MissingDependencyError,
    TransferError,
)

try:
    storage.get_metadata("missing.png")
except FileNotFoundError as err:
    print(err.key, err.status_code, err.provider)
except MaxRetriesExceededError as err:
    print(err.attempts)
except StoreBridgeError as err:
    print(err.code)

Install the matching extra if you see MissingDependencyError:

pip install py_storebridge[s3]

Provider examples

AWS S3

storage = create_storage({
    "provider": "s3",
    "credentials": {
        "access_key_id": "…",
        "secret_access_key": "…",
        "region": "us-east-1",
        "bucket": "my-bucket",
        "cdn_url": "https://cdn.example.com",
        "custom_domain": "https://files.example.com",
    },
    "visibility": "public",
})

Cloudflare R2

storage = create_storage({
    "provider": "r2",
    "credentials": {
        "account_id": "…",
        "access_key_id": "…",
        "secret_access_key": "…",
        "bucket": "my-bucket",
        "cdn_url": "https://pub-xxx.r2.dev",
    },
})

Google Cloud Storage

storage = create_storage({
    "provider": "gcs",
    "credentials": {
        "project_id": "my-gcp-project",
        "bucket": "my-bucket",
        "credentials": {
            "client_email": "…",
            "private_key": "-----BEGIN PRIVATE KEY-----\\n…\\n-----END PRIVATE KEY-----\\n",
        },
    },
})

Azure Blob Storage

storage = create_storage({
    "provider": "azure",
    "credentials": {
        "account_name": "…",
        "account_key": "…",
        "container_name": "uploads",
    },
})

DigitalOcean Spaces

storage = create_storage({
    "provider": "spaces",
    "credentials": {
        "access_key_id": "…",
        "secret_access_key": "…",
        "region": "nyc3",
        "bucket": "my-space",
        "cdn_url": "https://my-space.nyc3.cdn.digitaloceanspaces.com",
    },
})

Backblaze B2

storage = create_storage({
    "provider": "b2",
    "credentials": {
        "application_key_id": "…",
        "application_key": "…",
        "bucket": "my-bucket",
        "region": "us-west-004",
    },
})

MinIO

storage = create_storage({
    "provider": "minio",
    "credentials": {
        "end_point": "minio.example.com",
        "port": 9000,
        "use_ssl": True,
        "access_key": "…",
        "secret_key": "…",
        "bucket": "my-bucket",
    },
})

Supabase Storage

storage = create_storage({
    "provider": "supabase",
    "credentials": {
        "url": "https://xyz.supabase.co",
        "service_role_key": "…",
        "bucket": "avatars",
    },
    "visibility": "public",
})

Firebase Storage

storage = create_storage({
    "provider": "firebase",
    "credentials": {
        "project_id": "my-project",
        "bucket": "my-project.appspot.com",
        "service_account": {…},  # full service-account JSON
    },
})

Appwrite Storage

Appwrite file IDs cannot contain /. py_storebridge stores a stable SHA-256 of the key as the file ID and keeps the original key as the file name, so users/profile.png still works.

storage = create_storage({
    "provider": "appwrite",
    "credentials": {
        "endpoint": "https://cloud.appwrite.io/v1",
        "project_id": "…",
        "api_key": "…",
        "bucket_id": "…",
    },
})

Custom provider

from py_storebridge import BaseProvider, create_storage_from_provider

class MyProvider(BaseProvider):
    provider_name = "my-provider"

    def _upload(self, options):
        ...

storage = create_storage_from_provider(MyProvider())

Development

python -m venv .venv
.venv\Scripts\activate          # Windows
# source .venv/bin/activate     # macOS / Linux

pip install -e ".[dev]"
pytest

Publishing to PyPI is documented in docs/HOW_TO_DEPLOY.md.


License

MIT © Asif Khan

Metadata

Release files for py-storebridge 1.0.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 py-storebridge 1.0.0
File Size Uploaded
py_storebridge-1.0.0.tar.gz 44.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for py-storebridge 1.0.0
File Interpreter ABI Platform
py_storebridge-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 92.4 kB

Release files / py_storebridge-1.0.0.tar.gz

Download URL py_storebridge-1.0.0.tar.gz
Size 44.5 kB
Tags Source
SHA-256 checksum
How to use checksums
8d02695c3e275cd327ffdc6748b308cfa16ec88b3cffbc03800155a43d2d8a21
BLAKE2b-256 checksum
How to use checksums
557118f6049422950bc4e3283fb8bb987992be8d09d497647c24c0337bae3c44
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.10

Release files / py_storebridge-1.0.0-py3-none-any.whl

Download URL py_storebridge-1.0.0-py3-none-any.whl
Size 47.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cac1a49800663822eb2f67d2f80e007b7965d3bf601f496eacb56eba23fab8a8
BLAKE2b-256 checksum
How to use checksums
3e6e6fa0e1fc8177e86c8d73fb8977eb5ef71d276688faf973ad78d49efc53fe
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.10

Release history Release notifications | RSS feed

This release

1.0.0 This release

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