Install

pip install contree-sdk

Quick Start

Pull an image, run a command, read the output. Three lines of real code:

from contree_sdk import ContreeSync

contree = ContreeSync()
image = contree.images.pull("python:3.11-slim")
result = image.run(shell='python -c "print(2 ** 128)"').wait()
print(result.stdout)

The async counterpart looks the same, minus .wait():

from contree_sdk import Contree

contree = Contree()
image = await contree.images.pull("python:3.11-slim")
result = await image.run(shell='python -c "print(2 ** 128)"')

Both clients read CONTREE_TOKEN and CONTREE_BASE_URL from the environment by default, or accept them explicitly via ContreeConfig.

Images and Branching

Every non-disposable run produces a new immutable image. That image can be used as the starting point for the next command, creating a chain of states you can branch from at any point.

base = contree.images.pull("ubuntu:noble")

# Install dependencies once
env = base.run(
    shell="apt-get update && apt-get install -y build-essential",
    disposable=False,
).wait()

# Branch from the same prepared state
branch_a = env.run(shell="gcc -O2 solution_a.c -o a.out", files={"solution_a.c": "a.c"}).wait()
branch_b = env.run(shell="gcc -O2 solution_b.c -o b.out", files={"solution_b.c": "b.c"}).wait()

Each branch starts from an identical filesystem. No cleanup, no side effects between runs.

Sessions

Sessions track state automatically. Each command picks up where the previous one left off:

session = image.session()
session.run(shell="pip install numpy pandas", disposable=False).wait()
result = session.run(shell="python -c 'import numpy; print(numpy.__version__)'").wait()
print(result.stdout)

No need to pass UUIDs between calls. The session holds the current image reference internally.

File Handling

Upload files into a run, or download artifacts out of one:

# Upload files directly in a run
result = image.run(
    shell="python /app/main.py",
    files={"/app/main.py": "./main.py", "/app/data.csv": "./data.csv"},
).wait()

# Pre-upload for reuse across multiple runs
uploaded = contree.files.upload("./model.bin")
r1 = image.run(shell="./evaluate --model /model.bin", files={"/model.bin": uploaded}).wait()
r2 = image.run(shell="./benchmark --model /model.bin", files={"/model.bin": uploaded}).wait()

# Download artifacts
await result.download("/output/report.html", "./report.html")
content = await result.read("/output/metrics.json")

Subprocess Interface

For code that already works with subprocess.Popen, the SDK provides a drop-in replacement:

proc = image.popen(
    ["python", "test_suite.py"],
    cwd="/workspace",
    env={"PYTHONDONTWRITEBYTECODE": "1"},
)
stdout, stderr = proc.communicate(timeout=120)
print(proc.returncode)

Same patterns, same attribute names, sandboxed execution.

Flexible I/O

Control how stdout and stderr are captured:

# As string (default)
result = await image.run(shell="ls /", stdout=str)

# As bytes
result = await image.run(shell="cat /bin/ls", stdout=bytes)

# Into a buffer
from io import StringIO
buf = StringIO()
result = await image.run(shell="env", stdout=buf)

Error Handling

The SDK provides specific exception types for each failure mode:

  • ContreeImageStateError — invalid state transition.
  • FailedOperationError — command or import failed.
  • OperationTimedOutError — execution exceeded the timeout.
  • NotFoundError — image or resource not found.

All inherit from ContreeError, so a single except clause can catch everything.

Takeaway

The ConTree SDK turns ConTree's HTTP API into native Python objects. Pull images, execute commands, branch states, move files, and handle errors with the same patterns you already use. Install it, set a token, and start building.

Get Started