Official Python SDK for Tilde: an agent-native data platform that combines versioned object storage with sandboxed compute
Project description
Tilde Python SDK
Python SDK for the Tilde data versioning API.
Installation
pip install tilde-sdk
Or with uv:
uv add tilde-sdk
Requires Python 3.11+.
Quick Start
import tilde
repo = tilde.repository("my-org/my-repo")
# Run commands in an interactive sandbox
with repo.shell(image="python:3.12") as sh:
sh.run("pip install pandas")
result = sh.run("python train.py")
print(result.stdout.text())
# Stream output line by line
result = sh.run("cat /sandbox/results.csv")
for line in result.stdout.iter_lines():
print(line)
[!IMPORTANT] Transactional by default. All filesystem modifications made in a sandbox happen in the context of a transactional session. If anything fails midway or is aborted, changes don't take effect. Only successful sandboxes' changes are committed atomically to the repository -- so your data is always in a consistent state. See Sessions for more details.
Authentication
The SDK resolves credentials in this order (first match wins):
- Explicit parameter —
Client(api_key=...)ortilde.configure(api_key=...). - Environment variable —
TILDE_API_KEY. - CLI config file —
~/.tilde/config.yaml(written bytilde auth login). - Sandbox metadata endpoint —
TILDE_SANDBOX_CREDENTIALS_URI(auto-detected only when no static key is configured; injected by the sandbox runtime for code running inside a Tilde sandbox).
A static key always wins over sandbox auto-detection, so credentials set deliberately by the caller are never silently overridden.
Using the CLI's saved credentials
After tilde auth login, the SDK picks up the credentials automatically — no
env var or in-code configuration needed:
import tilde
repo = tilde.repository("my-org/my-repo")
Environment variables
Right for CI/CD, agent workflows, and Docker containers where the CLI hasn't run:
export TILDE_API_KEY="your-api-key"
export TILDE_ENDPOINT_URL="https://tilde.run" # optional
Module-level configuration
import tilde
tilde.configure(api_key="your-api-key")
repo = tilde.repository("my-org/my-repo")
Explicit client
Use an explicit client when you need multiple configurations or full control over the HTTP lifecycle:
from tilde import Client
with Client(api_key="your-api-key") as client:
repo = client.repository("my-org/my-repo")
A missing API key is not an error at construction time; a ConfigurationError
is raised when the first request is made.
Configuration reference
| Option | Environment variable | Default |
|---|---|---|
api_key |
TILDE_API_KEY |
required |
endpoint_url |
TILDE_ENDPOINT_URL |
https://tilde.run |
default_sandbox_image |
TILDE_DEFAULT_SANDBOX_IMAGE |
ubuntu:22.04 |
Sandbox Execution
The fastest way to run code against your repository data. Sandboxes execute inside isolated containers with your data mounted as a volume — every change is captured as a transaction.
Interactive shell
Use repo.shell() to run multiple commands in a single sandbox:
with repo.shell(image="python:3.12") as sh:
sh.run("pip install pandas")
result = sh.run("python train.py")
print(result.stdout.text())
print(result.stderr.text())
The shell is a context manager — on clean exit changes commit automatically,
on exception they roll back. Each sh.run() spawns an independent child
process: shell state (cd, exported vars, aliases) does not persist across
calls. Chain commands in one call (sh.run("cd /foo && ls")) or pass env= /
cwd= to run().
shell.run() returns a RunResult with stdout, stderr, and exit_code.
Pass check=True to raise CommandError on non-zero exits.
One-shot execution
For a single command that doesn't need an interactive session:
result = repo.execute("python train.py", image="python:3.12")
print(result.stdout.text())
# check=False to handle errors yourself
result = repo.execute("might-fail", image="python:3.12", check=False)
if result.exit_code != 0:
print(result.stdout.text())
Output streams
RunResult.stdout and RunResult.stderr are OutputStream instances:
| Method | Returns | Description |
|---|---|---|
.read() |
bytes |
Full output as raw bytes |
.text(encoding='utf-8') |
str |
Full output decoded as a string |
.iter_bytes(chunk_size) |
Iterator[bytes] |
Yield byte chunks |
.iter_text(chunk_size) |
Iterator[str] |
Yield text chunks |
.iter_lines() |
Iterator[str] |
Yield lines (no trailing newlines) |
Repositories
import tilde
# Shorthand: the only top-level shortcut in the SDK
repo = tilde.repository("my-org/my-repo")
# Lazy-loaded properties
print(repo.id, repo.description, repo.visibility)
# Update
repo.update(description="New description", visibility="public")
# Delete (soft delete)
repo.delete()
Create a repository through its organization:
org = tilde.organizations.get("my-org")
repo = org.repositories.create(
"my-repo",
description="My dataset",
session_max_duration_days=7,
retention_days=90,
)
for r in org.repositories.list():
print(r.name, r.description)
Commits
# Newest first, auto-paginating
for commit in repo.commits.list():
print(commit.id, commit.committer, commit.message)
# Cap results
for commit in repo.commits.list(amount=10):
print(commit.id)
# Look up by ID
commit = repo.commits.get("a1b2c3d4e5f6")
# Diff introduced by a commit
for change in commit.diff():
print(change.status, change.path)
# Revert
revert = commit.revert(message="undo")
Pagination
Every .list() returns a PaginatedIterator that fetches pages lazily. Two
keyword arguments tune iteration:
| Argument | Effect |
|---|---|
amount |
Cap on the total number of results yielded. |
page_size |
Number of results per HTTP page (default 100, server max 1000). |
for entry in commit.objects.list(prefix="data/", page_size=500):
process(entry)
Sessions
Sessions provide direct transactional access to objects. Prefer sandbox execution for running code; sessions are for fine-grained object operations from your own process.
# Context manager — rolls back on error
with repo.session() as session:
session.objects.put("data/report.csv", b"content")
session.objects.delete("data/old.csv")
session.commit("update CSV files")
# Explicit control
session = repo.session()
print(session.session_id)
session.objects.put("data/file.csv", b"content")
session.commit("modifying data")
# or: session.rollback()
Resume a session from another thread, process, or machine:
session = repo.attach(session_id)
session.objects.put("data/file2.csv", b"more content")
session.commit("finishing work")
Objects
Writing
import pathlib
with repo.session() as session:
# From bytes
session.objects.put("data/hello.txt", b"Hello, Tilde!")
# From a file
with open("dataset.parquet", "rb") as f:
session.objects.put("data/dataset.parquet", f)
# From a Path (opened/closed automatically)
session.objects.put("data/model.bin", pathlib.Path("model.bin"))
# Server-side copy (no download/upload)
session.objects.copy("data/hello.txt", "data/hello-backup.txt")
# Delete
session.objects.delete("data/old.csv")
session.objects.delete_many(["data/a.csv", "data/b.csv"])
session.commit("upload files")
Files ≥ 64 MB automatically use multipart upload; smaller files use a single presigned PUT. No code changes needed.
Reading
Objects can be read from a committed snapshot (via a Commit) or from within a
session (including uncommitted staged changes):
# From a commit
commit = next(iter(repo.commits.list()))
with commit.objects.get("data/hello.txt") as f:
print(f.read().decode())
# Within a session
with repo.session() as session:
with session.objects.get("data/file.csv") as f:
data = f.read()
# Metadata only
meta = session.objects.head("data/file.csv")
print(meta.etag, meta.content_type, meta.content_length)
Streaming and byte ranges
Large objects: disable caching and stream in chunks:
with commit.objects.get("data/large.bin", cache=False) as f:
for chunk in f.iter_bytes(chunk_size=1024 * 1024):
output.write(chunk)
Byte ranges work for file headers, log tails, or columnar formats like Parquet:
# First 4 bytes
with commit.objects.get("data/file.parquet", byte_range=(0, 3)) as f:
magic = f.read()
# From offset 1024 to end
with commit.objects.get("data/file.parquet", byte_range=(1024, None)) as f:
tail = f.read()
print(f.content_range) # "bytes 1024-49151/49152"
print(f.content_length) # bytes returned
Listing
for entry in commit.objects.list(prefix="data/", delimiter="/"):
print(entry.path, entry.type) # "object" or "prefix"
Uncommitted changes
session = repo.session()
session.objects.put("data/new.csv", b"content")
for entry in session.uncommitted():
print(entry.path)
Organizations
import tilde
# Create, list, get, delete at the module level
org = tilde.organizations.create("my-org", display_name="My Org")
for o in tilde.organizations.list():
print(o.name, o.display_name)
org = tilde.organizations.get("my-org")
tilde.organizations.delete("my-org")
An Organization exposes every org-scoped sub-resource as a property:
org = tilde.organizations.get("my-org")
org.repositories
org.members
org.agents
org.roles
org.groups
org.policies
org.connectors
Members
for m in org.members.list():
print(m.username, m.email)
org.members.create("alice") # add by username
org.members.delete("user-uuid") # remove by user_id
Groups
groups = org.groups
group = groups.create("engineers", description="Engineering team")
group = groups.get(group.id) # includes members + attachments
group.members.create(subject_type="user", subject_id="user-uuid")
group.members.delete(subject_type="user", subject_id="user-uuid")
group.update(name="engineering", description="Updated")
group.delete()
# Effective groups for a principal
for g in groups.effective(principal_type="user", principal_id="user-uuid"):
print(g.group_name, g.source)
Policies
policies = org.policies
# Validate before creating
result = policies.validate("GetObject()")
print(result.valid, result.errors)
policy = policies.create(
name="read-only",
policy_text="ListRepositories()\nGetRepository()\nGetObject()\n",
description="Read-only access",
)
# Generate from natural language
text = policies.generate("Allow read-only access to all repositories")
# Attach / detach
policy.attach(principal_type="group", principal_id="group-uuid")
policy.detach(principal_type="group", principal_id="group-uuid")
# Effective policies for a principal
for ep in policies.effective(principal_type="user", principal_id="user-uuid"):
print(ep.policy_name, ep.source)
Roles and Agents
Non-human identities authenticated via API keys:
- Roles (
trk-prefix) — CI/CD pipelines, automation without metadata. - Agents (
tak-prefix) — automated tools and AI assistants, with metadata and optional inline policies.
# Roles
role = org.roles.create("ci-deployer", description="CI/CD pipeline")
for role in org.roles.list():
print(role.name)
# Agents
agent = org.agents.create(
"data-pipeline",
description="Nightly pipeline",
metadata={"env": "prod"},
)
agent.update(metadata={"env": "staging"})
agent.update(inline_policy="ListRepositories()\nGetObject()\n")
API keys
agent = org.agents.get("data-pipeline")
# Create a key (the full token is only shown once)
created = agent.api_keys.create("pipeline-key")
print(created.token) # tak-...
for key in agent.api_keys.list():
print(key.name, key.token_hint)
key = agent.api_keys.get(key_id)
key.revoke()
The same api_keys collection exists on Role instances (tokens prefixed
trk-).
Secrets
Encrypted key-value pairs injected as environment variables into sandboxes.
# Repository-scoped
repo.secrets.create("API_KEY", "sk-abc123...")
secret = repo.secrets.get("API_KEY")
print(secret.value)
repo.secrets.delete("API_KEY")
# Agent-scoped (override repo secrets with the same key)
agent = org.agents.get("data-pipeline")
agent.secrets.create("OPENAI_KEY", "sk-abc123...")
Precedence at sandbox launch (highest to lowest):
env=passed in the sandbox request- Agent secrets (if running as an agent)
- Repository secrets
Connectors and Imports
connectors = org.connectors
# S3
conn = connectors.create(
name="production-s3",
type="s3",
source_uri="s3://my-bucket/datasets/",
config={
"access_key_id": "AKIA...",
"secret_access_key": "...",
"region": "us-west-2",
},
)
# Attach to a repo
repo.connectors.attach(conn.id)
for c in repo.connectors.list():
print(c.name, c.type)
Import from a connector:
import time
job = repo.imports.create_from_connector(
connector_id=conn.id,
destination_path="imported/",
source_prefix="datasets/",
commit_message="Import production datasets",
)
while job.status not in ("completed", "failed"):
time.sleep(2)
job.refresh()
print(job.status, job.objects_imported)
if job.status == "completed":
print(f"Import done! Commit: {job.commit_id}")
Cross-repository imports copy data from one Tilde repo to another:
job = repo.imports.create_from_repository(
repo_path="other-org/source-data",
destination_path="external/",
source_prefix="datasets/train/",
commit_message="Import training data",
)
Agent Approval Workflow
When a repository requires approval for agent commits, session.commit()
blocks by default until a human approves or rolls back. The approval URL is
emitted as a UserWarning.
with repo.session() as session:
session.objects.put("data/results.csv", b"col1,col2\na,b\n")
session.commit("add results") # blocks until approved
Non-blocking:
result = session.commit("add results", block_for_approval=False)
# result is None; session stays open for review
Structured result (no blocking, no warnings):
result = session.commit_result("add results")
if result.status == "committed":
print(result.commit_id)
elif result.status == "approval_required":
print(result.web_url)
Low-Level Sandboxes and Triggers
repo.shell() and repo.execute() cover most needs. For direct control over
async lifecycle, triggers, and delegation, use repo.sandboxes and
repo.sandbox_triggers. See the
full docs for details.
Error Handling
All SDK exceptions inherit from TildeError:
TildeError # base for all SDK errors
├── ConfigurationError # missing API key, bad endpoint
├── TransportError # network failures, DNS, timeouts
├── SerializationError # invalid JSON in response
├── SandboxError # sandbox lifecycle failure
├── CommandError # non-zero exit (repo.execute / shell.run(check=True))
└── APIError # base for HTTP API errors
├── BadRequestError # 400
├── AuthenticationError # 401
├── ForbiddenError # 403
├── NotFoundError # 404
├── ConflictError # 409
├── GoneError # 410
├── PreconditionFailedError # 412
├── LockedError # 423
└── ServerError # 5xx
APIError carries status_code, message, code, request_id, method,
url, and response_text.
from tilde import NotFoundError, TildeError
try:
with repo.session() as session:
with session.objects.get("nonexistent") as f:
f.read()
except NotFoundError as e:
print(f"Not found: {e.message} (request_id={e.request_id})")
except TildeError as e:
print(f"SDK error: {e}")
Documentation
Full documentation is available at https://docs.tilde.run/python-sdk/.
Development
# Install dev dependencies
uv sync --all-extras
# Run tests
uv run pytest
# Lint and format
uv run ruff check src/ tests/
uv run ruff format src/ tests/
# Type check
uv run mypy src/tilde/
# Build
uv build
License
Apache 2.0
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tilde_sdk-0.7.7.tar.gz.
File metadata
- Download URL: tilde_sdk-0.7.7.tar.gz
- Upload date:
- Size: 169.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.3 {"installer":{"name":"uv","version":"0.10.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e7edf04607a4f78b52fce11cd96bbbbc0a3847351356b0d319b43af95e3d13f2
|
|
| MD5 |
b25066e5e843d5323022cd24efa2337d
|
|
| BLAKE2b-256 |
bd1a25532e98bd6bceaab21714b073b31f6cd8b2f5e35828a9a4855b3b57d74c
|
File details
Details for the file tilde_sdk-0.7.7-py3-none-any.whl.
File metadata
- Download URL: tilde_sdk-0.7.7-py3-none-any.whl
- Upload date:
- Size: 63.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.3 {"installer":{"name":"uv","version":"0.10.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d0956800de62082da333cab8596ebdcd642e583e1c905b27b2a21838be0860ce
|
|
| MD5 |
5c2e0d43321d41c69c0788b4d82192e8
|
|
| BLAKE2b-256 |
5a35b0ceb8146bf1a96d306317e2b66024c1a2b3b222979cfaf4cd70957e01ec
|