Skip to main content

Per-tenant file storage, quota enforcement, and usage tracking for any Python application.

Project description

Tenantbox Python SDK

Per-tenant file storage, quota enforcement, and usage tracking for any Python application built on Tenantbox.

pip install tenantbox

What is Tenantbox?

Tenantbox gives your application per-tenant file storage with quota enforcement and usage tracking in two API calls. Files are stored in Tenantbox Bucket and served via presigned URLs, files never pass through your backend server, so uploads are fast and your server doesn't time out.


Quick Start

from tenantbox import TenantboxClient

client = TenantboxClient(api_key="tbx_your_key_here")

Or set the TENANTBOX_API_KEY environment variable and omit the argument:

client = TenantboxClient()

Core Concepts

The Two-Step Upload Pattern (Recommended)

For web applications, the correct pattern is:

  1. Your backend calls client.get_upload_url(...) and returns the presigned URL to the frontend
  2. Your frontend uploads directly to that URL - the file goes straight to Tenantbox storage, never through your server
Frontend ──── POST /your-api/upload-url ───► Django/Flask backend
                                                    │
                                                    ▼
                                            client.get_upload_url()
                                            (calls Tenantbox API)
                                                    │
                                            presigned_url + file_path
                                                    │
Frontend ◄──────────────────────────────────────────┘
    │
    └──── PUT file directly ────► Tenantbox storage
                                  (never touches your server)

This is why Tenantbox is faster, the file doesn't travel through your server twice.


Upload

get_upload_url() - For web apps (frontend uploads directly)

result = client.get_upload_url(
    tenant_id="user_123",         # Your user/customer ID/email/username
    filename="avatar.png",
    content_type="image/png",     # Optional — auto-detected from filename
    tenant_email="alice@acme.com" # Optional — for dashboard display
)

result.presigned_url  # Give this to your frontend to PUT the file
result.file_path      # Save this to your database — you'll need it later
result.is_new_tenant  # True if this tenant was auto-created
result.expires_in     # Seconds until the URL expires (default 3600)

upload_file() - For scripts and server-side uploads

When you have the file on disk or in memory and want the SDK to handle everything:

# From a file path
result = client.upload_file(
    tenant_id="user_123",
    file_path_or_obj="/tmp/monthly_report.pdf",
)

# From a file-like object (BytesIO, open file, etc.)
import io
buf = io.BytesIO(report_bytes)
buf.name = "report.pdf"
result = client.upload_file(tenant_id="user_123", file_path_or_obj=buf)

result.file_path   # Save this to your database
result.uploaded    # Always True (exception raised if upload fails)

The file goes directly to Tenantbox storage


Framework Examples

Django Templating Engine

The classic Django setup - a form-based upload using Django's templating engine. The view gets the presigned URL, passes it to the template, and the browser uploads directly to Tenantbox storage without touching your server.

views.py

import os
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from tenantbox import TenantboxClient

client = TenantboxClient(api_key=os.environ["TENANTBOX_API_KEY"])


@login_required
def upload_page(request):
    """Render the upload form with a fresh presigned URL."""
    result = client.get_upload_url(
        tenant_id=str(request.user.id),
        filename=request.GET.get("filename", "upload"),
        tenant_email=request.user.email,
    )
    return render(request, "upload.html", {
        "presigned_url": result.presigned_url,
        "file_path": result.file_path,
    })


@login_required
def save_file(request):
    """Called by the template after the browser finishes uploading."""
    if request.method == "POST":
        file_path = request.POST.get("file_path")
        filename = request.POST.get("filename")
        # Save file_path to your model
        request.user.profile.avatar_path = file_path
        request.user.profile.save()
        return redirect("dashboard")


@login_required
def download_file(request, file_path):
    """Redirect the user to a short-lived download URL."""
    from django.shortcuts import redirect
    dl = client.get_download_url(file_path=file_path, expires_in=300)
    return redirect(dl.download_url)

templates/upload.html

<!DOCTYPE html>
<html>
<head><title>Upload File</title></head>
<body>

