Skip to content

Commit 8c417ce

Browse files
committed
initial llm work
1 parent 7e5b66e commit 8c417ce

25 files changed

Lines changed: 2783 additions & 0 deletions

README-SDK.md

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Runloop SDK – Python Object-Oriented Client
2+
3+
The `RunloopSDK` builds on top of the generated REST client and provides a Pythonic, object-oriented API for managing devboxes, blueprints, snapshots, and storage objects. The SDK exposes synchronous and asynchronous variants to match your runtime requirements.
4+
5+
> **Installation**
6+
> The SDK ships with the `runloop_api_client` package—no extra dependencies are required.
7+
8+
```bash
9+
pip install runloop_api_client
10+
```
11+
12+
## Quickstart (synchronous)
13+
14+
```python
15+
from runloop_api_client import RunloopSDK
16+
17+
sdk = RunloopSDK()
18+
19+
# Create a ready-to-use devbox
20+
with sdk.devbox.create(name="my-devbox") as devbox:
21+
result = devbox.cmd.exec("echo 'Hello from Runloop!'")
22+
print(result.stdout())
23+
24+
# Stream stdout in real time
25+
devbox.cmd.exec(
26+
"ls -la",
27+
stdout=lambda line: print("stdout:", line),
28+
output=lambda line: print("combined:", line),
29+
)
30+
31+
# Blueprints
32+
blueprint = sdk.blueprint.create(
33+
name="my-blueprint",
34+
dockerfile="FROM ubuntu:22.04\nRUN echo 'Hello' > /hello.txt\n",
35+
)
36+
devbox = blueprint.create_devbox(name="dev-from-blueprint")
37+
38+
# Storage objects
39+
obj = sdk.storage_object.upload_from_text("Hello world!", name="greeting.txt")
40+
print(obj.download_as_text())
41+
```
42+
43+
## Quickstart (asynchronous)
44+
45+
```python
46+
import asyncio
47+
from runloop_api_client import AsyncRunloopSDK
48+
49+
async def main():
50+
sdk = AsyncRunloopSDK()
51+
async with sdk.devbox.create(name="async-devbox") as devbox:
52+
result = await devbox.cmd.exec("pwd")
53+
print(await result.stdout())
54+
55+
async def capture(line: str) -> None:
56+
print(">>", line)
57+
58+
await devbox.cmd.exec("ls", stdout=capture)
59+
60+
asyncio.run(main())
61+
```
62+
63+
## Available Resources
64+
65+
- **Devbox / AsyncDevbox**
66+
- Creation helpers (`create`, `create_from_blueprint_id`, `create_from_snapshot`, `from_id`)
67+
- Lifecycle management (`await_running`, `suspend`, `resume`, `keep_alive`, `shutdown`)
68+
- Command execution (`cmd.exec`, `cmd.exec_async`) with optional streaming callbacks
69+
- File operations (`read`, `write`, `upload`, `download`)
70+
- Network helpers (`net.create_ssh_key`, `net.create_tunnel`, `net.remove_tunnel`)
71+
72+
- **Blueprint / AsyncBlueprint**
73+
- Build orchestration (`create`)
74+
- Fetch metadata & logs (`get_info`, `logs`)
75+
- Spawn devboxes from existing blueprints (`create_devbox`)
76+
77+
- **Snapshot / AsyncSnapshot**
78+
- List and inspect snapshots (`list`, `get_info`, `await_completed`)
79+
- Metadata updates (`update`), deletion (`delete`)
80+
- Provision new devboxes from snapshots (`create_devbox`)
81+
82+
- **StorageObject / AsyncStorageObject**
83+
- Object creation (`create`, `from_id`, `list`)
84+
- Convenience uploads (`upload_from_file`, `upload_from_text`, `upload_from_bytes`)
85+
- Manual uploads via presigned URLs (`upload_content`, `complete`)
86+
- Downloads (`download_as_text`, `download_as_bytes`)
87+
88+
All objects expose the low-level REST ID through the `id` property, making it easy to cross-reference with existing tooling.
89+
90+
## Streaming Command Output
91+
92+
Pass callbacks into `cmd.exec` / `cmd.exec_async` to process logs in real time. Synchronous callbacks receive strings; asynchronous callbacks may return either `None` or `Awaitable[None]`.
93+
94+
```python
95+
def handle_output(line: str) -> None:
96+
print("LOG:", line)
97+
98+
result = devbox.cmd.exec(
99+
"python train.py",
100+
stdout=handle_output,
101+
stderr=lambda line: print("ERR:", line),
102+
output=lambda line: print("ANY:", line),
103+
)
104+
print("exit code:", result.exit_code)
105+
```
106+
107+
Async example:
108+
109+
```python
110+
async def capture(line: str) -> None:
111+
await log_queue.put(line)
112+
113+
await devbox.cmd.exec(
114+
"tail -f /var/log/app.log",
115+
stdout=capture,
116+
)
117+
```
118+
119+
## Storage Object Upload Helpers
120+
121+
The storage helpers manage the multi-step upload flow (create → PUT to presigned URL → complete):
122+
123+
```python
124+
from pathlib import Path
125+
126+
# Upload local file with content-type detection
127+
obj = sdk.storage_object.upload_from_file(Path("./report.csv"))
128+
129+
# Manual control
130+
obj = sdk.storage_object.create("data.bin", content_type="binary")
131+
obj.upload_content(b"\xDE\xAD\xBE\xEF")
132+
obj.complete()
133+
```
134+
135+
## Accessing the Generated REST Client
136+
137+
The SDK always exposes the underlying generated client through the `.api` attribute:
138+
139+
```python
140+
sdk = RunloopSDK()
141+
raw_devbox = sdk.api.devboxes.create()
142+
```
143+
144+
This makes it straightforward to mix high-level helpers with low-level calls whenever you need advanced control.
145+
146+
## Feedback
147+
148+
The object-oriented SDK is new for Python—feedback and ideas are welcome! Please open an issue or pull request on GitHub if you spot gaps, bugs, or ergonomic improvements.
149+

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ pip install runloop_api_client
2626

