| name | tigris-python-sdk |
| description | Use when working with Tigris from Python — boto3 setup, Django uploads via django-storages, snapshots, bucket forking, in-place object rename, conditional writes (IfMatch/IfNoneMatch), and the Bundle API for batch ML data fetches. Covers the tigris-boto3-ext extension library (context managers, decorators, helpers) plus framework integration. |
Tigris Python SDK
Use Tigris from Python via boto3 plus the tigris-boto3-ext extension package. Standard S3 calls work unchanged; the extension layers on Tigris-specific features — snapshots, bucket forking, in-place object rename, and the Bundle API for batch fetches — by injecting headers on activated boto3 events.
For Django uploads via django-storages, see ./resources/django.md.
Quick Start
npm install -g @tigrisdata/cli
tigris login
tigris buckets create my-bucket
tigris access-keys create "my-bucket-key"
tigris access-keys assign tid_xxx --bucket my-bucket --role Editor
pip install boto3 tigris-boto3-ext
AWS_ENDPOINT_URL_S3=https://t3.storage.dev
AWS_ACCESS_KEY_ID=tid_xxx
AWS_SECRET_ACCESS_KEY=tsec_yyy
AWS_REGION=auto
import boto3
from tigris_boto3_ext import (
TigrisSnapshotEnabled, create_snapshot, get_snapshot_version,
)
s3 = boto3.client("s3")
with TigrisSnapshotEnabled(s3):
s3.create_bucket(Bucket="my-bucket")
result = create_snapshot(s3, "my-bucket", snapshot_name="daily-backup")
version = get_snapshot_version(result)
See Getting Started with CLI for detailed setup steps.
Getting Started with CLI
Step 1: Install CLI
npm install -g @tigrisdata/cli
tigris --version
t3 is an alias for tigris — all commands work with either.
Step 2: Authenticate
tigris login
tigris whoami
For CI/CD:
tigris configure --access-key <key> --access-secret <secret>
Step 3: Create Bucket
tigris buckets create my-bucket
Buckets are private and global by default. Use --public for anonymous reads, --locations to pin regions.
Step 4: Create Access Key
tigris access-keys create "my-bucket-key"
Outputs an Access Key ID (tid_xxx) and Secret Access Key (tsec_yyy). The secret is shown only once.
Step 5: Assign Key to Bucket
tigris access-keys assign tid_xxx --bucket my-bucket --role Editor
| Role | Permissions | Use when |
|---|
Editor | Read + write + delete | App code that uploads/deletes objects |
ReadOnly | Read only | Apps that only download/list |
Step 6: Configure Environment
AWS_ENDPOINT_URL_S3=https://t3.storage.dev
AWS_ACCESS_KEY_ID=tid_xxx
AWS_SECRET_ACCESS_KEY=tsec_yyy
AWS_REGION=auto
boto3 reads AWS_ENDPOINT_URL_S3 natively (boto3 ≥ 1.34) — no client-side overrides needed if you set it.
Step 7: Install Python Dependencies
pip install boto3 tigris-boto3-ext
Requires Python 3.9+ and boto3 ≥ 1.26.0.
boto3 Client Setup
import boto3
s3 = boto3.client("s3")
s3 = boto3.client(
"s3",
endpoint_url="https://t3.storage.dev",
aws_access_key_id="tid_xxx",
aws_secret_access_key="tsec_yyy",
region_name="auto",
)
tigris-boto3-ext works with any boto3 S3 client — it registers handlers on before-sign.s3.* to inject Tigris headers when activated. Calls outside an activated scope pass through unchanged.
Usage Patterns
tigris-boto3-ext exposes three ways to scope Tigris-specific behavior. Pick whichever fits the call site:
| Pattern | Best for |
|---|
| Context managers | One-off blocks with a clear scope |
| Decorators | Functions that always need a Tigris feature |
| Helper functions | Direct calls without scoping ceremony |
Context Managers
from tigris_boto3_ext import (
TigrisSnapshotEnabled, TigrisSnapshot, TigrisFork, TigrisRename,
)
with TigrisSnapshotEnabled(s3):
s3.create_bucket(Bucket="my-bucket")
with TigrisSnapshot(s3, "my-bucket"):
snapshots = s3.list_buckets()
with TigrisSnapshot(s3, "my-bucket", snapshot_version="12345"):
obj = s3.get_object(Bucket="my-bucket", Key="file.txt")
with TigrisFork(s3, "source-bucket", snapshot_version="12345"):
s3.create_bucket(Bucket="forked-bucket")
with TigrisRename(s3):
s3.copy_object(
Bucket="my-bucket",
CopySource="my-bucket/old.txt",
Key="new.txt",
)
Decorators
from tigris_boto3_ext import snapshot_enabled, with_snapshot, forked_from, with_rename
@snapshot_enabled
def make_snapshot_bucket(s3, name):
return s3.create_bucket(Bucket=name)
@with_snapshot("my-bucket", snapshot_version="12345")
def read_old_config(s3):
return s3.get_object(Bucket="my-bucket", Key="config.json")
@forked_from("source-bucket", snapshot_version="12345")
def make_fork(s3, name):
return s3.create_bucket(Bucket=name)
@with_rename
def rename(s3, bucket, old, new):
return s3.copy_object(Bucket=bucket, CopySource=f"{bucket}/{old}", Key=new)
Helper Functions
from tigris_boto3_ext import (
create_snapshot_bucket,
create_snapshot,
list_snapshots,
create_fork,
get_object_from_snapshot,
get_snapshot_version,
list_objects_from_snapshot,
head_object_from_snapshot,
has_snapshot_enabled,
get_bucket_info,
rename_object,
)
create_snapshot_bucket(s3, "my-bucket")
result = create_snapshot(s3, "my-bucket", snapshot_name="backup-1")
version = get_snapshot_version(result)
snapshots = list_snapshots(s3, "my-bucket")
create_fork(s3, "fork-bucket", "my-bucket", snapshot_version=version)
obj = get_object_from_snapshot(s3, "my-bucket", "file.txt", version)
objects = list_objects_from_snapshot(s3, "my-bucket", version, Prefix="data/")
metadata = head_object_from_snapshot(s3, "my-bucket", "file.txt", version)
rename_object(s3, "my-bucket", "old.txt", "new.txt")
if has_snapshot_enabled(s3, "my-bucket"):
info = get_bucket_info(s3, "my-bucket")
Snapshots
Point-in-time bucket copies. Tigris stores only deltas, so they're cheap.
create_snapshot_bucket(s3, "production-data")
result = create_snapshot(s3, "production-data", snapshot_name="daily-2026-05-07")
version = get_snapshot_version(result)
with TigrisSnapshot(s3, "production-data", snapshot_version=version):
obj = s3.get_object(Bucket="production-data", Key="config.json")
snaps = list_snapshots(s3, "production-data")
for b in snaps.get("Buckets", []):
print(b["Name"])
Snapshots must be enabled at bucket creation — there is no retroactive enable.
Bucket Forking
Forks are independent buckets created from the live state or a snapshot of a source bucket. Storage is shared until divergence, so forks are essentially free.
with TigrisFork(s3, "source-bucket"):
s3.create_bucket(Bucket="forked-bucket")
create_fork(s3, "test-bucket", "production-data", snapshot_version=version)
info = get_bucket_info(s3, "test-bucket")
print(info["fork_source_bucket"], info["fork_source_snapshot"])
Common uses: test/staging environments off prod data, time-travel debugging from a pre-incident snapshot, per-experiment workspaces in ML pipelines.
In-Place Object Rename
Tigris implements rename as a copy_object plus the X-Tigris-Rename: true header — only the key changes, no data rewrite.
from tigris_boto3_ext import TigrisRename, rename_object
rename_object(s3, "my-bucket", "old-name.txt", "new-name.txt")
with TigrisRename(s3):
s3.copy_object(
Bucket="my-bucket",
CopySource="my-bucket/old.txt",
Key="new.txt",
)
Keep TigrisRename / with_rename scope tight. Any copy_object inside the block becomes a rename — unrelated copies will not behave as expected.
Conditional Writes
Tigris supports the standard S3 precondition headers — If-Match, If-None-Match, If-Modified-Since, If-Unmodified-Since — for optimistic concurrency and create-if-not-exists semantics. These are pure boto3 parameters; tigris-boto3-ext adds nothing on top because boto3 already passes them through.
from botocore.exceptions import ClientError
try:
s3.put_object(
Bucket="my-bucket",
Key="config.json",
Body=b'{"version": 1}',
IfNoneMatch="*",
)
except ClientError as e:
if e.response["Error"]["Code"] == "PreconditionFailed":
print("Object already exists")
else:
raise
head = s3.head_object(Bucket="my-bucket", Key="config.json")
etag = head["ETag"]
new_body = b'{"version": 2}'
try:
s3.put_object(
Bucket="my-bucket",
Key="config.json",
Body=new_body,
IfMatch=etag,
)
except ClientError as e:
if e.response["Error"]["Code"] == "PreconditionFailed":
...
else:
raise
| boto3 parameter | Behavior | Common use |
|---|
IfMatch=<etag> | Write only if current ETag matches | Optimistic concurrency / lost-update |
IfNoneMatch="*" | Write only if the object does not exist | Create-once / idempotent writes |
IfNoneMatch=<etag> | Write only if current ETag differs | Skip identical re-uploads |
IfModifiedSince | Proceed only if modified after the given RFC 1123 timestamp | Conditional reads / cache validation |
IfUnmodifiedSince | Proceed only if not modified after the given RFC 1123 timestamp | Time-bounded optimistic concurrency |
Failed preconditions surface as a ClientError with Error.Code == "PreconditionFailed" (HTTP 412). Catch that specific code — don't swallow all ClientErrors, since the same exception type covers NoSuchKey, throttling, etc.
IfMatch / IfNoneMatch work on put_object, copy_object, get_object, head_object, and delete_object.
Bundle API
Fetch many objects in a single streaming tar archive. Designed for ML training where per-object roundtrips dominate latency.
import tarfile
from tigris_boto3_ext import bundle_objects, BundleError, BUNDLE_ON_ERROR_FAIL
keys = [f"dataset/train/img_{i:05d}.jpg" for i in range(1000)]
response = bundle_objects(s3, "my-dataset-bucket", keys)
with tarfile.open(fileobj=response, mode="r|") as tar:
for member in tar:
if member.name == "__bundle_errors.json":
continue
f = tar.extractfile(member)
if f is not None:
data = f.read()
Error modes:
- Default: missing/failed objects are reported in
__bundle_errors.json inside the tar; partial success is OK.
on_error=BUNDLE_ON_ERROR_FAIL: any failure raises BundleError with status_code and body. Use for inference where every key must be present.
try:
response = bundle_objects(s3, "my-bucket", keys, on_error=BUNDLE_ON_ERROR_FAIL)
except BundleError as e:
print(f"Bundle failed (HTTP {e.status_code}): {e.body}")
Always open the tar in streaming mode (mode="r|"). Don't materialize the whole archive in memory.
Common Workflows
Backup and Restore
import boto3
from tigris_boto3_ext import (
create_snapshot_bucket, create_snapshot, create_fork, get_snapshot_version,
)
s3 = boto3.client("s3")
create_snapshot_bucket(s3, "production-data")
result = create_snapshot(s3, "production-data", snapshot_name="daily-backup")
version = get_snapshot_version(result)
create_fork(s3, "restored-data", "production-data", snapshot_version=version)
Testing Against Production Data
result = create_snapshot(s3, "production-data", snapshot_name="for-tests")
version = get_snapshot_version(result)
create_fork(s3, "test-data", "production-data", snapshot_version=version)
try:
s3.put_object(Bucket="test-data", Key="experiment.txt", Body=b"...")
finally:
s3.delete_bucket(Bucket="test-data")
Time-Travel Queries
from tigris_boto3_ext import get_object_from_snapshot, list_objects_from_snapshot
old = get_object_from_snapshot(s3, "my-bucket", "config.json", "12345")
config = old["Body"].read()
old_logs = list_objects_from_snapshot(
s3, "my-bucket", "12345", Prefix="logs/2025/12/",
)
for obj in old_logs.get("Contents", []):
print(obj["Key"])
How It Works
tigris-boto3-ext registers handlers on boto3's before-sign.s3.* event to inject Tigris headers when a context/decorator is active:
| Header | Purpose |
|---|
X-Tigris-Enable-Snapshot: true | Enable snapshots on bucket creation |
X-Tigris-Snapshot: true; name=<name> | Create a named snapshot |
X-Tigris-Snapshot: <bucket> | List snapshots for a bucket |
X-Tigris-Snapshot-Version: <version> | Read from a specific snapshot version |
X-Tigris-Fork-Source-Bucket: <bucket> | Fork from this bucket |
X-Tigris-Fork-Source-Bucket-Snapshot: <ver> | Fork from a specific snapshot |
X-Tigris-Rename: true | Turn CopyObject into in-place rename |
HeadBucket responses include X-Tigris-Enable-Snapshot, X-Tigris-Fork-Source-Bucket, and X-Tigris-Fork-Source-Bucket-Snapshot — surfaced via get_bucket_info() and has_snapshot_enabled().
Critical Rules
Always:
- Enable snapshots at bucket creation — there is no retroactive enable
- Capture
get_snapshot_version(result) immediately after create_snapshot — you need it to read or fork
- Stream Bundle API responses (
mode="r|") — never load the whole tar in memory
- Skip
__bundle_errors.json when iterating tar members
- Keep
TigrisRename / with_rename scope tight — every copy_object inside becomes a rename
Never:
- Hard-code
region_name to a real AWS region — use auto
- Use the production bucket directly for tests when a fork costs nothing
- Forget
AWS_ENDPOINT_URL_S3 — without it boto3 hits AWS S3, not Tigris
Known Issues
| Problem | Cause & Fix |
|---|
| Snapshot calls fail with 404/409 | Snapshots not enabled on the bucket. Recreate with create_snapshot_bucket. |
copy_object renamed unrelated file | TigrisRename block too broad. Wrap only the rename call. |
| Bundle hangs on iteration | Tar opened in non-streaming mode. Use mode="r|", not "r". |
BundleError raised unexpectedly | BUNDLE_ON_ERROR_FAIL was passed. Default mode reports errors in the tar. |
| Calls go to AWS, not Tigris | AWS_ENDPOINT_URL_S3 unset. Export it or pass endpoint_url= explicitly. |
| Fork has no snapshots enabled | Forks don't inherit snapshot capability — enable on the fork separately. |
CLI Quick Reference
t3 is an alias for tigris.
tigris login
tigris whoami
tigris buckets create <name> [--public] [--locations <region>]
tigris buckets list
tigris buckets delete <name>
tigris access-keys create "<name>"
tigris access-keys assign <tid_xxx> --bucket <name> --role Editor
tigris buckets snapshot <bucket> --name <snapshot-name>
tigris buckets snapshots <bucket>
tigris buckets fork <new> --source <bucket> [--snapshot <version>]
Related Skills
- file-storage — General Tigris setup and the JS/TS
@tigrisdata/storage SDK
- tigris-snapshots-forking — CLI-side snapshot/fork workflows
- tigris-agent-kit — Agent storage workflows (forks for sandboxes, checkpoints)
- tigris-sdk-guide — Picking SDKs across languages
Official Documentation