<h2>Upload a File</h2>

<input type="file" id="fileInput" />
<button onclick="startUpload()">Upload</button>
<p id="status"></p>

<script>
  // These values are injected by Django's template engine
  const presignedUrl = "{{ presigned_url }}";
  const filePath = "{{ file_path }}";

  async function startUpload() {
    const file = document.getElementById("fileInput").files[0];
    if (!file) return;

    document.getElementById("status").textContent = "Uploading...";

    // Upload directly to Tenantbox storage — never touches your Django server
    const response = await fetch(presignedUrl, {
      method: "PUT",
      body: file,
      headers: { "Content-Type": file.type },
    });

    if (response.ok) {
      document.getElementById("status").textContent = "Upload complete!";

      // Tell your Django backend to save the file_path
      const form = document.createElement("form");
      form.method = "POST";
      form.action = "/save-file/";

      const csrfInput = document.createElement("input");
      csrfInput.type = "hidden";
      csrfInput.name = "csrfmiddlewaretoken";
      csrfInput.value = "{{ csrf_token }}";

      const pathInput = document.createElement("input");
      pathInput.type = "hidden";
      pathInput.name = "file_path";
      pathInput.value = filePath;

      const nameInput = document.createElement("input");
      nameInput.type = "hidden";
      nameInput.name = "filename";
      nameInput.value = file.name;

      form.appendChild(csrfInput);
      form.appendChild(pathInput);
      form.appendChild(nameInput);
      document.body.appendChild(form);
      form.submit();
    } else {
      document.getElementById("status").textContent = "Upload failed. Please try again.";
    }
  }
</script>

</body>
</html>

urls.py

from django.urls import path
from . import views

urlpatterns = [
    path("upload/", views.upload_page, name="upload"),
    path("save-file/", views.save_file, name="save_file"),
    path("download/<path:file_path>/", views.download_file, name="download_file"),
]

Django Ninja

from ninja import Router
from tenantbox import TenantboxClient
import os

router = Router()
client = TenantboxClient(api_key=os.environ["TENANTBOX_API_KEY"])


@router.post("/upload-url/")
def upload_url(request, filename: str):
    result = client.get_upload_url(
        tenant_id=str(request.user.id),
        filename=filename,
        tenant_email=request.user.email,
    )
    return {
        "presigned_url": result.presigned_url,
        "file_path": result.file_path,
    }


@router.get("/download/")
def download_url(request, file_path: str):
    dl = client.get_download_url(file_path=file_path, expires_in=300)
    return {"download_url": dl.download_url}


@router.delete("/files/")
def delete_file(request, file_path: str):
    result = client.delete_file(file_path=file_path)
    return {"detail": result.detail}

Django REST Framework

from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from tenantbox import TenantboxClient
import os

client = TenantboxClient(api_key=os.environ["TENANTBOX_API_KEY"])


@api_view(["POST"])
@permission_classes([IsAuthenticated])
def upload_url(request):
    result = client.get_upload_url(
        tenant_id=str(request.user.id),
        filename=request.data["filename"],
        tenant_email=request.user.email,
    )
    return Response({
        "presigned_url": result.presigned_url,
        "file_path": result.file_path,
    })


@api_view(["GET"])
@permission_classes([IsAuthenticated])
def download_url(request):
    dl = client.get_download_url(
        file_path=request.query_params["file_path"],
        expires_in=300,
    )
    return Response({"download_url": dl.download_url})


@api_view(["DELETE"])
@permission_classes([IsAuthenticated])
def delete_file(request):
    result = client.delete_file(file_path=request.data["file_path"])
    return Response({"detail": result.detail})

Nuxt 4

Server route - server/api/upload-url.post.ts

export default defineEventHandler(async (event) => {
  const body = await readBody(event)

  const response = await $fetch("/api/storage/upload/", {
    baseURL: process.env.TENANTBOX_BASE_URL,
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.TENANTBOX_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: {
      tenant_id: body.tenantId,
      filename: body.filename,
      content_type: body.contentType,
    },
  })

  return response  // { presigned_url, file_path }
})

