Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- name: Install uv
uses: runloopai/setup-uv@main
with:
version: '0.8.11'
version: '0.9.13'

- name: Install dependencies
run: uv sync --all-extras
Expand All @@ -46,7 +46,7 @@ jobs:
- name: Install uv
uses: runloopai/setup-uv@main
with:
version: '0.8.11'
version: '0.9.13'

- name: Install dependencies
run: uv sync --all-extras
Expand Down Expand Up @@ -80,10 +80,10 @@ jobs:
- name: Install uv
uses: runloopai/setup-uv@main
with:
version: '0.8.11'
version: '0.9.13'

- name: Bootstrap
run: ./scripts/bootstrap

- name: Run tests
run: ./scripts/test --ignore=tests/smoketests
run: ./scripts/test
28 changes: 18 additions & 10 deletions README-SDK.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ print(obj.download_as_text())
import asyncio
from runloop_api_client import AsyncRunloopSDK


async def main():
runloop = AsyncRunloopSDK()
async with await runloop.devbox.create(name="async-devbox") as devbox:
Expand All @@ -88,6 +89,7 @@ async def main():

await devbox.cmd.exec("ls", stdout=capture)


asyncio.run(main())
```

Expand Down Expand Up @@ -194,7 +196,7 @@ print("Devbox ID:", execution.devbox_id)
# Poll for current state
state = execution.get_state()
print("Status:", state.status) # "running", "completed", etc.
print("Exit code:", state.exit_status) # only set when execution has completed
print("Exit code:", state.exit_status) # only set when execution has completed

# Wait for completion and get results
result = execution.result()
Expand Down Expand Up @@ -229,7 +231,7 @@ result = execution.result()
# Access execution results
print("Exit code:", result.exit_code)
print("Success:", result.success) # True if exit code is 0
print("Failed:", result.failed) # True if exit code is non-zero
print("Failed:", result.failed) # True if exit code is non-zero

# Get output streams
stdout = result.stdout()
Expand Down Expand Up @@ -261,6 +263,7 @@ Pass callbacks into `cmd.exec` / `cmd.exec_async` to process logs in real time:
def handle_output(line: str) -> None:
print("LOG:", line)


result = devbox.cmd.exec(
"python train.py",
stdout=handle_output,
Expand All @@ -278,6 +281,7 @@ def capture(line: str) -> None:
# Use thread-safe data structures if needed
log_queue.put_nowait(line)


await devbox.cmd.exec(
"tail -f /var/log/app.log",
stdout=capture,
Expand All @@ -299,6 +303,7 @@ print(content)

# Upload files
from pathlib import Path

devbox.file.upload(
path="/home/user/upload.txt",
file=Path("local_file.txt"),
Expand Down Expand Up @@ -535,6 +540,7 @@ storage_object.complete()

# Upload from file
from pathlib import Path

uploaded = runloop.storage_object.upload_from_file(
Path("/path/to/file.txt"),
name="my-file.txt",
Expand Down Expand Up @@ -584,7 +590,7 @@ obj = runloop.storage_object.create(
name="data.bin",
content_type="binary",
)
obj.upload_content(b"\xDE\xAD\xBE\xEF")
obj.upload_content(b"\xde\xad\xbe\xef")
obj.complete()
```

Expand Down Expand Up @@ -731,28 +737,30 @@ The async SDK has the same interface as the synchronous version, but all I/O ope
import asyncio
from runloop_api_client import AsyncRunloopSDK


async def main():
runloop = AsyncRunloopSDK()

# All the same operations, but with await
async with await runloop.devbox.create(name="async-devbox") as devbox:
result = await devbox.cmd.exec("pwd")
print(await result.stdout())

# Streaming (note: callbacks must be synchronous)
def capture(line: str) -> None:
print(">>", line)

await devbox.cmd.exec("ls", stdout=capture)

# Async file operations
await devbox.file.write(path="/tmp/test.txt", contents="Hello")
content = await devbox.file.read(path="/tmp/test.txt")

# Async network operations
tunnel = await devbox.net.create_tunnel(port=8080)
print("Tunnel URL:", tunnel.url)


asyncio.run(main())
```

Expand All @@ -768,15 +776,15 @@ devbox = runloop.devbox.create(
name="my-devbox",
polling_config=PollingConfig(
timeout_seconds=300.0, # Wait up to 5 minutes
interval_seconds=2.0, # Poll every 2 seconds
interval_seconds=2.0, # Poll every 2 seconds
),
)

# Wait for snapshot completion with custom polling
snapshot.await_completed(
polling_config=PollingConfig(
timeout_seconds=600.0, # Wait up to 10 minutes
interval_seconds=5.0, # Poll every 5 seconds
interval_seconds=5.0, # Poll every 5 seconds
),
)
```
Expand Down
51 changes: 15 additions & 36 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ dependencies = [
"sniffio",
"uuid-utils>=0.11.0",
]

