Source code for runloop_api_client.sdk.sync

"""Synchronous SDK entry points and management interfaces."""

from __future__ import annotations

import io
import tarfile
from typing import Dict, Mapping, Optional
from pathlib import Path
from datetime import timedelta
from typing_extensions import Unpack

import httpx

from ._types import (
    LongRequestOptions,
    SDKDevboxListParams,
    SDKObjectListParams,
    SDKDevboxCreateParams,
    SDKObjectCreateParams,
    SDKBlueprintListParams,
    SDKBlueprintCreateParams,
    SDKDiskSnapshotListParams,
    SDKDevboxCreateFromImageParams,
)
from .devbox import Devbox
from .._types import Timeout, NotGiven, not_given
from .._client import DEFAULT_MAX_RETRIES, Runloop
from ._helpers import detect_content_type
from .snapshot import Snapshot
from .blueprint import Blueprint
from .storage_object import StorageObject
from ..types.object_create_params import ContentType


[docs] class DevboxOps: """High-level manager for creating and managing Devbox instances. Accessed via ``runloop.devbox`` from :class:`RunloopSDK`, provides methods to create devboxes from scratch, blueprints, or snapshots, and to list existing devboxes. Example: >>> runloop = RunloopSDK() >>> devbox = runloop.devbox.create(name="my-devbox") >>> devboxes = runloop.devbox.list(limit=10) """ def __init__(self, client: Runloop) -> None: """Initialize the manager. :param client: Generated Runloop client to wrap :type client: Runloop """ self._client = client
[docs] def create( self, **params: Unpack[SDKDevboxCreateParams], ) -> Devbox: """Provision a new devbox and wait until it reaches ``running`` state. :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKDevboxCreateParams` for available parameters :return: Wrapper bound to the newly created devbox :rtype: Devbox """ devbox_view = self._client.devboxes.create_and_await_running( **params, ) return Devbox(self._client, devbox_view.id)
[docs] def create_from_blueprint_id( self, blueprint_id: str, **params: Unpack[SDKDevboxCreateFromImageParams], ) -> Devbox: """Create a devbox from an existing blueprint by identifier. :param blueprint_id: Blueprint ID to create from :type blueprint_id: str :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKDevboxCreateFromImageParams` for available parameters :type params: :return: Wrapper bound to the newly created devbox :rtype: Devbox """ devbox_view = self._client.devboxes.create_and_await_running( blueprint_id=blueprint_id, **params, ) return Devbox(self._client, devbox_view.id)
[docs] def create_from_blueprint_name( self, blueprint_name: str, **params: Unpack[SDKDevboxCreateFromImageParams], ) -> Devbox: """Create a devbox from the latest blueprint with the given name. :param blueprint_name: Blueprint name to create from :type blueprint_name: str :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKDevboxCreateFromImageParams` for available parameters :return: Wrapper bound to the newly created devbox :rtype: Devbox """ devbox_view = self._client.devboxes.create_and_await_running( blueprint_name=blueprint_name, **params, ) return Devbox(self._client, devbox_view.id)
[docs] def create_from_snapshot( self, snapshot_id: str, **params: Unpack[SDKDevboxCreateFromImageParams], ) -> Devbox: """Create a devbox initialized from a snapshot. :param snapshot_id: Snapshot ID to create from :type snapshot_id: str :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKDevboxCreateFromImageParams` for available parameters :return: Wrapper bound to the newly created devbox :rtype: Devbox """ devbox_view = self._client.devboxes.create_and_await_running( snapshot_id=snapshot_id, **params, ) return Devbox(self._client, devbox_view.id)
[docs] def from_id(self, devbox_id: str) -> Devbox: """Attach to an existing devbox by ID. Blocks until the devbox reaches ``running`` state so callers can begin issuing commands immediately. :param devbox_id: Existing devbox ID :type devbox_id: str :return: Wrapper bound to the requested devbox :rtype: Devbox """ self._client.devboxes.await_running(devbox_id) return Devbox(self._client, devbox_id)
[docs] def list( self, **params: Unpack[SDKDevboxListParams], ) -> list[Devbox]: """List devboxes accessible to the caller. :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKDevboxListParams` for available parameters :return: Collection of devbox wrappers :rtype: list[Devbox] """ page = self._client.devboxes.list( **params, ) return [Devbox(self._client, item.id) for item in page.devboxes]
[docs] class SnapshotOps: """High-level manager for working with disk snapshots. Accessed via ``runloop.snapshot`` from :class:`RunloopSDK`, provides methods to list snapshots and access snapshot details. Example: >>> runloop = RunloopSDK() >>> snapshots = runloop.snapshot.list(devbox_id="dev-123") >>> snapshot = runloop.snapshot.from_id("snap-123") """ def __init__(self, client: Runloop) -> None: """Initialize the manager with the generated Runloop client. :param client: Generated Runloop client :type client: Runloop """ self._client = client
[docs] def list( self, **params: Unpack[SDKDiskSnapshotListParams], ) -> list[Snapshot]: """List snapshots created from devboxes. :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKDiskSnapshotListParams` for available parameters :return: Snapshot wrappers for each record :rtype: list[Snapshot] """ page = self._client.devboxes.disk_snapshots.list( **params, ) return [Snapshot(self._client, item.id) for item in page.snapshots]
[docs] def from_id(self, snapshot_id: str) -> Snapshot: """Return a snapshot wrapper for the given ID. :param snapshot_id: Snapshot ID to wrap :type snapshot_id: str :return: Wrapper for the snapshot resource :rtype: Snapshot """ return Snapshot(self._client, snapshot_id)
[docs] class BlueprintOps: """High-level manager for creating and managing blueprints. Accessed via ``runloop.blueprint`` from :class:`RunloopSDK`, provides methods to create blueprints with Dockerfiles and system setup commands, and to list existing blueprints. Example: >>> runloop = RunloopSDK() >>> blueprint = runloop.blueprint.create( ... name="my-blueprint", dockerfile="FROM ubuntu:22.04\\nRUN apt-get update" ... ) >>> blueprints = runloop.blueprint.list() """ def __init__(self, client: Runloop) -> None: """Initialize the manager. :param client: Generated Runloop client to wrap :type client: Runloop """ self._client = client
[docs] def create( self, **params: Unpack[SDKBlueprintCreateParams], ) -> Blueprint: """Create a blueprint and wait for the build to finish. :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKBlueprintCreateParams` for available parameters :return: Wrapper bound to the finished blueprint :rtype: Blueprint """ blueprint = self._client.blueprints.create_and_await_build_complete( **params, ) return Blueprint(self._client, blueprint.id)
[docs] def from_id(self, blueprint_id: str) -> Blueprint: """Return a blueprint wrapper for the given ID. :param blueprint_id: Blueprint ID to wrap :type blueprint_id: str :return: Wrapper for the blueprint resource :rtype: Blueprint """ return Blueprint(self._client, blueprint_id)
[docs] def list( self, **params: Unpack[SDKBlueprintListParams], ) -> list[Blueprint]: """List available blueprints. :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKBlueprintListParams` for available parameters :return: Blueprint wrappers for each record :rtype: list[Blueprint] """ page = self._client.blueprints.list( **params, ) return [Blueprint(self._client, item.id) for item in page.blueprints]
[docs] class StorageObjectOps: """High-level manager for creating and managing storage objects. Accessed via ``runloop.storage_object`` from :class:`RunloopSDK`, provides methods to create, upload, download, and list storage objects with convenient helpers for file and text uploads. Example: >>> runloop = RunloopSDK() >>> obj = runloop.storage_object.upload_from_text("Hello!", "greeting.txt") >>> content = obj.download_as_text() >>> objects = runloop.storage_object.list() """ def __init__(self, client: Runloop) -> None: """Initialize the manager with the generated Runloop client. :param client: Generated Runloop client :type client: Runloop """ self._client = client
[docs] def create( self, **params: Unpack[SDKObjectCreateParams], ) -> StorageObject: """Create a storage object and obtain an upload URL. :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKObjectCreateParams` for available parameters :return: Wrapper with upload URL set for immediate uploads :rtype: StorageObject """ obj = self._client.objects.create(**params) return StorageObject(self._client, obj.id, upload_url=obj.upload_url)
[docs] def from_id(self, object_id: str) -> StorageObject: """Return a storage object wrapper by identifier. :param object_id: Storage object identifier to wrap :type object_id: str :return: Wrapper for the storage object resource :rtype: StorageObject """ return StorageObject(self._client, object_id, upload_url=None)
[docs] def list( self, **params: Unpack[SDKObjectListParams], ) -> list[StorageObject]: """List storage objects owned by the caller. :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKObjectListParams` for available parameters :return: Storage object wrappers for each record :rtype: list[StorageObject] """ page = self._client.objects.list( **params, ) return [StorageObject(self._client, item.id, upload_url=item.upload_url) for item in page.objects]
[docs] def upload_from_file( self, file_path: str | Path, *, name: Optional[str] = None, content_type: Optional[ContentType] = None, metadata: Optional[Dict[str, str]] = None, ttl: Optional[timedelta] = None, **options: Unpack[LongRequestOptions], ) -> StorageObject: """Create and upload an object from a local file path. :param file_path: Local filesystem path to read :type file_path: str | Path :param name: Optional object name; defaults to the file name :type name: Optional[str] :param content_type: Optional MIME type to apply to the object :type content_type: Optional[ContentType] :param metadata: Optional key-value metadata :type metadata: Optional[Dict[str, str]] :param ttl: Optional Time-To-Live, after which the object is automatically deleted :type ttl: Optional[timedelta] :param options: See :typeddict:`~runloop_api_client.sdk._types.LongRequestOptions` for available options :return: Wrapper for the uploaded object :rtype: StorageObject :raises OSError: If the local file cannot be read """ path = Path(file_path) try: content = path.read_bytes() except OSError as error: raise OSError(f"Failed to read file {path}: {error}") from error name = name or path.name content_type = content_type or detect_content_type(str(file_path)) ttl_ms = int(ttl.total_seconds()) * 1000 if ttl else None obj = self.create(name=name, content_type=content_type, metadata=metadata, ttl_ms=ttl_ms, **options) obj.upload_content(content) obj.complete() return obj
[docs] def upload_from_dir( self, dir_path: str | Path, *, name: Optional[str] = None, metadata: Optional[Dict[str, str]] = None, ttl: Optional[timedelta] = None, **options: Unpack[LongRequestOptions], ) -> StorageObject: """Create and upload an object from a local directory. The resulting object will be uploaded as a compressed tarball. :param dir_path: Local filesystem directory path to tar :type dir_path: str | Path :param name: Optional object name; defaults to the directory name + '.tar.gz' :type name: Optional[str] :param metadata: Optional key-value metadata :type metadata: Optional[Dict[str, str]] :param ttl: Optional Time-To-Live, after which the object is automatically deleted :type ttl: Optional[timedelta] :param options: See :typeddict:`~runloop_api_client.sdk._types.LongRequestOptions` for available options :return: Wrapper for the uploaded object :rtype: StorageObject :raises OSError: If the local file cannot be read """ path = Path(dir_path) name = name or f"{path.name}.tar.gz" ttl_ms = int(ttl.total_seconds()) * 1000 if ttl else None tar_buffer = io.BytesIO() with tarfile.open(fileobj=tar_buffer, mode="w:gz") as tar: tar.add(path, arcname=".", recursive=True) tar_buffer.seek(0) obj = self.create(name=name, content_type="tgz", metadata=metadata, ttl_ms=ttl_ms, **options) obj.upload_content(tar_buffer) obj.complete() return obj
[docs] def upload_from_text( self, text: str, *, name: str, metadata: Optional[Dict[str, str]] = None, ttl: Optional[timedelta] = None, **options: Unpack[LongRequestOptions], ) -> StorageObject: """Create and upload an object from a text payload. :param text: Text content to upload :type text: str :param name: Object display name :type name: str :param metadata: Optional key-value metadata :type metadata: Optional[Dict[str, str]] :param ttl: Optional Time-To-Live, after which the object is automatically deleted :type ttl: Optional[timedelta] :param options: See :typeddict:`~runloop_api_client.sdk._types.LongRequestOptions` for available options :return: Wrapper for the uploaded object :rtype: StorageObject """ ttl_ms = int(ttl.total_seconds()) * 1000 if ttl else None obj = self.create(name=name, content_type="text", metadata=metadata, ttl_ms=ttl_ms, **options) obj.upload_content(text) obj.complete() return obj
[docs] def upload_from_bytes( self, data: bytes, *, name: str, content_type: ContentType, metadata: Optional[Dict[str, str]] = None, ttl: Optional[timedelta] = None, **options: Unpack[LongRequestOptions], ) -> StorageObject: """Create and upload an object from a bytes payload. :param data: Binary payload to upload :type data: bytes :param name: Object display name :type name: str :param content_type: MIME type describing the payload :type content_type: ContentType :param metadata: Optional key-value metadata :type metadata: Optional[Dict[str, str]] :param ttl: Optional Time-To-Live, after which the object is automatically deleted :type ttl: Optional[timedelta] :param options: See :typeddict:`~runloop_api_client.sdk._types.LongRequestOptions` for available options :return: Wrapper for the uploaded object :rtype: StorageObject """ ttl_ms = int(ttl.total_seconds()) * 1000 if ttl else None obj = self.create(name=name, content_type=content_type, metadata=metadata, ttl_ms=ttl_ms, **options) obj.upload_content(data) obj.complete() return obj
class RunloopSDK: """High-level synchronous entry point for the Runloop SDK. Provides a Pythonic, object-oriented interface for managing devboxes, blueprints, snapshots, and storage objects. Exposes the generated REST client via the ``api`` attribute for advanced use cases. :ivar api: Direct access to the generated REST API client :vartype api: Runloop :ivar devbox: High-level interface for devbox management :vartype devbox: DevboxOps :ivar blueprint: High-level interface for blueprint management :vartype blueprint: BlueprintOps :ivar snapshot: High-level interface for snapshot management :vartype snapshot: SnapshotOps :ivar storage_object: High-level interface for storage object management :vartype storage_object: StorageObjectOps Example: >>> runloop = RunloopSDK() # Uses RUNLOOP_API_KEY env var >>> devbox = runloop.devbox.create(name="my-devbox") >>> result = devbox.cmd.exec(command="echo 'hello'") >>> print(result.stdout()) >>> devbox.shutdown() """ api: Runloop devbox: DevboxOps blueprint: BlueprintOps snapshot: SnapshotOps storage_object: StorageObjectOps def __init__( self, *, bearer_token: str | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, default_query: Mapping[str, object] | None = None, http_client: httpx.Client | None = None, ) -> None: """Configure the synchronous SDK wrapper. :param bearer_token: API token; falls back to ``RUNLOOP_API_KEY`` env var, defaults to None :type bearer_token: str | None, optional :param base_url: Override the API base URL, defaults to None :type base_url: str | httpx.URL | None, optional :param timeout: Request timeout (seconds) or ``Timeout`` object, defaults to not_given :type timeout: float | Timeout | None | NotGiven, optional :param max_retries: Maximum automatic retry attempts, defaults to DEFAULT_MAX_RETRIES :type max_retries: int, optional :param default_headers: Headers merged into every request, defaults to None :type default_headers: Mapping[str, str] | None, optional :param default_query: Default query parameters merged into every request, defaults to None :type default_query: Mapping[str, object] | None, optional :param http_client: Custom ``httpx.Client`` instance to reuse, defaults to None :type http_client: httpx.Client | None, optional """ self.api = Runloop( bearer_token=bearer_token, base_url=base_url, timeout=timeout, max_retries=max_retries, default_headers=default_headers, default_query=default_query, http_client=http_client, ) self.devbox = DevboxOps(self.api) self.blueprint = BlueprintOps(self.api) self.snapshot = SnapshotOps(self.api) self.storage_object = StorageObjectOps(self.api)
[docs] def close(self) -> None: """Close the underlying HTTP client and release resources.""" self.api.close()
def __enter__(self) -> "RunloopSDK": """Allow ``with RunloopSDK() as runloop`` usage. :return: The active SDK instance :rtype: RunloopSDK """ return self def __exit__(self, *_exc_info: object) -> None: """Ensure the API client closes when leaving the context manager.""" self.close()