Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,4 @@ jobs:
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.8"
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
Loading