2727
The full API of this library can be found in [api.md](api.md).
2828

29+
### Object-Oriented SDK
30+
31+
For a higher-level, Pythonic interface, check out the new [`RunloopSDK`](README-SDK.md) which layers an object-oriented API on top of the generated client (including synchronous and asynchronous variants).
32+
2933
```python
3034
import os
3135
from runloop_api_client import Runloop

src/runloop_api_client/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import typing as _t
44

55
from . import types
6+
from .sdk import RunloopSDK, AsyncRunloopSDK
67
from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes, omit, not_given
78
from ._utils import file_from_path
89
from ._client import Client, Stream, Runloop, Timeout, Transport, AsyncClient, AsyncStream, AsyncRunloop, RequestOptions
@@ -39,6 +40,8 @@
3940
"NotGiven",
4041
"NOT_GIVEN",
4142
"not_given",
43+
"RunloopSDK",
44+
"AsyncRunloopSDK",
4245
"Omit",
4346
"omit",
4447
"RunloopError",
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from __future__ import annotations
2+
3+
from ._sync import RunloopSDK
4+
from ._async import AsyncRunloopSDK
5+
from .devbox import Devbox, DevboxClient
6+
from .snapshot import Snapshot, SnapshotClient
7+
from .blueprint import Blueprint, BlueprintClient
8+
from .execution import Execution
9+
from .async_devbox import AsyncDevbox, AsyncDevboxClient
10+
from .async_snapshot import AsyncSnapshot, AsyncSnapshotClient
11+
from .storage_object import StorageObject, StorageObjectClient
12+
from .async_blueprint import AsyncBlueprint, AsyncBlueprintClient
13+
from .async_execution import AsyncExecution
14+
from .execution_result import ExecutionResult
15+
from .async_storage_object import AsyncStorageObject, AsyncStorageObjectClient
16+
from .async_execution_result import AsyncExecutionResult
17+
18+
__all__ = [
19+
"RunloopSDK",
20+
"AsyncRunloopSDK",
21+
"Devbox",
22+
"DevboxClient",
23+
"Execution",
24+
"ExecutionResult",
25+
"Blueprint",
26+
"BlueprintClient",
27+
"Snapshot",
28+
"SnapshotClient",
29+
"StorageObject",
30+
"StorageObjectClient",
31+
"AsyncDevbox",
32+
"AsyncDevboxClient",
33+
"AsyncExecution",
34+
"AsyncExecutionResult",
35+
"AsyncBlueprint",
36+
"AsyncBlueprintClient",
37+
"AsyncSnapshot",
38+
"AsyncSnapshotClient",
39+
"AsyncStorageObject",
40+
"AsyncStorageObjectClient",
41+
]
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from __future__ import annotations
2+
3+
from typing import Mapping
4+
5+
import httpx
6+
7+
from .._types import Timeout, NotGiven, not_given
8+
from .._client import AsyncRunloop
9+
from .async_devbox import AsyncDevboxClient
10+
from .async_snapshot import AsyncSnapshotClient
11+
from .async_blueprint import AsyncBlueprintClient
12+
from .async_storage_object import AsyncStorageObjectClient
13+
14+
15+
class AsyncRunloopSDK:
16+
"""
17+
High-level asynchronous entry point for the Runloop SDK.
18+
19+
The generated async REST client remains available via the ``api`` attribute.
20+
Higher-level helpers will be introduced incrementally.
21+
"""
22+
23+
api: AsyncRunloop
24+
devbox: AsyncDevboxClient
25+
blueprint: AsyncBlueprintClient
26+
snapshot: AsyncSnapshotClient
27+
storage_object: AsyncStorageObjectClient
28+
29+
def __init__(
30+
self,
31+
*,
32+
client: AsyncRunloop | None = None,
33+
bearer_token: str | None = None,
34+
base_url: str | httpx.URL | None = None,
35+
timeout: float | Timeout | None | NotGiven = not_given,
36+
max_retries: int | None = None,
37+
default_headers: Mapping[str, str] | None = None,
38+
default_query: Mapping[str, object] | None = None,
39+
http_client: httpx.AsyncClient | None = None,
40+
_strict_response_validation: bool = False,
41+
) -> None:
42+
"""
43+
Create an asynchronous Runloop SDK instance.
44+
45+
Arguments mirror :class:`runloop_api_client.AsyncRunloop`.
46+
"""
47+
if client is None:
48+
runloop_kwargs: dict[str, object] = {
49+
"bearer_token": bearer_token,
50+
"base_url": base_url,
51+
"timeout": timeout,
52+
"default_headers": default_headers,
53+
"default_query": default_query,
54+
"http_client": http_client,
55+
"_strict_response_validation": _strict_response_validation,
56+
}
57+
if max_retries is not None:
58+
runloop_kwargs["max_retries"] = max_retries
59+
60+
self.api = AsyncRunloop(**runloop_kwargs)
61+
self._owns_client = True
62+
else:
63+
self.api = client
64+
self._owns_client = False
65+
66+
self.devbox = AsyncDevboxClient(self.api)
67+
self.blueprint = AsyncBlueprintClient(self.api, self.devbox)
68+
self.snapshot = AsyncSnapshotClient(self.api, self.devbox)
69+
self.storage_object = AsyncStorageObjectClient(self.api)
70+
71+
async def aclose(self) -> None:
72+
"""Close the underlying async HTTP client."""
73+
if self._owns_client:
74+
await self.api.close()
75+
76+
async def __aenter__(self) -> "AsyncRunloopSDK":
77+
return self
78+
79+
async def __aexit__(self, *_exc_info: object) -> None:
80+
await self.aclose()
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from __future__ import annotations
2+
3+
import io
4+
import os
5+
from typing import Union
6+
from pathlib import Path
7+
8+
from .._types import FileTypes
9+
from .._utils import file_from_path
10+
11+
UploadInput = Union[FileTypes, str, os.PathLike[str], Path, bytes, bytearray, io.IOBase]
12+
13+
14+
def normalize_upload_input(file: UploadInput) -> FileTypes:
15+
"""
16+
Normalize a variety of Python file representations into the generated client's FileTypes.
17+
"""
18+
if isinstance(file, tuple):
19+
return file
20+
if isinstance(file, bytes):
21+
return file
22+
if isinstance(file, bytearray):
23+
return bytes(file)
24+
if isinstance(file, (str, Path, os.PathLike)):
25+
return file_from_path(file)
26+
if isinstance(file, io.TextIOBase):
27+
return file.read().encode("utf-8")
28+
if isinstance(file, io.BufferedIOBase) or isinstance(file, io.RawIOBase):
29+
return file
30+
if isinstance(file, io.IOBase) and hasattr(file, "read"):
31+
data = file.read()
32+
if isinstance(data, str):
33+
return data.encode("utf-8")
34+
return data
35+
raise TypeError("Unsupported file type for upload. Provide path, bytes, or file-like object.")

0 commit comments

Comments
 (0)