diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8c652335f..0e5b32a4c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -32,6 +32,9 @@ jobs:
- name: Run lints
run: ./scripts/lint
+ - name: Verify generated examples docs
+ run: uv run python scripts/generate_examples_md.py --check
+
build:
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
timeout-minutes: 10
diff --git a/.github/workflows/smoketests.yml b/.github/workflows/smoketests.yml
index ba29508b5..f6846f473 100644
--- a/.github/workflows/smoketests.yml
+++ b/.github/workflows/smoketests.yml
@@ -45,6 +45,10 @@ jobs:
fi
echo "DEBUG=false" >> $GITHUB_ENV
echo "PYTHONPATH=${{ github.workspace }}/src" >> $GITHUB_ENV
+ echo "RUN_EXAMPLE_LIVE_TESTS=1" >> $GITHUB_ENV
+
+ - name: Verify generated example artifacts
+ run: uv run python scripts/generate_examples_md.py --check
- name: Run smoke tests (pytest via uv)
env:
diff --git a/EXAMPLES.md b/EXAMPLES.md
new file mode 100644
index 000000000..9377b08bb
--- /dev/null
+++ b/EXAMPLES.md
@@ -0,0 +1,72 @@
+# Examples
+
+> This file is auto-generated from metadata in `examples/*.py`.
+> Do not edit this file manually. Run `uv run python scripts/generate_examples_md.py` instead.
+
+Runnable examples live in [`examples/`](./examples).
+
+## Table of Contents
+
+- [Devbox From Blueprint (Run Command, Shutdown)](#devbox-from-blueprint-lifecycle)
+- [MCP Hub + Claude Code + GitHub](#mcp-github-tools)
+
+
+## Devbox From Blueprint (Run Command, Shutdown)
+
+**Use case:** Create a devbox from a blueprint, run a command, validate output, and cleanly tear everything down.
+
+**Tags:** `devbox`, `blueprint`, `commands`, `cleanup`
+
+### Workflow
+- Create a blueprint
+- Create a devbox from the blueprint
+- Execute a command in the devbox
+- Validate exit code and stdout
+- Shutdown devbox and delete blueprint
+
+### Prerequisites
+- `RUNLOOP_API_KEY`
+
+### Run
+```sh
+uv run python -m examples.devbox_from_blueprint_lifecycle
+```
+
+### Test
+```sh
+uv run pytest -m smoketest tests/smoketests/examples/
+```
+
+**Source:** [`examples/devbox_from_blueprint_lifecycle.py`](./examples/devbox_from_blueprint_lifecycle.py)
+
+
+## MCP Hub + Claude Code + GitHub
+
+**Use case:** Connect Claude Code running in a devbox to GitHub tools through MCP Hub without exposing raw GitHub credentials to the devbox.
+
+**Tags:** `mcp`, `devbox`, `github`, `commands`, `cleanup`
+
+### Workflow
+- Create an MCP config for GitHub
+- Store GitHub token as a Runloop secret
+- Launch a devbox with MCP Hub wiring
+- Install Claude Code and register MCP endpoint
+- Run a Claude prompt through MCP tools
+- Shutdown devbox and clean up cloud resources
+
+### Prerequisites
+- `RUNLOOP_API_KEY`
+- `GITHUB_TOKEN (GitHub PAT with repo scope)`
+- `ANTHROPIC_API_KEY`
+
+### Run
+```sh
+GITHUB_TOKEN=ghp_xxx ANTHROPIC_API_KEY=sk-ant-xxx uv run python -m examples.mcp_github_tools
+```
+
+### Test
+```sh
+uv run pytest -m smoketest tests/smoketests/examples/
+```
+
+**Source:** [`examples/mcp_github_tools.py`](./examples/mcp_github_tools.py)
diff --git a/README.md b/README.md
index 14e808a10..1907bf12b 100644
--- a/README.md
+++ b/README.md
@@ -88,6 +88,23 @@ asyncio.run(main())
Functionality between the synchronous and asynchronous clients is otherwise identical.
+## Examples
+
+Workflow-oriented runnable examples are documented in [`EXAMPLES.md`](./EXAMPLES.md).
+
+`EXAMPLES.md` is generated from metadata in `examples/*.py` and should not be edited manually.
+Regenerate it with:
+
+```sh
+uv run python scripts/generate_examples_md.py
+```
+
+## Agent Guidance
+
+Detailed agent-specific instructions for developing using this package live in [`llms.txt`](./llms.txt). Consolidated recipes for frequent tasks are in [`EXAMPLES.md`](./EXAMPLES.md).
+
+After completing any modifications to this project, ensure `llms.txt` and `README.md` are kept in sync.
+
### With aiohttp
By default, the async client uses `httpx` for HTTP requests. However, for improved concurrency performance you may also use `aiohttp` as the HTTP backend.
diff --git a/examples/.keep b/examples/.keep
deleted file mode 100644
index d8c73e937..000000000
--- a/examples/.keep
+++ /dev/null
@@ -1,4 +0,0 @@
-File generated from our OpenAPI spec by Stainless.
-
-This directory can be used to store example files demonstrating usage of this SDK.
-It is ignored by Stainless code generation and its content (other than this keep file) won't be touched.
\ No newline at end of file
diff --git a/examples/__init__.py b/examples/__init__.py
new file mode 100644
index 000000000..9f6967a61
--- /dev/null
+++ b/examples/__init__.py
@@ -0,0 +1 @@
+# Examples package for Runloop Python SDK
diff --git a/examples/_harness.py b/examples/_harness.py
new file mode 100644
index 000000000..dec6dfdf4
--- /dev/null
+++ b/examples/_harness.py
@@ -0,0 +1,172 @@
+from __future__ import annotations
+
+import sys
+import json
+import time
+import asyncio
+from typing import Any, TypeVar, Callable, Awaitable
+from dataclasses import asdict
+
+from .example_types import (
+ ExampleCheck,
+ RecipeOutput,
+ ExampleResult,
+ RecipeContext,
+ ExampleCleanupStatus,
+ ExampleCleanupFailure,
+)
+
+T = TypeVar("T")
+
+
+def unique_name(prefix: str) -> str:
+ """Generate a unique name with timestamp and random suffix."""
+ return f"{prefix}-{int(time.time())}-{hex(int(time.time() * 1000) % 0xFFFFFF)[2:]}"
+
+
+class _CleanupTracker:
+ """Tracks cleanup actions and executes them in LIFO order."""
+
+ def __init__(self, status: ExampleCleanupStatus) -> None:
+ self._status = status
+ self._actions: list[tuple[str, Callable[[], Any]]] = []
+
+ def add(self, resource: str, action: Callable[[], Any]) -> None:
+ """Register a cleanup action for a resource."""
+ self._actions.append((resource, action))
+
+ async def run(self) -> None:
+ """Execute all cleanup actions in reverse order."""
+ while self._actions:
+ resource, action = self._actions.pop()
+ self._status.attempted.append(resource)
+ try:
+ result = action()
+ if asyncio.iscoroutine(result):
+ await result
+ self._status.succeeded.append(resource)
+ except Exception as e:
+ self._status.failed.append(ExampleCleanupFailure(resource, str(e)))
+
+ if self._status.attempted:
+ if not self._status.failed:
+ print("Cleanup completed.") # noqa: T201
+ else:
+ print("Cleanup finished with errors.") # noqa: T201
+
+
+def _should_fail_process(result: ExampleResult) -> bool:
+ """Determine if the process should exit with failure."""
+ has_failed_checks = any(not check.passed for check in result.checks)
+ return result.skipped or has_failed_checks or len(result.cleanup_status.failed) > 0
+
+
+def _run_recipe_impl(
+ recipe_call: Callable[[], RecipeOutput | Awaitable[RecipeOutput]],
+ cleanup: _CleanupTracker,
+ cleanup_status: ExampleCleanupStatus,
+) -> ExampleResult:
+ """Shared implementation for running recipes with cleanup."""
+
+ async def _run_async() -> RecipeOutput:
+ try:
+ result = recipe_call()
+ if asyncio.iscoroutine(result):
+ output: RecipeOutput = await result
+ return output
+ return result # type: ignore[return-value]
+ finally:
+ await cleanup.run()
+
+ loop = asyncio.new_event_loop()
+ try:
+ output = loop.run_until_complete(_run_async())
+ return ExampleResult(
+ resources_created=output.resources_created,
+ checks=output.checks,
+ cleanup_status=cleanup_status,
+ )
+ finally:
+ loop.close()
+
+
+def wrap_recipe(
+ recipe: Callable[[RecipeContext], RecipeOutput] | Callable[[RecipeContext], Awaitable[RecipeOutput]],
+ validate_env: Callable[[], tuple[bool, list[ExampleCheck]]] | None = None,
+) -> Callable[[], ExampleResult]:
+ """Wrap a recipe function with cleanup tracking and result handling.
+
+ Args:
+ recipe: The recipe function to wrap. Can be sync or async.
+ validate_env: Optional function to validate environment before running.
+ Returns (skip, checks) tuple.
+
+ Returns:
+ A callable that runs the recipe and returns ExampleResult.
+ """
+
+ def run() -> ExampleResult:
+ cleanup_status = ExampleCleanupStatus()
+ cleanup = _CleanupTracker(cleanup_status)
+
+ if validate_env is not None:
+ skip, checks = validate_env()
+ if skip:
+ return ExampleResult(
+ resources_created=[],
+ checks=checks,
+ cleanup_status=cleanup_status,
+ skipped=True,
+ )
+
+ ctx = RecipeContext(cleanup=cleanup)
+ return _run_recipe_impl(lambda: recipe(ctx), cleanup, cleanup_status)
+
+ return run
+
+
+def wrap_recipe_with_options(
+ recipe: Callable[[RecipeContext, T], RecipeOutput] | Callable[[RecipeContext, T], Awaitable[RecipeOutput]],
+ validate_env: Callable[[T], tuple[bool, list[ExampleCheck]]] | None = None,
+) -> Callable[[T], ExampleResult]:
+ """Wrap a recipe function that takes options with cleanup tracking.
+
+ Args:
+ recipe: The recipe function to wrap. Can be sync or async. Takes options parameter.
+ validate_env: Optional function to validate environment before running.
+ Takes options and returns (skip, checks) tuple.
+
+ Returns:
+ A callable that runs the recipe with options and returns ExampleResult.
+ """
+
+ def run(options: T) -> ExampleResult:
+ cleanup_status = ExampleCleanupStatus()
+ cleanup = _CleanupTracker(cleanup_status)
+
+ if validate_env is not None:
+ skip, checks = validate_env(options)
+ if skip:
+ return ExampleResult(
+ resources_created=[],
+ checks=checks,
+ cleanup_status=cleanup_status,
+ skipped=True,
+ )
+
+ ctx = RecipeContext(cleanup=cleanup)
+ return _run_recipe_impl(lambda: recipe(ctx, options), cleanup, cleanup_status)
+
+ return run
+
+
+def run_as_cli(run: Callable[[], ExampleResult]) -> None:
+ """Run an example and exit with appropriate status code."""
+ try:
+ result = run()
+ print(json.dumps(asdict(result), indent=2)) # noqa: T201
+ if _should_fail_process(result):
+ sys.exit(1)
+ except Exception as e:
+ print(f"Error: {e}") # noqa: T201
+ sys.exit(1)
diff --git a/examples/devbox_from_blueprint_lifecycle.py b/examples/devbox_from_blueprint_lifecycle.py
new file mode 100644
index 000000000..9dc98c4ed
--- /dev/null
+++ b/examples/devbox_from_blueprint_lifecycle.py
@@ -0,0 +1,82 @@
+#!/usr/bin/env -S uv run python
+"""
+---
+title: Devbox From Blueprint (Run Command, Shutdown)
+slug: devbox-from-blueprint-lifecycle
+use_case: Create a devbox from a blueprint, run a command, validate output, and cleanly tear everything down.
+workflow:
+ - Create a blueprint
+ - Create a devbox from the blueprint
+ - Execute a command in the devbox
+ - Validate exit code and stdout
+ - Shutdown devbox and delete blueprint
+tags:
+ - devbox
+ - blueprint
+ - commands
+ - cleanup
+prerequisites:
+ - RUNLOOP_API_KEY
+run: uv run python -m examples.devbox_from_blueprint_lifecycle
+test: uv run pytest -m smoketest tests/smoketests/examples/
+---
+"""
+
+from __future__ import annotations
+
+from runloop_api_client import RunloopSDK
+from runloop_api_client.lib.polling import PollingConfig
+
+from ._harness import run_as_cli, unique_name, wrap_recipe
+from .example_types import ExampleCheck, RecipeOutput, RecipeContext
+
+BLUEPRINT_POLL_TIMEOUT_S = 10 * 60
+
+
+def recipe(ctx: RecipeContext) -> RecipeOutput:
+ """Create a devbox from a blueprint, run a command, and clean up."""
+ cleanup = ctx.cleanup
+
+ sdk = RunloopSDK()
+
+ blueprint = sdk.blueprint.create(
+ name=unique_name("example-blueprint"),
+ dockerfile='FROM ubuntu:22.04\nRUN echo "Hello from your blueprint"',
+ polling_config=PollingConfig(timeout_seconds=BLUEPRINT_POLL_TIMEOUT_S),
+ )
+ cleanup.add(f"blueprint:{blueprint.id}", blueprint.delete)
+
+ devbox = blueprint.create_devbox(
+ name=unique_name("example-devbox"),
+ launch_parameters={
+ "resource_size_request": "X_SMALL",
+ "keep_alive_time_seconds": 60 * 5,
+ },
+ )
+ cleanup.add(f"devbox:{devbox.id}", devbox.shutdown)
+
+ result = devbox.cmd.exec('echo "Hello from your devbox"')
+ stdout = result.stdout()
+
+ return RecipeOutput(
+ resources_created=[f"blueprint:{blueprint.id}", f"devbox:{devbox.id}"],
+ checks=[
+ ExampleCheck(
+ name="command exits successfully",
+ passed=result.exit_code == 0,
+ details=f"exitCode={result.exit_code}",
+ ),
+ ExampleCheck(
+ name="command output contains expected text",
+ passed="Hello from your devbox" in stdout,
+ details=stdout.strip(),
+ ),
+ ],
+ )
+
+
+run_devbox_from_blueprint_lifecycle_example = wrap_recipe(recipe)
+
+
+if __name__ == "__main__":
+ run_as_cli(run_devbox_from_blueprint_lifecycle_example)
diff --git a/examples/example_types.py b/examples/example_types.py
new file mode 100644
index 000000000..46e8931c8
--- /dev/null
+++ b/examples/example_types.py
@@ -0,0 +1,64 @@
+from __future__ import annotations
+
+from typing import Any, Union, Callable, Protocol, Awaitable
+from dataclasses import field, dataclass
+
+
+@dataclass
+class ExampleCheck:
+ """Result of a single validation check in an example."""
+
+ name: str
+ passed: bool
+ details: str | None = None
+
+
+@dataclass
+class ExampleCleanupFailure:
+ """Record of a cleanup action that failed."""
+
+ resource: str
+ reason: str
+
+
+@dataclass
+class ExampleCleanupStatus:
+ """Tracks cleanup operations during example execution."""
+
+ attempted: list[str] = field(default_factory=lambda: [])
+ succeeded: list[str] = field(default_factory=lambda: [])
+ failed: list[ExampleCleanupFailure] = field(default_factory=lambda: [])
+
+
+@dataclass
+class ExampleResult:
+ """Full result of running an example, including checks and cleanup status."""
+
+ resources_created: list[str]
+ checks: list[ExampleCheck]
+ cleanup_status: ExampleCleanupStatus
+ skipped: bool = False
+
+
+@dataclass
+class RecipeOutput:
+ """Output from a recipe function before cleanup runs."""
+
+ resources_created: list[str]
+ checks: list[ExampleCheck]
+
+
+CleanupAction = Callable[[], Union[None, Awaitable[None]]]
+
+
+class CleanupTracker(Protocol):
+ """Protocol for tracking cleanup actions."""
+
+ def add(self, resource: str, action: CleanupAction) -> None: ...
+
+
+@dataclass
+class RecipeContext:
+ """Context passed to recipe functions."""
+
+ cleanup: Any # CleanupTracker, but using Any to avoid circular typing issues
diff --git a/examples/mcp_github_tools.py b/examples/mcp_github_tools.py
new file mode 100644
index 000000000..b2850c7af
--- /dev/null
+++ b/examples/mcp_github_tools.py
@@ -0,0 +1,184 @@
+#!/usr/bin/env -S uv run python
+"""
+---
+title: MCP Hub + Claude Code + GitHub
+slug: mcp-github-tools
+use_case: Connect Claude Code running in a devbox to GitHub tools through MCP Hub without exposing raw GitHub credentials to the devbox.
+workflow:
+ - Create an MCP config for GitHub
+ - Store GitHub token as a Runloop secret
+ - Launch a devbox with MCP Hub wiring
+ - Install Claude Code and register MCP endpoint
+ - Run a Claude prompt through MCP tools
+ - Shutdown devbox and clean up cloud resources
+tags:
+ - mcp
+ - devbox
+ - github
+ - commands
+ - cleanup
+prerequisites:
+ - RUNLOOP_API_KEY
+ - GITHUB_TOKEN (GitHub PAT with repo scope)
+ - ANTHROPIC_API_KEY
+run: GITHUB_TOKEN=ghp_xxx ANTHROPIC_API_KEY=sk-ant-xxx uv run python -m examples.mcp_github_tools
+test: uv run pytest -m smoketest tests/smoketests/examples/
+---
+"""
+
+from __future__ import annotations
+
+import os
+from dataclasses import dataclass
+
+from runloop_api_client import RunloopSDK
+
+from ._harness import run_as_cli, unique_name, wrap_recipe_with_options
+from .example_types import ExampleCheck, RecipeOutput, RecipeContext
+
+GITHUB_MCP_ENDPOINT = "https://api.githubcopilot.com/mcp/"
+
+
+@dataclass
+class McpExampleOptions:
+ """Options for the MCP GitHub tools example."""
+
+ skip_if_missing_credentials: bool = False
+
+
+def recipe(ctx: RecipeContext, options: McpExampleOptions) -> RecipeOutput: # noqa: ARG001
+ """Connect Claude Code to GitHub tools via MCP Hub."""
+ cleanup = ctx.cleanup
+
+ sdk = RunloopSDK()
+ resources_created: list[str] = []
+ checks: list[ExampleCheck] = []
+
+ github_token = os.environ.get("GITHUB_TOKEN")
+ anthropic_key = os.environ.get("ANTHROPIC_API_KEY")
+
+ if not github_token:
+ raise RuntimeError("Set GITHUB_TOKEN to a GitHub PAT with repo scope.")
+ if not anthropic_key:
+ raise RuntimeError("Set ANTHROPIC_API_KEY for Claude Code.")
+
+ # Register GitHub's MCP server with Runloop
+ mcp_config = sdk.api.mcp_configs.create(
+ name=unique_name("github-example"),
+ endpoint=GITHUB_MCP_ENDPOINT,
+ allowed_tools=[
+ "get_me",
+ "search_pull_requests",
+ "get_pull_request",
+ "get_repository",
+ "get_file_contents",
+ ],
+ description="GitHub MCP server - example",
+ )
+ resources_created.append(f"mcp_config:{mcp_config.id}")
+ cleanup.add(f"mcp_config:{mcp_config.id}", lambda: sdk.api.mcp_configs.delete(mcp_config.id))
+
+ # Store the GitHub PAT as a Runloop secret
+ secret_name = unique_name("example-github-mcp")
+ sdk.api.secrets.create(name=secret_name, value=github_token)
+ resources_created.append(f"secret:{secret_name}")
+ cleanup.add(f"secret:{secret_name}", lambda: sdk.api.secrets.delete(secret_name))
+
+ # Launch a devbox with MCP Hub wiring
+ devbox = sdk.devbox.create(
+ name=unique_name("mcp-claude-code"),
+ launch_parameters={
+ "resource_size_request": "SMALL",
+ "keep_alive_time_seconds": 300,
+ },
+ mcp={
+ "MCP_SECRET": {
+ "mcp_config": mcp_config.id,
+ "secret": secret_name,
+ },
+ },
+ )
+ resources_created.append(f"devbox:{devbox.id}")
+ cleanup.add(f"devbox:{devbox.id}", devbox.shutdown)
+
+ # Install Claude Code
+ install_result = devbox.cmd.exec("npm install -g @anthropic-ai/claude-code")
+ checks.append(
+ ExampleCheck(
+ name="install Claude Code",
+ passed=install_result.exit_code == 0,
+ details="installed" if install_result.exit_code == 0 else install_result.stderr(),
+ )
+ )
+ if install_result.exit_code != 0:
+ return RecipeOutput(resources_created=resources_created, checks=checks)
+
+ # Register MCP Hub endpoint with Claude Code
+ add_mcp_result = devbox.cmd.exec(
+ 'claude mcp add runloop-mcp --transport http "$RL_MCP_URL" --header "Authorization: Bearer $RL_MCP_TOKEN"',
+ )
+ checks.append(
+ ExampleCheck(
+ name="register MCP Hub in Claude",
+ passed=add_mcp_result.exit_code == 0,
+ details="registered" if add_mcp_result.exit_code == 0 else add_mcp_result.stderr(),
+ )
+ )
+ if add_mcp_result.exit_code != 0:
+ return RecipeOutput(resources_created=resources_created, checks=checks)
+
+ # Ask Claude to summarize latest PR via MCP tools
+ prompt = (
+ "Use the MCP tools to get my last pr and describe what it does in 2-3 sentences. "
+ "Also detail how you collected this information"
+ )
+ claude_result = devbox.cmd.exec(
+ f'ANTHROPIC_API_KEY={anthropic_key} claude -p "{prompt}" --dangerously-skip-permissions',
+ )
+ claude_stdout = claude_result.stdout().strip()
+ checks.append(
+ ExampleCheck(
+ name="Claude prompt through MCP succeeds",
+ passed=claude_result.exit_code == 0 and len(claude_stdout) > 0,
+ details="non-empty response received" if claude_result.exit_code == 0 else claude_result.stderr(),
+ )
+ )
+
+ return RecipeOutput(resources_created=resources_created, checks=checks)
+
+
+def validate_env(options: McpExampleOptions) -> tuple[bool, list[ExampleCheck]]:
+ """Validate required environment variables."""
+ checks: list[ExampleCheck] = []
+ skip_if_missing = options.skip_if_missing_credentials
+
+ github_token = os.environ.get("GITHUB_TOKEN")
+ if not github_token and skip_if_missing:
+ checks.append(
+ ExampleCheck(
+ name="GITHUB_TOKEN provided",
+ passed=False,
+ details="Skipped: missing GITHUB_TOKEN",
+ )
+ )
+ return True, checks
+
+ anthropic_key = os.environ.get("ANTHROPIC_API_KEY")
+ if not anthropic_key and skip_if_missing:
+ checks.append(
+ ExampleCheck(
+ name="ANTHROPIC_API_KEY provided",
+ passed=False,
+ details="Skipped: missing ANTHROPIC_API_KEY",
+ )
+ )
+ return True, checks
+
+ return False, checks
+
+
+run_mcp_github_tools_example = wrap_recipe_with_options(recipe, validate_env)
+
+
+if __name__ == "__main__":
+ run_as_cli(lambda: run_mcp_github_tools_example(McpExampleOptions()))
diff --git a/examples/registry.py b/examples/registry.py
new file mode 100644
index 000000000..41a4b4b51
--- /dev/null
+++ b/examples/registry.py
@@ -0,0 +1,39 @@
+"""
+This file is auto-generated by scripts/generate_examples_md.py.
+Do not edit manually.
+"""
+
+from __future__ import annotations
+
+from typing import Any, Callable, cast
+
+from .example_types import ExampleResult
+from .mcp_github_tools import run_mcp_github_tools_example
+from .devbox_from_blueprint_lifecycle import run_devbox_from_blueprint_lifecycle_example
+
+ExampleRegistryEntry = dict[str, Any]
+
+example_registry: list[ExampleRegistryEntry] = [
+ {
+ "slug": "devbox-from-blueprint-lifecycle",
+ "title": "Devbox From Blueprint (Run Command, Shutdown)",
+ "file_name": "devbox_from_blueprint_lifecycle.py",
+ "required_env": ["RUNLOOP_API_KEY"],
+ "run": run_devbox_from_blueprint_lifecycle_example,
+ },
+ {
+ "slug": "mcp-github-tools",
+ "title": "MCP Hub + Claude Code + GitHub",
+ "file_name": "mcp_github_tools.py",
+ "required_env": ["RUNLOOP_API_KEY", "GITHUB_TOKEN", "ANTHROPIC_API_KEY"],
+ "run": run_mcp_github_tools_example,
+ },
+]
+
+
+def get_example_runner(slug: str) -> Callable[[], ExampleResult] | None:
+ """Get the runner function for an example by slug."""
+ for entry in example_registry:
+ if entry["slug"] == slug:
+ return cast(Callable[[], ExampleResult], entry["run"])
+ return None
diff --git a/llms.txt b/llms.txt
new file mode 100644
index 000000000..6683b6554
--- /dev/null
+++ b/llms.txt
@@ -0,0 +1,57 @@
+# Runloop Python SDK
+
+> Python client for Runloop.ai - create cloud devboxes, execute commands, manage blueprints and snapshots. Additional platform documentation is available at [docs.runloop.ai](https://docs.runloop.ai) and [docs.runloop.ai/llms.txt](https://docs.runloop.ai/llms.txt).
+
+## Quick Start
+
+- [README.md](README.md): Installation, authentication, and quickstart example
+- [README-SDK.md](README-SDK.md): Object-oriented SDK documentation
+- [EXAMPLES.md](EXAMPLES.md): Consolidated workflow recipes
+
+## Core Patterns
+
+- [Devbox lifecycle example](examples/devbox_from_blueprint_lifecycle.py): Create blueprint, launch devbox, run commands, cleanup
+- [MCP GitHub example](examples/mcp_github_tools.py): MCP Hub integration with Claude Code
+
+## API Reference
+
+- [SDK entry point](src/runloop_api_client/__init__.py): `AsyncRunloopSDK` and `RunloopSDK` classes
+- [Type definitions](src/runloop_api_client/types/): Pydantic types for all resources
+
+## Key Guidance
+
+- **Prefer `AsyncRunloopSDK` over `RunloopSDK`** for better concurrency and performance; all SDK methods have async equivalents
+- Use `async with await runloop.devbox.create()` for automatic cleanup via context manager
+- For resources without SDK coverage (e.g., secrets, benchmarks), use `runloop.api.*` as a fallback
+- Execute commands with `await devbox.cmd.exec('command')`, not raw API calls
+- Always call `await devbox.shutdown()` to clean up resources (or use context manager)
+- Streaming callbacks (`stdout`, `stderr`) must be synchronous functions even with async SDK
+- In example files, focus on the `recipe` function body for SDK usage patterns; ignore test harness files (`_harness.py`, `registry.py`, `example_types.py`)
+
+## Async vs Sync
+
+The SDK provides both sync and async variants. **The async SDK (`AsyncRunloopSDK`) is recommended** because:
+
+- Better resource utilization when running multiple devbox operations
+- Non-blocking I/O for long-running commands
+- Native async/await support integrates cleanly with modern Python frameworks
+
+Sync SDK example:
+```python
+from runloop_api_client import RunloopSDK
+runloop = RunloopSDK()
+with runloop.devbox.create(name="my-devbox") as devbox:
+ result = devbox.cmd.exec("echo hello")
+```
+
+Async SDK example (preferred):
+```python
+from runloop_api_client import AsyncRunloopSDK
+runloop = AsyncRunloopSDK()
+async with await runloop.devbox.create(name="my-devbox") as devbox:
+ result = await devbox.cmd.exec("echo hello")
+```
+
+## Optional
+
+- [External docs](https://docs.runloop.ai/llms.txt): Additional agent guidance from Runloop platform documentation
diff --git a/pyproject.toml b/pyproject.toml
index dfd204508..e80f1b5ea 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -66,6 +66,7 @@ dev = [
"pytest-xdist>=3.6.1",
"uuid-utils>=0.11.0",
"pytest-cov>=7.0.0",
+ "python-frontmatter>=1.0.0",
]
docs = [
"furo>=2025.9.25",
diff --git a/scripts/generate_examples_md.py b/scripts/generate_examples_md.py
new file mode 100644
index 000000000..bf4dc587a
--- /dev/null
+++ b/scripts/generate_examples_md.py
@@ -0,0 +1,263 @@
+#!/usr/bin/env python
+"""Generate EXAMPLES.md and examples/registry.py from example file frontmatter.
+
+Usage:
+ uv run python scripts/generate_examples_md.py # Generate files
+ uv run python scripts/generate_examples_md.py --check # Check if files are up to date
+"""
+
+from __future__ import annotations
+
+import re
+import sys
+import argparse
+from typing import Any
+from pathlib import Path
+
+import frontmatter # type: ignore[import-untyped]
+
+ROOT = Path(__file__).parent.parent
+EXAMPLES_DIR = ROOT / "examples"
+OUTPUT_FILE = ROOT / "EXAMPLES.md"
+OUTPUT_REGISTRY_FILE = EXAMPLES_DIR / "registry.py"
+LLMS_FILE = ROOT / "llms.txt"
+
+REQUIRED_FIELDS = ["title", "slug", "use_case", "workflow", "tags", "prerequisites", "run", "test"]
+EXCLUDED_FILES = {"_harness.py", "example_types.py", "registry.py", "__init__.py"}
+
+
+def parse_example(path: Path) -> dict[str, Any]:
+ """Parse frontmatter from a Python file's docstring."""
+ content = path.read_text()
+ match = re.search(r'^(?:#!.*\n)?(?:\s*\n)*"""([\s\S]*?)"""', content)
+ if not match:
+ raise ValueError(f"{path}: missing docstring")
+
+ docstring = match.group(1).strip()
+ if not docstring.startswith("---"):
+ raise ValueError(f"{path}: docstring must start with frontmatter (---)")
+
+ try:
+ post = frontmatter.loads(docstring)
+ return dict(post.metadata)
+ except Exception as e:
+ raise ValueError(f"{path}: invalid frontmatter: {e}") from e
+
+
+def validate_example(metadata: dict[str, Any], file_name: str, seen_slugs: set[str]) -> None:
+ """Validate all example metadata in one pass."""
+ path = f"examples/{file_name}"
+
+ missing = [f for f in REQUIRED_FIELDS if f not in metadata]
+ if missing:
+ raise ValueError(f"{path}: missing fields: {', '.join(missing)}")
+
+ for field in ("workflow", "tags", "prerequisites"):
+ if not isinstance(metadata[field], list) or not metadata[field]:
+ raise ValueError(f"{path}: '{field}' must be a non-empty list")
+
+ slug = metadata["slug"]
+ expected_slug = file_name.replace(".py", "").replace("_", "-")
+ if slug != expected_slug:
+ raise ValueError(f"{path}: slug '{slug}' must match '{expected_slug}'")
+ if slug in seen_slugs:
+ raise ValueError(f"{path}: duplicate slug")
+ seen_slugs.add(slug)
+
+ module_name = file_name.replace(".py", "")
+ if f"examples.{module_name}" not in metadata["run"]:
+ raise ValueError(f"{path}: run command must reference 'examples.{module_name}'")
+
+
+def ensure_llms_references(examples: list[dict[str, Any]]) -> None:
+ """Ensure llms.txt references at least one example file."""
+ if not LLMS_FILE.exists():
+ raise ValueError(f"Missing llms file: {LLMS_FILE.relative_to(ROOT)}")
+
+ llms_text = LLMS_FILE.read_text()
+ referenced = set(re.findall(r"examples/([a-z0-9_]+\.py)", llms_text))
+
+ if not referenced:
+ raise ValueError(f"{LLMS_FILE.relative_to(ROOT)}: expected at least one reference to examples/*.py")
+
+ generated = {e["file_name"] for e in examples}
+ for file_name in referenced:
+ if file_name not in generated:
+ raise ValueError(f"{LLMS_FILE.relative_to(ROOT)}: references unknown example file 'examples/{file_name}'")
+
+
+def normalize_env_var(prerequisite: str) -> str | None:
+ """Extract environment variable name from prerequisite string."""
+ match = re.match(r"^[A-Z0-9_]+", prerequisite)
+ return match.group(0) if match else None
+
+
+def markdown_for_example(example: dict[str, Any]) -> str:
+ """Generate markdown section for a single example."""
+ lines = [
+ f'',
+ f"## {example['title']}",
+ "",
+ f"**Use case:** {example['use_case']}",
+ "",
+ f"**Tags:** {', '.join(f'`{tag}`' for tag in example['tags'])}",
+ "",
+ "### Workflow",
+ *[f"- {step}" for step in example["workflow"]],
+ "",
+ "### Prerequisites",
+ *[f"- `{item}`" for item in example["prerequisites"]],
+ "",
+ "### Run",
+ "```sh",
+ example["run"],
+ "```",
+ "",
+ "### Test",
+ "```sh",
+ example["test"],
+ "```",
+ "",
+ f"**Source:** [`examples/{example['file_name']}`](./examples/{example['file_name']})",
+ "",
+ ]
+ return "\n".join(lines)
+
+
+def generate_markdown(examples: list[dict[str, Any]]) -> str:
+ """Generate the full EXAMPLES.md content."""
+ toc = "\n".join(f"- [{e['title']}](#{e['slug']})" for e in examples)
+ sections = "\n".join(markdown_for_example(e) for e in examples)
+
+ return "\n".join(
+ [
+ "# Examples",
+ "",
+ "> This file is auto-generated from metadata in `examples/*.py`.",
+ "> Do not edit this file manually. Run `uv run python scripts/generate_examples_md.py` instead.",
+ "",
+ "Runnable examples live in [`examples/`](./examples).",
+ "",
+ "## Table of Contents",
+ "",
+ toc,
+ "",
+ sections.rstrip(),
+ "",
+ ]
+ )
+
+
+def generate_registry(examples: list[dict[str, Any]]) -> str:
+ """Generate the registry.py content."""
+ imports: list[str] = []
+ for example in examples:
+ module = example["file_name"].replace(".py", "")
+ runner = f"run_{module}_example"
+ imports.append(f"from .{module} import {runner}")
+ imports.sort(key=len)
+
+ entries: list[str] = []
+ for example in examples:
+ module = example["file_name"].replace(".py", "")
+ runner = f"run_{module}_example"
+ env_vars = [normalize_env_var(p) for p in example["prerequisites"]]
+ env_list = ", ".join(f'"{e}"' for e in env_vars if e)
+ title = example["title"].replace('"', '\\"')
+ entries.append(f''' {{
+ "slug": "{example["slug"]}",
+ "title": "{title}",
+ "file_name": "{example["file_name"]}",
+ "required_env": [{env_list}],
+ "run": {runner},
+ }},''')
+
+ return f'''"""
+This file is auto-generated by scripts/generate_examples_md.py.
+Do not edit manually.
+"""
+
+from __future__ import annotations
+
+from typing import Any, Callable, cast
+
+from .example_types import ExampleResult
+{chr(10).join(imports)}
+
+ExampleRegistryEntry = dict[str, Any]
+
+example_registry: list[ExampleRegistryEntry] = [
+{chr(10).join(entries)}
+]
+
+
+def get_example_runner(slug: str) -> Callable[[], ExampleResult] | None:
+ """Get the runner function for an example by slug."""
+ for entry in example_registry:
+ if entry["slug"] == slug:
+ return cast(Callable[[], ExampleResult], entry["run"])
+ return None
+'''
+
+
+def main() -> int:
+ """Main entry point."""
+ parser = argparse.ArgumentParser(description="Generate EXAMPLES.md and registry.py")
+ parser.add_argument("--check", action="store_true", help="Check if files are up to date")
+ args = parser.parse_args()
+
+ seen_slugs: set[str] = set()
+ examples: list[dict[str, Any]] = []
+
+ for path in sorted(EXAMPLES_DIR.glob("*.py")):
+ if path.name in EXCLUDED_FILES or path.name.startswith("_"):
+ continue
+ try:
+ metadata = parse_example(path)
+ validate_example(metadata, path.name, seen_slugs)
+ examples.append({**metadata, "file_name": path.name})
+ except ValueError as e:
+ print(f"Error: {e}", file=sys.stderr)
+ return 1
+
+ examples.sort(key=lambda e: e["title"])
+
+ try:
+ ensure_llms_references(examples)
+ except ValueError as e:
+ print(f"Error: {e}", file=sys.stderr)
+ return 1
+
+ markdown = generate_markdown(examples)
+ registry_source = generate_registry(examples)
+
+ if args.check:
+ errors: list[str] = []
+ if not OUTPUT_FILE.exists():
+ errors.append(f"{OUTPUT_FILE.relative_to(ROOT)} does not exist")
+ elif OUTPUT_FILE.read_text() != markdown:
+ errors.append(f"{OUTPUT_FILE.relative_to(ROOT)} is out of date")
+
+ if not OUTPUT_REGISTRY_FILE.exists():
+ errors.append(f"{OUTPUT_REGISTRY_FILE.relative_to(ROOT)} does not exist")
+ elif OUTPUT_REGISTRY_FILE.read_text() != registry_source:
+ errors.append(f"{OUTPUT_REGISTRY_FILE.relative_to(ROOT)} is out of date")
+
+ if errors:
+ for err in errors:
+ print(f"Error: {err}", file=sys.stderr)
+ print("\nRun `uv run python scripts/generate_examples_md.py` to regenerate.", file=sys.stderr)
+ return 1
+
+ print("All generated files are up to date.")
+ return 0
+
+ OUTPUT_FILE.write_text(markdown)
+ OUTPUT_REGISTRY_FILE.write_text(registry_source)
+ print(f"Wrote {OUTPUT_FILE.relative_to(ROOT)} from {len(examples)} example(s)")
+ print(f"Wrote {OUTPUT_REGISTRY_FILE.relative_to(ROOT)} from {len(examples)} example(s)")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/tests/smoketests/examples/__init__.py b/tests/smoketests/examples/__init__.py
new file mode 100644
index 000000000..c0b52f6e9
--- /dev/null
+++ b/tests/smoketests/examples/__init__.py
@@ -0,0 +1 @@
+# Smoketests for examples
diff --git a/tests/smoketests/examples/test_examples.py b/tests/smoketests/examples/test_examples.py
new file mode 100644
index 000000000..7dbe82581
--- /dev/null
+++ b/tests/smoketests/examples/test_examples.py
@@ -0,0 +1,107 @@
+"""Smoketests for SDK examples.
+
+These tests run the example scripts against the live API.
+Set RUN_EXAMPLE_LIVE_TESTS=1 to enable live tests.
+"""
+
+from __future__ import annotations
+
+import os
+import sys
+from typing import Any
+from pathlib import Path
+
+import pytest
+
+# Add the root directory to the path so we can import examples
+sys.path.insert(0, str(Path(__file__).parents[3]))
+
+from examples.registry import example_registry # noqa: E402
+from examples.example_types import ExampleResult # noqa: E402
+from examples.mcp_github_tools import McpExampleOptions, run_mcp_github_tools_example # noqa: E402
+
+LONG_TIMEOUT = 600 # 10 minutes for live tests
+SHORT_TIMEOUT = 30 # 30 seconds for skip mode tests
+
+
+class TestExamples:
+ """Tests for SDK examples."""
+
+ @pytest.mark.smoketest
+ @pytest.mark.timeout(LONG_TIMEOUT)
+ @pytest.mark.parametrize("entry", example_registry, ids=lambda e: e["slug"])
+ def test_example_runs_with_successful_checks(self, entry: dict[str, Any]) -> None:
+ """Test that examples run successfully with all checks passing."""
+ if not os.environ.get("RUN_EXAMPLE_LIVE_TESTS"):
+ pytest.skip("RUN_EXAMPLE_LIVE_TESTS not set")
+
+ required_env: list[str] = entry["required_env"]
+ missing_env = [e for e in required_env if not os.environ.get(e)]
+ if missing_env:
+ pytest.skip(f"Missing env vars: {missing_env}")
+
+ # Handle examples that need options
+ result: ExampleResult
+ if entry["slug"] == "mcp-github-tools":
+ result = run_mcp_github_tools_example(McpExampleOptions())
+ else:
+ result = entry["run"]()
+
+ assert not result.skipped, "Example was unexpectedly skipped"
+ assert len(result.resources_created) > 0, "No resources were created"
+ assert len(result.checks) > 0, "No checks were performed"
+
+ failed_checks = [c for c in result.checks if not c.passed]
+ assert not failed_checks, f"Failed checks: {[c.name for c in failed_checks]}"
+
+ assert len(result.cleanup_status.failed) == 0, (
+ f"Cleanup failures: {[f.resource for f in result.cleanup_status.failed]}"
+ )
+
+ @pytest.mark.timeout(SHORT_TIMEOUT)
+ def test_mcp_skip_mode_for_missing_credentials(self) -> None:
+ """Test that mcp-github-tools example skips deterministically when credentials are missing."""
+ # Save original env vars
+ original_github_token = os.environ.get("GITHUB_TOKEN")
+ original_anthropic_key = os.environ.get("ANTHROPIC_API_KEY")
+
+ # Remove credentials
+ if "GITHUB_TOKEN" in os.environ:
+ del os.environ["GITHUB_TOKEN"]
+ if "ANTHROPIC_API_KEY" in os.environ:
+ del os.environ["ANTHROPIC_API_KEY"]
+
+ try:
+ result = run_mcp_github_tools_example(McpExampleOptions(skip_if_missing_credentials=True))
+
+ assert result.skipped, "Example should be skipped when credentials are missing"
+ assert len(result.resources_created) == 0, "No resources should be created when skipped"
+ assert len(result.cleanup_status.attempted) == 0, "No cleanup should be attempted when skipped"
+ finally:
+ # Restore original env vars
+ if original_github_token is not None:
+ os.environ["GITHUB_TOKEN"] = original_github_token
+ elif "GITHUB_TOKEN" in os.environ:
+ del os.environ["GITHUB_TOKEN"]
+
+ if original_anthropic_key is not None:
+ os.environ["ANTHROPIC_API_KEY"] = original_anthropic_key
+ elif "ANTHROPIC_API_KEY" in os.environ:
+ del os.environ["ANTHROPIC_API_KEY"]
+
+ @pytest.mark.timeout(SHORT_TIMEOUT)
+ def test_example_registry_is_populated(self) -> None:
+ """Test that the example registry contains expected entries."""
+ assert len(example_registry) >= 2, "Expected at least 2 examples in registry"
+
+ slugs = {e["slug"] for e in example_registry}
+ assert "devbox-from-blueprint-lifecycle" in slugs
+ assert "mcp-github-tools" in slugs
+
+ for entry in example_registry:
+ assert "slug" in entry
+ assert "title" in entry
+ assert "file_name" in entry
+ assert "required_env" in entry
+ assert "run" in entry
+ assert callable(entry["run"])
diff --git a/uv.lock b/uv.lock
index 26c8d993a..4f0209e1f 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2082,6 +2082,91 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
+[[package]]
+name = "python-frontmatter"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyyaml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/96/de/910fa208120314a12f9a88ea63e03707261692af782c99283f1a2c8a5e6f/python-frontmatter-1.1.0.tar.gz", hash = "sha256:7118d2bd56af9149625745c58c9b51fb67e8d1294a0c76796dafdc72c36e5f6d", size = 16256, upload-time = "2024-01-16T18:50:04.052Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/49/87/3c8da047b3ec5f99511d1b4d7a5bc72d4b98751c7e78492d14dc736319c5/python_frontmatter-1.1.0-py3-none-any.whl", hash = "sha256:335465556358d9d0e6c98bbeb69b1c969f2a4a21360587b9873bfc3b213407c1", size = 9834, upload-time = "2024-01-16T18:50:00.911Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
+ { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
+ { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
+ { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
+ { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
+ { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
+ { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" },
+ { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" },
+ { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" },
+]
+
[[package]]
name = "requests"
version = "2.32.5"
@@ -2267,7 +2352,7 @@ wheels = [
[[package]]
name = "runloop-api-client"
-version = "1.9.0"
+version = "1.10.3"
source = { editable = "." }
dependencies = [
{ name = "anyio" },
@@ -2298,6 +2383,7 @@ dev = [
{ name = "pytest-cov" },
{ name = "pytest-timeout" },
{ name = "pytest-xdist" },
+ { name = "python-frontmatter" },
{ name = "respx" },
{ name = "rich" },
{ name = "ruff" },
@@ -2343,6 +2429,7 @@ dev = [
{ name = "pytest-cov", specifier = ">=7.0.0" },
{ name = "pytest-timeout" },
{ name = "pytest-xdist", specifier = ">=3.6.1" },
+ { name = "python-frontmatter", specifier = ">=1.0.0" },
{ name = "respx" },
{ name = "rich", specifier = ">=13.7.1" },
{ name = "ruff" },