{"id":40909650,"url":"https://github.com/runloopai/api-client-python","last_synced_at":"2026-04-10T04:26:47.494Z","repository":{"id":246123598,"uuid":"817923847","full_name":"runloopai/api-client-python","owner":"runloopai","description":"Python API to access the Runloop platform","archived":false,"fork":false,"pushed_at":"2026-01-22T04:24:45.000Z","size":4515,"stargazers_count":22,"open_issues_count":6,"forks_count":2,"subscribers_count":1,"default_branch":"next","last_synced_at":"2026-01-22T15:37:24.486Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://runloopai.github.io/api-client-python/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/runloopai.png","metadata":{"files":{"readme":"README-SDK.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-06-20T18:15:28.000Z","updated_at":"2026-01-21T01:39:41.000Z","dependencies_parsed_at":"2024-06-26T01:30:52.924Z","dependency_job_id":"3d13f960-0601-4746-85c1-5f16805fda8c","html_url":"https://github.com/runloopai/api-client-python","commit_stats":null,"previous_names":["runloopai/api-client-python"],"tags_count":108,"template":false,"template_full_name":null,"purl":"pkg:github/runloopai/api-client-python","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/runloopai%2Fapi-client-python","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/runloopai%2Fapi-client-python/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/runloopai%2Fapi-client-python/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/runloopai%2Fapi-client-python/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/runloopai","download_url":"https://codeload.github.com/runloopai/api-client-python/tar.gz/refs/heads/next","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/runloopai%2Fapi-client-python/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28856911,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-28T22:56:21.783Z","status":"ssl_error","status_checked_at":"2026-01-28T22:56:00.861Z","response_time":57,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-22T03:02:19.906Z","updated_at":"2026-01-29T00:03:40.187Z","avatar_url":"https://github.com/runloopai.png","language":"Python","funding_links":[],"categories":["[Runloop](https://runloop.ai)"],"sub_categories":[],"readme":"# Runloop SDK – Python Object-Oriented Client\n\nThe `RunloopSDK` builds on top of the underlying 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.\n\n## Table of Contents\n\n- [Installation](#installation)\n- [Quickstart (synchronous)](#quickstart-synchronous)\n- [Quickstart (asynchronous)](#quickstart-asynchronous)\n- [Core Concepts](#core-concepts)\n  - [RunloopSDK](#runloopsdk)\n  - [Available Resources](#available-resources)\n  - [Devbox](#devbox)\n    - [Command Execution](#command-execution)\n    - [Execution Management](#execution-management)\n    - [Execution Results](#execution-results)\n    - [Streaming Command Output](#streaming-command-output)\n    - [File Operations](#file-operations)\n    - [Network Operations](#network-operations)\n    - [Snapshot Operations](#snapshot-operations)\n    - [Devbox Lifecycle Management](#devbox-lifecycle-management)\n    - [Context Manager Support](#context-manager-support)\n  - [Blueprint](#blueprint)\n  - [Snapshot](#snapshot)\n  - [StorageObject](#storageobject)\n    - [Storage Object Upload Helpers](#storage-object-upload-helpers)\n  - [Mounting Storage Objects to Devboxes](#mounting-storage-objects-to-devboxes)\n- [Accessing the Underlying REST Client](#accessing-the-underlying-rest-client)\n- [Error Handling](#error-handling)\n- [Advanced Configuration](#advanced-configuration)\n- [Async Usage](#async-usage)\n- [Polling Configuration](#polling-configuration)\n- [Complete API Reference](#complete-api-reference)\n- [Feedback](#feedback)\n\n## Installation\n\nThe SDK ships with the `runloop_api_client` package—no extra dependencies are required.\n\n```bash\npip install runloop_api_client\n```\n\n## Quickstart (synchronous)\n\n```python\nfrom runloop_api_client import RunloopSDK\n\nrunloop = RunloopSDK()\n\n# Create a ready-to-use devbox\nwith runloop.devbox.create(name=\"my-devbox\") as devbox:\n    result = devbox.cmd.exec(\"echo 'Hello from Runloop!'\")\n    print(result.stdout())\n\n    # Stream stdout in real time\n    devbox.cmd.exec(\n        \"ls -la\",\n        stdout=lambda line: print(\"stdout:\", line),\n    )\n\n# Blueprints\nblueprint = runloop.blueprint.create(\n    name=\"my-blueprint\",\n    dockerfile=\"FROM ubuntu:22.04\\nRUN echo 'Hello' \u003e /hello.txt\\n\",\n)\ndevbox = blueprint.create_devbox(name=\"dev-from-blueprint\")\n\n# Storage objects\nobj = runloop.storage_object.upload_from_text(\"Hello world!\", name=\"greeting.txt\")\nprint(obj.download_as_text())\n```\n\n## Quickstart (asynchronous)\n\n```python\nimport asyncio\nfrom runloop_api_client import AsyncRunloopSDK\n\n\nasync def main():\n    runloop = AsyncRunloopSDK()\n    async with await runloop.devbox.create(name=\"async-devbox\") as devbox:\n        result = await devbox.cmd.exec(\"pwd\")\n        print(await result.stdout())\n\n        def capture(line: str) -\u003e None:\n            print(\"\u003e\u003e\", line)\n\n        await devbox.cmd.exec(\"ls\", stdout=capture)\n\n\nasyncio.run(main())\n```\n\n## Core Concepts\n\n### RunloopSDK\n\nThe main SDK class that provides access to all Runloop functionality:\n\n```python\nfrom runloop_api_client import RunloopSDK\n\nrunloop = RunloopSDK(\n    bearer_token=\"your-api-key\",  # defaults to RUNLOOP_API_KEY env var\n    # ... other options\n)\n```\n\n### Available Resources\n\nThe SDK provides object-oriented interfaces for all major Runloop resources:\n\n- **`runloop.devbox`** - Devbox management (create, list, execute commands, file operations)\n- **`runloop.blueprint`** - Blueprint management (create, list, build blueprints)\n- **`runloop.snapshot`** - Snapshot management (list disk snapshots)\n- **`runloop.storage_object`** - Storage object management (upload, download, list objects)\n- **`runloop.api`** - Direct access to the underlying REST API client\n\n### Devbox\n\nObject-oriented interface for working with devboxes. Created via `runloop.devbox.create()`, `runloop.devbox.create_from_blueprint_id()`, `runloop.devbox.create_from_blueprint_name()`, `runloop.devbox.create_from_snapshot()`, or `runloop.devbox.from_id()`:\n\n```python\n# Create a new devbox\ndevbox = runloop.devbox.create(name=\"my-devbox\")\n\n# Create a devbox from a blueprint ID\ndevbox_from_blueprint = runloop.devbox.create_from_blueprint_id(\n    blueprint_id=\"bpt_123\",\n    name=\"my-devbox-from-blueprint\",\n)\n\n# Create a devbox from a blueprint name\ndevbox_from_name = runloop.devbox.create_from_blueprint_name(\n    blueprint_name=\"my-blueprint-name\",\n    name=\"my-devbox-from-blueprint\",\n)\n\n# Create a devbox from a snapshot\ndevbox_from_snapshot = runloop.devbox.create_from_snapshot(\n    snapshot_id=\"snp_123\",\n    name=\"my-devbox-from-snapshot\",\n)\n\n# Or get an existing one (waits for it to be running)\nexisting_devbox = runloop.devbox.from_id(devbox_id=\"dbx_123\")\n\n# List all devboxes\ndevboxes = runloop.devbox.list(limit=10)\n\n# Get devbox information\ninfo = devbox.get_info()\nprint(f\"Devbox {info.name} is {info.status}\")\n```\n\n#### Command Execution\n\nExecute commands synchronously or asynchronously:\n\n```python\n# Synchronous command execution (waits for completion)\nresult = devbox.cmd.exec(\"ls -la\")\nprint(\"Output:\", result.stdout())\nprint(\"Exit code:\", result.exit_code)\nprint(\"Success:\", result.success)\n\n# Asynchronous command execution (returns immediately)\nexecution = devbox.cmd.exec_async(\"npm run dev\")\n\n# Check execution status\nstate = execution.get_state()\nprint(\"Status:\", state.status)\n\n# Wait for completion and get result\nresult = execution.result()\nprint(\"Final output:\", result.stdout())\n\n# Kill the process\nexecution.kill()\n```\n\n#### Execution Management\n\nThe `Execution` object provides fine-grained control over asynchronous command execution:\n\n```python\n# Start a long-running process\nexecution = devbox.cmd.exec_async(\"python train_model.py\")\n\n# Get the execution ID\nprint(\"Execution ID:\", execution.execution_id)\nprint(\"Devbox ID:\", execution.devbox_id)\n\n# Poll for current state\nstate = execution.get_state()\nprint(\"Status:\", state.status)  # \"running\", \"completed\", etc.\nprint(\"Exit code:\", state.exit_status)  # only set when execution has completed\n\n# Wait for completion and get results\nresult = execution.result()\nprint(\"Exit code:\", result.exit_code)\nprint(\"Output:\", result.stdout())\nprint(\"Errors:\", result.stderr())\n\n# Or kill the process early\nexecution.kill()\n```\n\n**Key methods:**\n\n- `execution.get_state()` - Get current execution state (status, exit_code, etc.)\n- `execution.result()` - Wait for completion and return `ExecutionResult`\n- `execution.kill()` - Terminate the running process\n- `execution.execution_id` - Get the execution ID (property)\n- `execution.devbox_id` - Get the devbox ID (property)\n\n#### Execution Results\n\nThe `ExecutionResult` object contains the output and exit status of a completed command:\n\n```python\n# From synchronous execution\nresult = devbox.cmd.exec(\"ls -la /tmp\")\n\n# Or from asynchronous execution\nexecution = devbox.cmd.exec_async(\"echo 'test'\")\nresult = execution.result()\n\n# Access execution results\nprint(\"Exit code:\", result.exit_code)\nprint(\"Success:\", result.success)  # True if exit code is 0\nprint(\"Failed:\", result.failed)  # True if exit code is non-zero\n\n# Get output streams\nstdout = result.stdout()\nstderr = result.stderr()\nprint(\"Standard output:\", stdout)\nprint(\"Standard error:\", stderr)\n\n# Access raw result data\nraw_result = result.raw\nprint(\"Raw result:\", raw_result)\n```\n\n**Key methods and properties:**\n\n- `result.exit_code` - The process exit code (property)\n- `result.success` - Boolean indicating success (exit code 0) (property)\n- `result.failed` - Boolean indicating failure (non-zero exit code) (property)\n- `result.stdout()` - Get standard output as string\n- `result.stderr()` - Get standard error as string\n- `result.raw` - Get the raw result data (property)\n\n#### Streaming Command Output\n\n\u003e **Callback requirement:** All callbacks (`stdout`, `stderr`, `output`) must be synchronous functions. Even when using `AsyncDevbox`, callbacks cannot be async. Use thread-safe queues or other coordination primitives if you need to bridge into async code.\n\nPass callbacks into `cmd.exec` / `cmd.exec_async` to process logs in real time:\n\n```python\ndef handle_output(line: str) -\u003e None:\n    print(\"LOG:\", line)\n\n\nresult = devbox.cmd.exec(\n    \"python train.py\",\n    stdout=handle_output,\n    stderr=lambda line: print(\"ERR:\", line),\n    output=lambda line: print(\"ANY:\", line),\n)\nprint(\"exit code:\", result.exit_code)\n```\n\nAsync example (note that the callback itself is still synchronous):\n\n```python\ndef capture(line: str) -\u003e None:\n    # Callbacks must be synchronous\n    # Use thread-safe data structures if needed\n    log_queue.put_nowait(line)\n\n\nawait devbox.cmd.exec(\n    \"tail -f /var/log/app.log\",\n    stdout=capture,\n)\n```\n\n#### File Operations\n\n```python\n# Write files\ndevbox.file.write(\n    path=\"/home/user/app.js\",\n    contents='console.log(\"Hello from devbox!\");',\n)\n\n# Read files\ncontent = devbox.file.read(path=\"/home/user/app.js\")\nprint(content)\n\n# Upload files\nfrom pathlib import Path\n\ndevbox.file.upload(\n    path=\"/home/user/upload.txt\",\n    file=Path(\"local_file.txt\"),\n)\n\n# Download files\ndata = devbox.file.download(path=\"/home/user/download.txt\")\nwith open(\"local_download.txt\", \"wb\") as f:\n    f.write(data)\n```\n\n#### Network Operations\n\n```python\n# Create SSH key for remote access\nssh_key = devbox.net.create_ssh_key()\nprint(\"SSH URL:\", ssh_key.url)\n\n# Create tunnel to expose port\ntunnel = devbox.net.create_tunnel(port=8080)\nprint(\"Public URL:\", tunnel.url)\n\n# Remove tunnel when done\ndevbox.net.remove_tunnel(port=8080)\n```\n\n#### Snapshot Operations\n\n```python\n# Create a snapshot (waits for completion)\nsnapshot = devbox.snapshot_disk(\n    name=\"my-snapshot\",\n    commit_message=\"Added new features\",\n)\n\n# Create a snapshot asynchronously (returns immediately)\nsnapshot = devbox.snapshot_disk_async(\n    name=\"my-snapshot\",\n    commit_message=\"Added new features\",\n)\n# Wait for it to complete later\nsnapshot.await_completed()\n\n# Create new devbox from snapshot\nnew_devbox = snapshot.create_devbox(name=\"devbox-from-snapshot\")\n```\n\n#### Devbox Lifecycle Management\n\n```python\n# Suspend devbox (pause without losing state)\ndevbox.suspend()\n\n# Resume suspended devbox\ndevbox.resume()\n\n# Keep devbox alive (extend timeout)\ndevbox.keep_alive()\n\n# Wait for devbox to reach running state\ndevbox.await_running()\n\n# Wait for devbox to be suspended\ndevbox.await_suspended()\n\n# Shutdown devbox\ndevbox.shutdown()\n```\n\n\u003e **State-waiting behavior:** In the synchronous SDK, `runloop.devbox.from_id()` and `devbox.resume()` block until the devbox reaches the `running` state, and `devbox.suspend()` blocks until the devbox is `suspended`. The async counterparts return immediately; call `await_running()` / `await_suspended()` explicitly if you need to wait for the state transition.\n\n#### Context Manager Support\n\nDevboxes support context managers for automatic cleanup:\n\n```python\n# Synchronous\nwith runloop.devbox.create(name=\"temp-devbox\") as devbox:\n    result = devbox.cmd.exec(\"echo 'Hello'\")\n    print(result.stdout())\n# devbox is automatically shutdown when exiting the context\n\n# Asynchronous\nasync with await runloop.devbox.create(name=\"temp-devbox\") as devbox:\n    result = await devbox.cmd.exec(\"echo 'Hello'\")\n    print(await result.stdout())\n# devbox is automatically shutdown when exiting the context\n```\n\n**Key methods:**\n\n- `devbox.get_info()` - Get devbox details and status\n- `devbox.cmd.exec()` - Execute commands synchronously\n- `devbox.cmd.exec_async()` - Execute commands asynchronously\n- `devbox.file.read()` - Read file contents\n- `devbox.file.write()` - Write file contents\n- `devbox.file.upload()` - Upload files\n- `devbox.file.download()` - Download files\n- `devbox.net.create_ssh_key()` - Create SSH key for remote access\n- `devbox.net.create_tunnel()` - Create network tunnel\n- `devbox.net.remove_tunnel()` - Remove network tunnel\n- `devbox.snapshot_disk()` - Create disk snapshot (waits for completion)\n- `devbox.snapshot_disk_async()` - Create disk snapshot (async)\n- `devbox.suspend()` - Suspend devbox\n- `devbox.resume()` - Resume suspended devbox\n- `devbox.keep_alive()` - Extend devbox timeout\n- `devbox.await_running()` - Wait for devbox to be running\n- `devbox.await_suspended()` - Wait for devbox to be suspended\n- `devbox.shutdown()` - Shutdown the devbox\n\n### Blueprint\n\nObject-oriented interface for working with blueprints. Created via `runloop.blueprint.create()` or `runloop.blueprint.from_id()`:\n\n```python\n# Create a new blueprint\nblueprint = runloop.blueprint.create(\n    name=\"my-blueprint\",\n    dockerfile=\"FROM ubuntu:22.04\\nRUN apt-get update \u0026\u0026 apt-get install -y python3\\n\",\n    system_setup_commands=[\"pip install numpy pandas\"],\n)\n\n# Or create a blueprint with a Docker build context from a local directory\nfrom pathlib import Path\nfrom runloop_api_client.lib.context_loader import build_docker_context_tar\n\ncontext_root = Path(\"./my-app\")\ntar_bytes = build_docker_context_tar(context_root)\n\nbuild_ctx_obj = runloop.storage_object.upload_from_bytes(\n    data=tar_bytes,\n    name=\"my-app-context.tar.gz\",\n    content_type=\"tgz\",\n)\n\nblueprint_with_context = runloop.blueprint.create(\n    name=\"my-blueprint-with-context\",\n    dockerfile=\"\"\"\\\nFROM node:22\nWORKDIR /usr/src/app\n\n# copy using the build context from the object\nCOPY package.json package.json\nCOPY src src\n\nRUN npm install --only=production\nCMD [\"node\", \"src/app.js\"]\n\"\"\",\n    # Build context\n    build_context=build_ctx_obj.as_build_context(),\n)\n\n# Or get an existing one\nblueprint = runloop.blueprint.from_id(blueprint_id=\"bpt_123\")\n\n# List all blueprints\nblueprints = runloop.blueprint.list()\n\n# Get blueprint details and build logs\ninfo = blueprint.get_info()\nlogs = blueprint.logs()\n\n# Create a devbox from this blueprint\ndevbox = blueprint.create_devbox(name=\"devbox-from-blueprint\")\n\n# Delete the blueprint when done\nblueprint.delete()\n```\n\n**Key methods:**\n\n- `blueprint.get_info()` - Get blueprint details\n- `blueprint.logs()` - Get build logs for the blueprint\n- `blueprint.delete()` - Delete the blueprint\n- `blueprint.create_devbox()` - Create a devbox from this blueprint\n\n### Snapshot\n\nObject-oriented interface for working with disk snapshots. Created via `runloop.snapshot.from_id()`:\n\n```python\n# Get an existing snapshot\nsnapshot = runloop.snapshot.from_id(snapshot_id=\"snp_123\")\n\n# List all snapshots\nsnapshots = runloop.snapshot.list()\n\n# List snapshots for a specific devbox\ndevbox_snapshots = runloop.snapshot.list(devbox_id=\"dbx_123\")\n\n# Get snapshot details and check status\ninfo = snapshot.get_info()\nprint(f\"Snapshot status: {info.status}\")\n\n# Update snapshot metadata\nsnapshot.update(\n    name=\"updated-snapshot-name\",\n    metadata={\"version\": \"v2.0\"},\n)\n\n# Wait for async snapshot to complete\nsnapshot.await_completed()\n\n# Create a devbox from this snapshot\ndevbox = snapshot.create_devbox(name=\"devbox-from-snapshot\")\n\n# Delete the snapshot when done\nsnapshot.delete()\n```\n\n**Key methods:**\n\n- `snapshot.get_info()` - Get snapshot details and status\n- `snapshot.update()` - Update snapshot name and metadata\n- `snapshot.delete()` - Delete the snapshot\n- `snapshot.await_completed()` - Wait for snapshot completion\n- `snapshot.create_devbox()` - Create a devbox from this snapshot\n\n### StorageObject\n\nObject-oriented interface for working with storage objects. Created via `runloop.storage_object.create()` or `runloop.storage_object.from_id()`:\n\n```python\n# Create a new storage object\nstorage_object = runloop.storage_object.create(\n    name=\"my-file.txt\",\n    content_type=\"text\",\n    metadata={\"project\": \"demo\"},\n)\n\n# Upload content to the object\nstorage_object.upload_content(\"Hello, World!\")\nstorage_object.complete()\n\n# Upload from file\nfrom pathlib import Path\n\nuploaded = runloop.storage_object.upload_from_file(\n    Path(\"/path/to/file.txt\"),\n    name=\"my-file.txt\",\n)\n\n# Upload text content directly\nuploaded = runloop.storage_object.upload_from_text(\n    \"Hello, World!\",\n    name=\"my-text.txt\",\n    metadata={\"source\": \"text\"},\n)\n\n# Upload from bytes\nuploaded = runloop.storage_object.upload_from_bytes(\n    b\"binary content\",\n    name=\"my-file.bin\",\n    content_type=\"binary\",\n)\n\n# Get object details and download\ninfo = storage_object.refresh()\ndownload_url = storage_object.get_download_url(duration_seconds=3600)\n\n# Download content\ntext_content = storage_object.download_as_text()\nbinary_content = storage_object.download_as_bytes()\n\n# List all storage objects\nobjects = runloop.storage_object.list()\n\n# Delete when done\nstorage_object.delete()\n```\n\n#### Storage Object Upload Helpers\n\nThe storage helpers manage the multi-step upload flow (create → PUT to presigned URL → complete):\n\n```python\nfrom pathlib import Path\n\n# Upload local file with content-type detection\nobj = runloop.storage_object.upload_from_file(file_path=Path(\"./report.csv\"))\n\n# Manual control\nobj = runloop.storage_object.create(\n    name=\"data.bin\",\n    content_type=\"binary\",\n)\nobj.upload_content(b\"\\xde\\xad\\xbe\\xef\")\nobj.complete()\n```\n\n**Key methods:**\n\n- `storage_object.refresh()` - Get updated object details\n- `storage_object.upload_content()` - Upload content to the object\n- `storage_object.complete()` - Mark upload as complete\n- `storage_object.get_download_url()` - Get presigned download URL\n- `storage_object.download_as_text()` - Download content as text\n- `storage_object.download_as_bytes()` - Download content as bytes\n- `storage_object.delete()` - Delete the object\n\n**Static upload methods:**\n\n- `runloop.storage_object.upload_from_file()` - Upload from filesystem\n- `runloop.storage_object.upload_from_text()` - Upload text content directly\n- `runloop.storage_object.upload_from_bytes()` - Upload from bytes\n\n### Mounting Storage Objects to Devboxes\n\nYou can mount storage objects to devboxes to access their contents:\n\n```python\n# Create a storage object first\nstorage_object = runloop.storage_object.upload_from_text(\n    \"Hello, World!\",\n    name=\"my-data.txt\",\n)\n\n# Create a devbox and mount the storage object\ndevbox = runloop.devbox.create(\n    name=\"my-devbox\",\n    mounts=[\n        {\n            \"type\": \"object_mount\",\n            \"object_id\": storage_object.id,\n            \"object_path\": \"/home/user/data.txt\",\n        },\n    ],\n)\n\n# The storage object is now accessible at /home/user/data.txt in the devbox\nresult = devbox.cmd.exec(\"cat /home/user/data.txt\")\nprint(result.stdout())  # \"Hello, World!\"\n\n# Mount archived objects (tar, tgz, gzip) - they get extracted to a directory\narchive_object = runloop.storage_object.upload_from_file(\n    Path(\"./project.tar.gz\"),\n    name=\"project.tar.gz\",\n)\n\ndevbox_with_archive = runloop.devbox.create(\n    name=\"archive-devbox\",\n    mounts=[\n        {\n            \"type\": \"object_mount\",\n            \"object_id\": archive_object.id,\n            \"object_path\": \"/home/user/project\",  # Archive gets extracted here\n        },\n    ],\n)\n\n# Access extracted archive contents\nresult = devbox_with_archive.cmd.exec(\"ls -la /home/user/project/\")\nprint(result.stdout())\n```\n\n## Accessing the Underlying REST Client\n\nThe SDK always exposes the underlying client through the `.api` attribute:\n\n```python\nrunloop = RunloopSDK()\nraw_devbox = runloop.api.devboxes.create()\n```\n\nThis makes it straightforward to mix high-level helpers with low-level calls whenever you need advanced control.\n\n## Error Handling\n\nThe SDK provides comprehensive error handling with typed exceptions:\n\n```python\nfrom runloop_api_client import RunloopSDK\nimport runloop_api_client\n\nrunloop = RunloopSDK()\n\ntry:\n    devbox = runloop.devbox.create(name=\"example-devbox\")\n    result = devbox.cmd.exec(\"invalid-command\")\nexcept runloop_api_client.APIConnectionError as e:\n    print(\"The server could not be reached\")\n    print(e.__cause__)  # an underlying Exception, likely raised within httpx.\nexcept runloop_api_client.RateLimitError as e:\n    print(\"A 429 status code was received; we should back off a bit.\")\nexcept runloop_api_client.APIStatusError as e:\n    print(\"Another non-200-range status code was received\")\n    print(e.status_code)\n    print(e.response)\n```\n\nError codes are as follows:\n\n| Status Code | Error Type                 |\n| ----------- | -------------------------- |\n| 400         | `BadRequestError`          |\n| 401         | `AuthenticationError`      |\n| 403         | `PermissionDeniedError`    |\n| 404         | `NotFoundError`            |\n| 422         | `UnprocessableEntityError` |\n| 429         | `RateLimitError`           |\n| \u003e=500       | `InternalServerError`      |\n| N/A         | `APIConnectionError`       |\n\n## Advanced Configuration\n\n```python\nimport httpx\nfrom runloop_api_client import RunloopSDK, DefaultHttpxClient\n\nrunloop = RunloopSDK(\n    bearer_token=\"your-api-key\",  # defaults to RUNLOOP_API_KEY env var\n    base_url=\"https://api.runloop.ai\",  # or use RUNLOOP_BASE_URL env var\n    timeout=60.0,  # 60 second timeout (default is 30)\n    max_retries=3,  # Retry failed requests (default is 5)\n    default_headers={\n        \"X-Custom-Header\": \"value\",\n    },\n    # Custom HTTP client with proxy\n    http_client=DefaultHttpxClient(\n        proxy=\"http://my.test.proxy.example.com\",\n        transport=httpx.HTTPTransport(local_address=\"0.0.0.0\"),\n    ),\n)\n```\n\n## Async Usage\n\nThe async SDK has the same interface as the synchronous version, but all I/O operations are async:\n\n```python\nimport asyncio\nfrom runloop_api_client import AsyncRunloopSDK\n\n\nasync def main():\n    runloop = AsyncRunloopSDK()\n\n    # All the same operations, but with await\n    async with await runloop.devbox.create(name=\"async-devbox\") as devbox:\n        result = await devbox.cmd.exec(\"pwd\")\n        print(await result.stdout())\n\n        # Streaming (note: callbacks must be synchronous)\n        def capture(line: str) -\u003e None:\n            print(\"\u003e\u003e\", line)\n\n        await devbox.cmd.exec(\"ls\", stdout=capture)\n\n        # Async file operations\n        await devbox.file.write(path=\"/tmp/test.txt\", contents=\"Hello\")\n        content = await devbox.file.read(path=\"/tmp/test.txt\")\n\n        # Async network operations\n        tunnel = await devbox.net.create_tunnel(port=8080)\n        print(\"Tunnel URL:\", tunnel.url)\n\n\nasyncio.run(main())\n```\n\n## Polling Configuration\n\nMany operations that wait for state changes accept a `polling_config` parameter:\n\n```python\nfrom runloop_api_client.lib.polling import PollingConfig\n\n# Create devbox with custom polling\ndevbox = runloop.devbox.create(\n    name=\"my-devbox\",\n    polling_config=PollingConfig(\n        timeout_seconds=300.0,  # Wait up to 5 minutes\n        interval_seconds=2.0,  # Poll every 2 seconds\n    ),\n)\n\n# Wait for snapshot completion with custom polling\nsnapshot.await_completed(\n    polling_config=PollingConfig(\n        timeout_seconds=600.0,  # Wait up to 10 minutes\n        interval_seconds=5.0,  # Poll every 5 seconds\n    ),\n)\n```\n\n## Complete API Reference\n\nFor the full REST API documentation and all available parameters, see:\n\n- **[api.md](api.md)** - Complete REST API documentation\n- **[README.md](README.md)** - Advanced topics (retries, timeouts, error handling, pagination)\n\n## Feedback\n\nThe 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.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frunloopai%2Fapi-client-python","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frunloopai%2Fapi-client-python","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frunloopai%2Fapi-client-python/lists"}