Skip to content
Open
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
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Welcome to the official Python library for Runpod API & SDK.
- [Quick Start](#quick-start)
- [Local Test Worker](#local-test-worker)
- [📚 | REST API v2 Wrapper](#--rest-api-v2-wrapper)
- [Sandboxes](#sandboxes)
- [Endpoints](#endpoints)
- [GPU Cloud (Pods)](#gpu-cloud-pods)
- [📁 | Directory](#--directory)
Expand Down Expand Up @@ -172,6 +173,41 @@ import runpod
runpod.api_key = "your_runpod_api_key_found_under_settings"
```

### Sandboxes

`Sandbox` and `AsyncioSandbox` manage isolated CPU sandboxes through REST API v2. Set `RUNPOD_API_KEY`, assign `runpod.api_key`, or pass `api_key` to a handle. Supply exactly one of `image_name` or `template_id`.

```python
from runpod import Sandbox

with Sandbox(image_name="python:3.12-slim") as sandbox:
result = sandbox.exec(["python", "-c", "print('hello from a sandbox')"], check=True)
print(result.output)
```

Use `AsyncioSandbox` in asynchronous applications:

```python
import asyncio
from runpod import AsyncioSandbox

async def main():
async with AsyncioSandbox(image_name="python:3.12-slim") as sandbox:
result = await sandbox.exec(
["python", "-c", "print('hello from a sandbox')"],
check=True,
)
print(result.output)

asyncio.run(main())
```

These contexts create a sandbox on entry and terminate it on exit, including when the body raises. `Sandbox.create(...)` and `await AsyncioSandbox.create(...)` return owned handles for explicit lifetime management. Call `terminate()` to release remote compute; `close()` releases local connections only. Handles returned by `get(sandbox_id)` or `list(state=..., labels=...)` are borrowed, so their contexts only close local resources. Async factories and lifecycle methods are awaited.

Handle properties such as `state`, `compute`, and `expires_at` read cached metadata; `refresh()` fetches a current snapshot. Command execution accepts an argument sequence. `check=True` raises `SandboxExecutionError` with the partial output available in `error.result`. Explicit startup rejections are retried within `startup_timeout`; ambiguous transport failures are not replayed.

`sandbox.logs(source="container", tail=10)` streams typed log events from the container's main process; `source="system"` selects lifecycle logs. Command output is returned by `exec`, not this stream. Use `with` and regular iteration for synchronous log streams, or `async with` and `async for` for asynchronous streams, to close the connection when stopping early. Preserve an event's `id` and pass it as `last_event_id` to resume a stream, or filter by `since`.

### Endpoints

You can interact with Runpod endpoints via a `run` or `run_sync` method.
Expand Down
16 changes: 10 additions & 6 deletions runpod/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
""" Allows runpod to be imported as a module. """
"""Allows runpod to be imported as a module."""

import logging
import os
Expand Down Expand Up @@ -29,19 +29,20 @@
set_credentials,
)
from .endpoint import AsyncioEndpoint, AsyncioJob, Endpoint
from .sandbox import AsyncioSandbox, Sandbox
from .serverless.modules.rp_logger import RunPodLogger
from .version import __version__

__all__ = [
# API functions
"create_container_registry_auth",
"create_endpoint",
"create_endpoint",
"create_pod",
"create_template",
"delete_container_registry_auth",
"get_endpoints",
"get_gpu",
"get_gpus",
"get_gpus",
"get_pod",
"get_pods",
"get_user",
Expand All @@ -53,12 +54,15 @@
"update_user_settings",
# Config functions
"check_credentials",
"get_credentials",
"get_credentials",
"set_credentials",
# Endpoint classes
"AsyncioEndpoint",
"AsyncioJob",
"Endpoint",
# Sandbox classes
"AsyncioSandbox",
"Sandbox",
# Serverless module
"serverless",
# Logger class
Expand All @@ -68,8 +72,8 @@
# Module variables
"SSH_KEY_PATH",
"profile",
"api_key",
"endpoint_url_base"
"api_key",
"endpoint_url_base",
]

# ------------------------------- Config Paths ------------------------------- #
Expand Down
38 changes: 28 additions & 10 deletions runpod/api/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,20 @@


def _resolve_api_key(api_key: Optional[str]) -> str:
from runpod import api_key as global_api_key # pylint: disable=import-outside-toplevel,cyclic-import
from runpod import (
api_key as global_api_key,
) # pylint: disable=import-outside-toplevel,cyclic-import

effective_api_key = api_key or global_api_key
if not effective_api_key:
raise error.AuthenticationError("No API key provided")
return effective_api_key


def _build_url(path: str) -> str:
api_url_base = os.environ.get("RUNPOD_API_BASE_URL", "https://api.runpod.io")
def _build_url(path: str, base_url: Optional[str] = None) -> str:
api_url_base = base_url or os.environ.get(
"RUNPOD_API_BASE_URL", "https://api.runpod.io"
)
return f"{api_url_base.rstrip('/')}/{path.lstrip('/')}"


Expand All @@ -45,30 +49,44 @@ def _response_json(response: requests.Response) -> dict[str, Any]:
return payload if isinstance(payload, dict) else {}


def _raise_for_error(
response: requests.Response, method: str, path: str
def _raise_for_status(
status_code: int,
payload: Mapping[str, Any],
text: str,
method: str,
path: str,
) -> None:
if response.status_code == HTTP_STATUS_UNAUTHORIZED:
"""Map HTTP error details consistently across REST transports."""
if status_code == HTTP_STATUS_UNAUTHORIZED:
raise error.AuthenticationError(
"Unauthorized request, please check your API key."
)

if response.status_code < HTTP_STATUS_BAD_REQUEST:
if status_code < HTTP_STATUS_BAD_REQUEST:
return

payload = _response_json(response)
message = payload.get("detail") or payload.get("title")
if not message:
message = response.text or f"Request failed with status {response.status_code}"
message = text or f"Request failed with status {status_code}"

raise error.QueryError(
str(message),
f"{method.upper()} {path}",
status_code=response.status_code,
status_code=status_code,
errors=payload.get("errors"),
)


def _raise_for_error(response: requests.Response, method: str, path: str) -> None:
if response.status_code < HTTP_STATUS_BAD_REQUEST:
return
if response.status_code == HTTP_STATUS_UNAUTHORIZED:
_raise_for_status(response.status_code, {}, "", method, path)
_raise_for_status(
response.status_code, _response_json(response), response.text, method, path
)


def run_rest_request(
method: str,
path: str,
Expand Down
Loading