Component - components/FileUpload.vue

<template>
  <div>
    <input type="file" @change="handleFileChange" />
    <button @click="upload" :disabled="!file || uploading">
      {{ uploading ? "Uploading..." : "Upload" }}
    </button>
    <p v-if="message">{{ message }}</p>
  </div>
</template>

<script setup lang="ts">
const file = ref<File | null>(null)
const uploading = ref(false)
const message = ref("")

function handleFileChange(event: Event) {
  const input = event.target as HTMLInputElement
  file.value = input.files?.[0] ?? null
}

async function upload() {
  if (!file.value) return
  uploading.value = true
  message.value = ""

  try {
    // Step 1: Get presigned URL from your Nuxt server route
    const { presigned_url, file_path } = await $fetch("/api/upload-url", {
      method: "POST",
      body: {
        tenantId: useAuth().user.id,   // your auth composable
        filename: file.value.name,
        contentType: file.value.type,
      },
    })

    // Step 2: Upload directly to Tenantbox storage
    await $fetch(presigned_url, {
      method: "PUT",
      body: file.value,
      headers: { "Content-Type": file.value.type },
    })

    // Step 3: Save file_path to your own database
    await $fetch("/api/documents", {
      method: "POST",
      body: { file_path, filename: file.value.name },
    })

    message.value = "Upload complete!"
  } catch (err) {
    message.value = "Upload failed. Please try again."
  } finally {
    uploading.value = false
  }
}
</script>

Next.js

API route - app/api/upload-url/route.ts

import { NextRequest, NextResponse } from "next/server"

export async function POST(req: NextRequest) {
  const { tenantId, filename, contentType } = await req.json()

  const response = await fetch(
    `${process.env.TENANTBOX_BASE_URL}/api/storage/upload/`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.TENANTBOX_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        tenant_id: tenantId,
        filename,
        content_type: contentType,
      }),
    }
  )

  if (!response.ok) {
    return NextResponse.json({ error: "Failed to get upload URL" }, { status: 500 })
  }

  const data = await response.json()
  return NextResponse.json(data)  // { presigned_url, file_path }
}

Component - components/FileUpload.tsx

"use client"

import { useState } from "react"

export default function FileUpload({ tenantId }: { tenantId: string }) {
  const [file, setFile] = useState<File | null>(null)
  const [uploading, setUploading] = useState(false)
  const [message, setMessage] = useState("")

  async function handleUpload() {
    if (!file) return
    setUploading(true)
    setMessage("")

    try {
      // Step 1: Get presigned URL from your Next.js API route
      const res = await fetch("/api/upload-url", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          tenantId,
          filename: file.name,
          contentType: file.type,
        }),
      })
      const { presigned_url, file_path } = await res.json()

      // Step 2: Upload directly to Tenantbox storage
      await fetch(presigned_url, {
        method: "PUT",
        body: file,
        headers: { "Content-Type": file.type },
      })

      // Step 3: Save file_path to your own database
      await fetch("/api/documents", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ file_path, filename: file.name }),
      })

      setMessage("Upload complete!")
    } catch (err) {
      setMessage("Upload failed. Please try again.")
    } finally {
      setUploading(false)
    }
  }

  return (
    <div>
      <input
        type="file"
        onChange={(e) => setFile(e.target.files?.[0] ?? null)}
      />
      <button onClick={handleUpload} disabled={!file || uploading}>
        {uploading ? "Uploading..." : "Upload"}
      </button>
      {message && <p>{message}</p>}
    </div>
  )
}

.env.local

TENANTBOX_API_KEY=your_key_here
TENANTBOX_BASE_URL=https://api.tenantbox.dev

Download

dl = client.get_download_url(
    file_path="projects/.../file.pdf",  # Saved at upload time
    expires_in=3600,                     # Optional, default 3600
)

dl.download_url   # Presigned URL — redirect your user here
dl.filename       # Original filename
dl.content_type   # MIME type
dl.size_bytes     # File size in bytes

