Skip to content

Commit 5fb7945

Browse files
committed
added context helpers to SDKs
1 parent 4f47bf3 commit 5fb7945

5 files changed

Lines changed: 108 additions & 16 deletions

File tree

README-SDK.md

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,36 @@ The `RunloopSDK` builds on top of the underlying REST client and provides a Pyth
44

55
## Table of Contents
66

7-
- [Installation](#installation)
8-
- [Quickstart (synchronous)](#quickstart-synchronous)
9-
- [Quickstart (asynchronous)](#quickstart-asynchronous)
10-
- [Core Concepts](#core-concepts)
11-
- [Devbox](#devbox)
12-
- [Blueprint](#blueprint)
13-
- [Snapshot](#snapshot)
14-
- [StorageObject](#storageobject)
15-
- [Mounting Storage Objects to Devboxes](#mounting-storage-objects-to-devboxes)
16-
- [Accessing the Underlying REST Client](#accessing-the-underlying-rest-client)
17-
- [Error Handling](#error-handling)
18-
- [Advanced Configuration](#advanced-configuration)
19-
- [Async Usage](#async-usage)
20-
- [Polling Configuration](#polling-configuration)
21-
- [Complete API Reference](#complete-api-reference)
22-
- [Feedback](#feedback)
7+
- [Runloop SDK – Python Object-Oriented Client](#runloop-sdk--python-object-oriented-client)
8+
- [Table of Contents](#table-of-contents)
9+
- [Installation](#installation)
10+
- [Quickstart (synchronous)](#quickstart-synchronous)
11+
- [Quickstart (asynchronous)](#quickstart-asynchronous)
12+
- [Core Concepts](#core-concepts)
13+
- [RunloopSDK](#runloopsdk)
14+
- [Available Resources](#available-resources)
15+
- [Devbox](#devbox)
16+
- [Command Execution](#command-execution)
17+
- [Execution Management](#execution-management)
18+
- [Execution Results](#execution-results)
19+
- [Streaming Command Output](#streaming-command-output)
20+
- [File Operations](#file-operations)
21+
- [Network Operations](#network-operations)
22+
- [Snapshot Operations](#snapshot-operations)
23+
- [Devbox Lifecycle Management](#devbox-lifecycle-management)
24+
- [Context Manager Support](#context-manager-support)
25+
- [Blueprint](#blueprint)
26+
- [Snapshot](#snapshot)
27+
- [StorageObject](#storageobject)
28+
- [Storage Object Upload Helpers](#storage-object-upload-helpers)
29+
- [Mounting Storage Objects to Devboxes](#mounting-storage-objects-to-devboxes)
30+
- [Accessing the Underlying REST Client](#accessing-the-underlying-rest-client)
31+
- [Error Handling](#error-handling)
32+
- [Advanced Configuration](#advanced-configuration)
33+
- [Async Usage](#async-usage)
34+
- [Polling Configuration](#polling-configuration)
35+
- [Complete API Reference](#complete-api-reference)
36+
- [Feedback](#feedback)
2337

2438
## Installation
2539

@@ -409,6 +423,44 @@ blueprint = runloop.blueprint.create(
409423
system_setup_commands=["pip install numpy pandas"],
410424
)
411425

426+
# Or create a blueprint with a Docker build context from a local directory
427+
from pathlib import Path
428+
from runloop_api_client.lib.context_loader import build_docker_context_tar
429+
430+
context_root = Path("./my-app")
431+
tar_bytes = build_docker_context_tar(context_root)
432+
433+
build_ctx_obj = runloop.storage_object.upload_from_bytes(
434+
data=tar_bytes,
435+
name="my-app-context.tar.gz",
436+
content_type="tgz",
437+
)
438+
439+
shared_root = Path("./shared-lib")
440+
shared_tar = build_docker_context_tar(shared_root)
441+
442+
shared_ctx_obj = runloop.storage_object.upload_from_bytes(
443+
data=shared_tar,
444+
name="shared-lib-context.tar.gz",
445+
content_type="tgz",
446+
)
447+
448+
blueprint_with_context = runloop.blueprint.create(
449+
name="my-blueprint-with-context",
450+
dockerfile=\"\"\"\
451+
FROM ubuntu:22.04
452+
WORKDIR /app
453+
# use the named context
454+
RUN --mount=type=bind,from=shared,source=/,target=/shared ls -R /shared
455+
\"\"\",
456+
# Primary build context
457+
build_context=build_ctx_obj.as_build_context(),
458+
# Additional named build contexts (for Docker buildx-style usage)
459+
named_build_contexts={
460+
"shared": shared_ctx_obj.as_build_context(),
461+
},
462+
)
463+
412464
# Or get an existing one
413465
blueprint = runloop.blueprint.from_id(blueprint_id="bpt_123")
414466

src/runloop_api_client/sdk/async_storage_object.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,17 @@ async def upload_content(self, content: str | bytes) -> None:
158158
response = await self._client._client.put(url, content=content)
159159
response.raise_for_status()
160160

161+
def as_build_context(self) -> dict[str, str]:
162+
"""Return this object in the shape expected for a Blueprint build context.
163+
164+
The returned dict can be passed directly to ``build_context`` or
165+
``named_build_contexts`` when creating a blueprint.
166+
"""
167+
return {
168+
"object_id": self._id,
169+
"type": "object",
170+
}
171+
161172
def _ensure_upload_url(self) -> str:
162173
"""Return the upload URL, ensuring it exists.
163174

src/runloop_api_client/sdk/storage_object.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,17 @@ def upload_content(self, content: str | bytes) -> None:
158158
response = self._client._client.put(url, content=content)
159159
response.raise_for_status()
160160

161+
def as_build_context(self) -> dict[str, str]:
162+
"""Return this object in the shape expected for a Blueprint build context.
163+
164+
The returned dict can be passed directly to ``build_context`` or
165+
``named_build_contexts`` when creating a blueprint.
166+
"""
167+
return {
168+
"object_id": self._id,
169+
"type": "object",
170+
}
171+
161172
def _ensure_upload_url(self) -> str:
162173
"""Return the upload URL, ensuring it is present.
163174

tests/sdk/test_async_clients.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,15 @@ async def test_upload_from_file_missing_path(self, mock_async_client: AsyncMock,
327327
with pytest.raises(OSError, match="Failed to read file"):
328328
await client.upload_from_file(missing_file)
329329

330+
def test_as_build_context(self, mock_async_client: AsyncMock, object_view: MockObjectView) -> None:
331+
"""as_build_context should return the correct dict shape."""
332+
obj = AsyncStorageObject(mock_async_client, object_view.id, upload_url=None)
333+
334+
assert obj.as_build_context() == {
335+
"object_id": object_view.id,
336+
"type": "object",
337+
}
338+
330339

331340
class TestAsyncRunloopSDK:
332341
"""Tests for AsyncRunloopSDK class."""

tests/sdk/test_clients.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,15 @@ def test_upload_from_file_missing_path(self, mock_client: Mock, tmp_path: Path)
306306
with pytest.raises(OSError, match="Failed to read file"):
307307
client.upload_from_file(missing_file)
308308

309+
def test_as_build_context(self, mock_client: Mock, object_view: MockObjectView) -> None:
310+
"""as_build_context should return the correct dict shape."""
311+
obj = StorageObject(mock_client, object_view.id, upload_url=None)
312+
313+
assert obj.as_build_context() == {
314+
"object_id": object_view.id,
315+
"type": "object",
316+
}
317+
309318

310319
class TestRunloopSDK:
311320
"""Tests for RunloopSDK class."""

0 commit comments

Comments
 (0)