requires-python = ">= 3.9"
classifiers = [
"Typing :: Typed",
Expand All @@ -26,6 +25,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Operating System :: OS Independent",
"Operating System :: POSIX",
"Operating System :: MacOS",
Expand All @@ -44,7 +44,13 @@ aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"]

[tool.uv]
managed = true
required-version = ">=0.5.0"
required-version = ">=0.9"
conflicts = [
[
{ group = "pydantic-v1" },
{ group = "pydantic-v2" },
],
]

[dependency-groups]
# version pins are in uv.lock
Expand All @@ -64,51 +70,24 @@ dev = [
"uuid-utils>=0.11.0",
"pytest-cov>=7.0.0",
]
pydantic-v1 = [
"pydantic>=1.9.0,<2",
]
pydantic-v2 = [
"pydantic~=2.0 ; python_full_version < '3.14'",
"pydantic~=2.12 ; python_full_version >= '3.14'",
]
docs = [
"furo>=2025.9.25",
"sphinx>=7.4.7",
"sphinx-autodoc-typehints>=2.3.0",
"sphinx-toolbox>=4.0.0",
]
pydantic-v1 = [
"pydantic>=1.9.0, <2",
]

[tool.rye.scripts]
format = { chain = [
"format:ruff",
"format:docs",
"fix:ruff",
# run formatting again to fix any inconsistencies when imports are stripped
"format:ruff",
]}
"format:docs" = "python scripts/utils/ruffen-docs.py README.md api.md"
"format:ruff" = "ruff format"

"lint" = { chain = [
"check:ruff",
"typecheck",
"check:importable",
]}
"check:ruff" = "ruff check ."
"fix:ruff" = "ruff check --fix ."

"check:importable" = "python -c 'import runloop_api_client'"

typecheck = { chain = [
"typecheck:pyright",
"typecheck:mypy"
]}

"typecheck:pyright" = "pyright"
"typecheck:verify-types" = "pyright --verifytypes runloop_api_client --ignoreexternal"
"typecheck:mypy" = "mypy ."

[build-system]
requires = ["hatchling==1.26.3", "hatch-fancy-pypi-readme"]
build-backend = "hatchling.build"


[tool.hatch.build]
include = [
"src/*"
Expand Down
58 changes: 31 additions & 27 deletions requirements-dev.lock
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
# This file was autogenerated by uv via the following command:
# uv export -o requirements-dev.lock --no-hashes
-e .
annotated-types==0.7.0
# via pydantic
# uv pip compile --group dev --output-file requirements-dev.lock
anyio==4.8.0
# via
# httpx
# runloop-api-client
# via httpx
certifi==2024.12.14
# via
# httpcore
# httpx
colorama==0.4.6 ; sys_platform == 'win32'
# via pytest
coverage==7.10.7
# via pytest-cov
dirty-equals==0.9.0
distro==1.9.0
# via runloop-api-client
exceptiongroup==1.2.2 ; python_full_version < '3.11'
# via runloop-api-client (pyproject.toml:dev)
exceptiongroup==1.2.2
# via
# anyio
# pytest
Expand All @@ -27,68 +21,78 @@ h11==0.16.0
httpcore==1.0.9
# via httpx
httpx==0.28.1
# via
# respx
# runloop-api-client
# via respx
idna==3.10
# via
# anyio
# httpx
importlib-metadata==8.6.1
# via runloop-api-client (pyproject.toml:dev)
iniconfig==2.0.0
# via pytest
markdown-it-py==3.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
mypy==1.14.1
# via runloop-api-client (pyproject.toml:dev)
mypy-extensions==1.0.0
# via mypy
nodeenv==1.9.1
# via pyright
packaging==24.2
# via pytest
pluggy==1.5.0
# via pytest
pydantic==2.10.3
# via runloop-api-client
pydantic-core==2.27.1
# via pydantic
# via
# pytest
# pytest-cov
pygments==2.19.1
# via
# pytest
# rich
pyright==1.1.399
# via runloop-api-client (pyproject.toml:dev)
pytest==8.4.1
# via
# runloop-api-client (pyproject.toml:dev)
# pytest-asyncio
# pytest-cov
# pytest-timeout
# pytest-xdist
pytest-asyncio==0.24.0
# via runloop-api-client (pyproject.toml:dev)
pytest-cov==7.0.0
# via runloop-api-client (pyproject.toml:dev)
pytest-timeout==2.4.0
# via runloop-api-client (pyproject.toml:dev)
pytest-xdist==3.7.0
# via runloop-api-client (pyproject.toml:dev)
python-dateutil==2.9.0.post0
# via time-machine
respx==0.22.0
# via runloop-api-client (pyproject.toml:dev)
rich==13.9.4
# via runloop-api-client (pyproject.toml:dev)
ruff==0.9.4
# via runloop-api-client (pyproject.toml:dev)
six==1.17.0
# via python-dateutil
sniffio==1.3.1
# via
# anyio
# runloop-api-client
# via anyio
time-machine==2.16.0
tomli==2.2.1 ; python_full_version < '3.11'
# via runloop-api-client (pyproject.toml:dev)
tomli==2.2.1
# via
# coverage
# mypy
# pytest
typing-extensions==4.12.2
# via
# anyio
# mypy
# pydantic
# pydantic-core
# pyright
# rich
# runloop-api-client
uuid-utils==0.12.0
# via runloop-api-client (pyproject.toml:dev)
zipp==3.21.0
# via importlib-metadata
1 change: 0 additions & 1 deletion scripts/bootstrap
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,3 @@ uv python install
echo "==> Installing Python dependencies…"
uv sync --all-extras

uv sync --all-extras --all-groups
6 changes: 4 additions & 2 deletions scripts/format
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ set -e

cd "$(dirname "$0")/.."

echo "==> Running formatters"
echo "==> Running ruff"
uv run ruff format
uv run python scripts/utils/ruffen-docs.py README.md api.md
uv run ruff check --fix .
# run formatting again to fix any inconsistencies when imports are stripped
uv run ruff format

echo "==> Formatting docs"
uv run python scripts/utils/ruffen-docs.py README.md api.md README-SDK.md
Loading