Delete

result = client.delete_file(file_path="projects/.../file.pdf")
result.success  # True
result.detail   # "File deleted successfully."

Storage usage for the tenant is automatically decremented.


Usage & Quotas

Get tenant usage

usage = client.get_usage("user_123")

usage.storage_used_bytes   # Raw bytes
usage.storage_used_mb      # Megabytes (float)
usage.storage_limit_bytes  # None if unlimited
usage.storage_limit_mb     # None if unlimited
usage.usage_percentage     # 0–100, None if unlimited
usage.total_files          # Number of files
usage.is_unlimited         # True if no limit set
usage.email                # Email if provided at upload

Set a storage limit

from tenantbox.utils import MB, GB

client.set_limit("user_123", MB(500))   # 500 MB
client.set_limit("user_123", GB(10))    # 10 GB
client.set_limit("user_123", 52428800)  # Raw bytes also fine

Remove a storage limit (make unlimited)

client.remove_limit("user_123")

Error Handling

All SDK exceptions inherit from TenantboxError:

from tenantbox import (
    QuotaExceededError,
    TenantboxAuthError,
    TenantboxNotFoundError,
    TenantboxAPIError,
    TenantboxUploadError,
)

try:
    result = client.get_upload_url(tenant_id="user_123", filename="file.pdf")
except QuotaExceededError:
    return {"error": "You have exceeded your storage quota."}
except TenantboxAuthError:
    raise
except TenantboxNotFoundError:
    return {"error": "File not found."}
except TenantboxAPIError as e:
    logger.error("Tenantbox API error: %s (status %s)", e.message, e.status_code)
    return {"error": "Storage service unavailable."}
Exception When raised
TenantboxAuthError Invalid or inactive API key (401)
QuotaExceededError Tenant has exceeded their quota (403)
TenantboxNotFoundError File or tenant not found (404)
TenantboxUploadError Presigned URL obtained but Tenantbox storage upload failed
TenantboxAPIError Unexpected API error (5xx, malformed response)
TenantboxConfigError SDK misconfiguration (missing API key, bad URL)

Utility Helpers

from tenantbox.utils import KB, MB, GB, TB, human_readable_bytes

MB(100)     # → 104857600  (bytes)
GB(2)       # → 2147483648 (bytes)

human_readable_bytes(5_242_880)     # → "5.0 MB"
human_readable_bytes(1_073_741_824) # → "1.0 GB"

Environment Variables

Variable Description
TENANTBOX_API_KEY Your Tenantbox project API key
TENANTBOX_BASE_URL Override the API base URL (optional)

Requirements

  • Python 3.9+
  • requests >= 2.28.0

Running tests

To run automated tests, execute:

pytest tests/ -v

To run manual tests, execute:

python test_smoke.py


License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tenantbox-0.1.1.tar.gz (21.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tenantbox-0.1.1-py3-none-any.whl (16.1 kB view details)

Uploaded Python 3

File details

Details for the file tenantbox-0.1.1.tar.gz.

File metadata

  • Download URL: tenantbox-0.1.1.tar.gz
  • Upload date:
  • Size: 21.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.10

File hashes

Hashes for tenantbox-0.1.1.tar.gz
Algorithm Hash digest
SHA256 4328aeefb884608bf64c1da9c4743ef930239fd61e89f6a5f3eac117ac05aaba
MD5 15b092e6b90018dbef9587f8d87b228b
BLAKE2b-256 cfb9d3377caacdde282c80d00eee0aad71630ea21f500cd1300df5af8ee98abf

See more details on using hashes here.

File details

Details for the file tenantbox-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: tenantbox-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 16.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.10

File hashes

Hashes for tenantbox-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 b41f4941ed7e5130a2dac25d9e73520eaa7b7ddf566e5724a85326c72f14b510
MD5 76fa7bdca59e76726a09ddd91928df66
BLAKE2b-256 a4e5c0fad299791127a6cf3b2f705f3b19e16b828214e3dcb6f35064acca65fa

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page