Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12,365 changes: 8,412 additions & 3,953 deletions docs/api.md

Large diffs are not rendered by default.

12,319 changes: 8,366 additions & 3,953 deletions docs/api_async.md

Large diffs are not rendered by default.

4,474 changes: 2,674 additions & 1,800 deletions docs/sandbox.md

Large diffs are not rendered by default.

45 changes: 45 additions & 0 deletions examples/28_service_pool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""Service pool lifecycle: create, list, update, and delete a pool.

A service pool keeps a target number of warm sandboxes ready so that
claiming (see examples/29_pool_claim.py) hands out a pre-provisioned
service instead of provisioning one on demand.
"""

import os
import sys

from koyeb.sandbox import ServicePool


def main() -> int:
api_token = os.environ.get("KOYEB_API_TOKEN")
if not api_token:
print("KOYEB_API_TOKEN is not set", file=sys.stderr)
return 1

# Create a pool of 3 warm sandboxes. Definition options mirror
# Sandbox.create (image, instance_type, env, region, ...).
pool = ServicePool.create(name="my-pool", size=3, api_token=api_token)
print(f"✓ Created {pool}")

# List every pool the caller can see.
pools = ServicePool.list(api_token=api_token)
print(f"✓ Listed {len(pools)} pool(s)")

# Update the pool's target size.
pool.update(size=5)
print("✓ Updated: size=5")

# Refresh re-fetches the pool (status, ready_count, ...).
pool.refresh()
print(f"✓ Refreshed, ready_count={pool.ready_count}, status={pool.status}")

# Delete the pool. The server fences it until outstanding claims drain.
pool.delete()
print("✓ Deleted")
return 0


if __name__ == "__main__":
sys.exit(main())
83 changes: 83 additions & 0 deletions examples/29_pool_claim.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""Claim a sandbox from a service pool (sync).

Spawns a pool, claims a sandbox, runs a command, and cleans up both.
claim() is idempotent per (pool_id, request_id): replaying the same pair
returns the same sandbox instead of consuming another.
"""

import os
import sys

from koyeb.sandbox import (
PoolClaimError,
Sandbox,
SandboxError,
ServicePool,
claim,
get_claim,
wait_claim_ready,
)


def main() -> int:
api_token = os.environ.get("KOYEB_API_TOKEN")
if not api_token:
print("KOYEB_API_TOKEN is not set", file=sys.stderr)
return 1

pool = ServicePool.create(name="claim-demo", size=1, api_token=api_token)
print(f"✓ Created pool {pool.id} (size {pool.size})")

try:
# Claim a sandbox from the pool. The request id is generated once
# and preserved across internal retries.
result = claim(pool.id, api_token=api_token)
print("✓ Claim fulfilled")
print(f" Claim ID: {result.claim_id}")
print(f" Service ID: {result.service_id}")
print(f" Prewarmed: {result.prewarmed}")
print(f" Request ID: {result.request_id}")

# Fetch the claim record by id.
info = get_claim(result.claim_id, api_token=api_token)
print(f" Claim status: {info.status}")

# A claimed service is detached from the pool and owned by the
# caller: delete it like any other sandbox when done.
sandbox = Sandbox.get_from_id(result.service_id, api_token=api_token)
try:
if not result.prewarmed:
# Cold path: no warm sandbox was available, so the claimed
# service was provisioned on demand — wait for it to boot.
wait_claim_ready(result, api_token=api_token)
print("✓ Claimed sandbox is ready")

out = sandbox.exec("echo 'Hello from a claimed sandbox!'")
print(f" Output: {out.stdout.strip()}")

# Replay demo: the same (pool_id, request_id) returns the same
# claim — safe to retry after a network failure.
replay = claim(
pool.id, request_id=result.request_id, api_token=api_token
)
assert replay.service_id == result.service_id
print("✓ Replay with the same request_id returned the same service")
finally:
sandbox.delete()
print("✓ Deleted the claimed sandbox service")
return 0
except PoolClaimError as e:
# Claim failed (pool missing, terminal state, retries exhausted, ...)
print(f"Claim failed: {e}")
return 1
except SandboxError as e:
print(f"Sandbox error: {e}")
return 1
finally:
pool.delete()
print("✓ Deleted the pool")


if __name__ == "__main__":
sys.exit(main())
77 changes: 77 additions & 0 deletions examples/29_pool_claim_async.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""Claim a sandbox from a service pool (async variant)"""

import asyncio
import os
import sys

from koyeb.sandbox import (
AsyncSandbox,
PoolClaimError,
SandboxError,
ServicePool,
claim_async,
get_claim_async,
wait_claim_ready_async,
)


async def main() -> int:
api_token = os.environ.get("KOYEB_API_TOKEN")
if not api_token:
print("KOYEB_API_TOKEN is not set", file=sys.stderr)
return 1

pool = await ServicePool.create(name="claim-demo", size=1, api_token=api_token)
print(f"✓ Created pool {pool.id} (size {pool.size})")

try:
# Claim a sandbox from the pool. The request id is generated once
# and preserved across internal retries.
result = await claim_async(pool.id, api_token=api_token)
print("✓ Claim fulfilled")
print(f" Claim ID: {result.claim_id}")
print(f" Service ID: {result.service_id}")
print(f" Prewarmed: {result.prewarmed}")
print(f" Request ID: {result.request_id}")

# Fetch the claim record by id.
info = await get_claim_async(result.claim_id, api_token=api_token)
print(f" Claim status: {info.status}")

# A claimed service is detached from the pool and owned by the
# caller: delete it like any other sandbox when done.
sandbox = await AsyncSandbox.get_from_id(result.service_id, api_token=api_token)
try:
if not result.prewarmed:
# Cold path: the claimed service was provisioned on demand.
await wait_claim_ready_async(result, api_token=api_token)
print("✓ Claimed sandbox is ready")

out = await sandbox.exec("echo 'Hello from a claimed sandbox!'")
print(f" Output: {out.stdout.strip()}")

# Replay demo: the same (pool_id, request_id) returns the same
# claim — safe to retry after a network failure.
replay = await claim_async(
pool.id, request_id=result.request_id, api_token=api_token
)
assert replay.service_id == result.service_id
print("✓ Replay with the same request_id returned the same service")
finally:
await sandbox.delete()
print("✓ Deleted the claimed sandbox service")
return 0
except PoolClaimError as e:
print(f"Claim failed: {e}")
return 1
except SandboxError as e:
print(f"Sandbox error: {e}")
return 1
finally:
await pool.delete()
print("✓ Deleted the pool")


if __name__ == "__main__":
sys.exit(asyncio.run(main()))
49 changes: 49 additions & 0 deletions examples/30_sandbox_list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""List sandboxes with Sandbox.list() and connect a lazy handle.

list() returns lazy handles: they know id/name but carry no executor
secret, so connected operations raise NoSandboxSecretError — connect
with Sandbox.get_from_id(handle.id) when you need to run commands.
"""

import os
import sys

from koyeb.sandbox import NoSandboxSecretError, Sandbox


def main() -> int:
api_token = os.environ.get("KOYEB_API_TOKEN")
if not api_token:
print("KOYEB_API_TOKEN is not set", file=sys.stderr)
return 1

# Create one sandbox so the listing has something of ours to find.
ours = Sandbox.create(name="list-demo", api_token=api_token)
print(f"✓ Created {ours.service_id}")

try:
handles = Sandbox.list(api_token=api_token)
print(f"✓ Listed {len(handles)} sandbox service(s)")
for handle in handles[:5]:
print(f" {handle.id} {handle.name}")

# Lazy handles have no executor secret...
try:
ours_handle = next(h for h in handles if h.id == ours.service_id)
ours_handle.exec("echo hi")
except NoSandboxSecretError:
print("✓ Lazy handle has no secret, as documented")

# ...so connect through get_from_id before running commands.
connected = Sandbox.get_from_id(ours.service_id, api_token=api_token)
out = connected.exec("echo hello from the list demo")
print(f"✓ Connected handle output: {out.stdout.strip()}")
finally:
ours.delete()
print("✓ Cleaned up")
return 0


if __name__ == "__main__":
sys.exit(main())
47 changes: 47 additions & 0 deletions examples/30_sandbox_list_async.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""List sandboxes with AsyncSandbox.list() and connect a lazy handle (async)"""

import asyncio
import os
import sys

from koyeb.sandbox import AsyncSandbox, NoSandboxSecretError


async def main() -> int:
api_token = os.environ.get("KOYEB_API_TOKEN")
if not api_token:
print("KOYEB_API_TOKEN is not set", file=sys.stderr)
return 1

# Create one sandbox so the listing has something of ours to find.
ours = await AsyncSandbox.create(name="list-demo", api_token=api_token)
print(f"✓ Created {ours.service_id}")

try:
handles = await AsyncSandbox.list(api_token=api_token)
print(f"✓ Listed {len(handles)} sandbox service(s)")
for handle in handles[:5]:
print(f" {handle.id} {handle.name}")

# Lazy handles have no executor secret...
try:
ours_handle = next(h for h in handles if h.id == ours.service_id)
await ours_handle.exec("echo hi")
except NoSandboxSecretError:
print("✓ Lazy handle has no secret, as documented")

# ...so connect through get_from_id before running commands.
connected = await AsyncSandbox.get_from_id(
ours.service_id, api_token=api_token
)
out = await connected.exec("echo hello from the list demo")
print(f"✓ Connected handle output: {out.stdout.strip()}")
finally:
await ours.delete()
print("✓ Cleaned up")
return 0


if __name__ == "__main__":
sys.exit(asyncio.run(main()))
41 changes: 41 additions & 0 deletions examples/31_raise_on_error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""Opt-in exec errors: raise_on_error turns a failed command into an exception.

By default a failed command returns a failed CommandResult; with
raise_on_error=True it raises SandboxCommandError carrying the result.
"""

import os
import sys

from koyeb.sandbox import Sandbox, SandboxCommandError


def main() -> int:
api_token = os.environ.get("KOYEB_API_TOKEN")
if not api_token:
print("KOYEB_API_TOKEN is not set", file=sys.stderr)
return 1

sandbox = Sandbox.create(name="raise-demo", api_token=api_token)
print(f"✓ Created {sandbox.service_id}")

try:
# Default: a failed command is a value, not an exception.
result = sandbox.exec("ls /definitely-missing")
print(f"✓ Default returned success={result.success}, exit={result.exit_code}")

# Opt-in: the same command raises, with the result attached.
try:
sandbox.exec("ls /definitely-missing", raise_on_error=True)
except SandboxCommandError as e:
print(f"✓ Raised SandboxCommandError: exit={e.result.exit_code}")
print(f" stderr: {e.result.stderr.strip()}")
return 0
finally:
sandbox.delete()
print("✓ Cleaned up")


if __name__ == "__main__":
sys.exit(main())
38 changes: 38 additions & 0 deletions examples/31_raise_on_error_async.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
"""Opt-in exec errors, async variant (raise_on_error)"""

import asyncio
import os
import sys

from koyeb.sandbox import AsyncSandbox, SandboxCommandError


async def main() -> int:
api_token = os.environ.get("KOYEB_API_TOKEN")
if not api_token:
print("KOYEB_API_TOKEN is not set", file=sys.stderr)
return 1

sandbox = await AsyncSandbox.create(name="raise-demo", api_token=api_token)
print(f"✓ Created {sandbox.service_id}")

try:
# Default: a failed command is a value, not an exception.
result = await sandbox.exec("ls /definitely-missing")
print(f"✓ Default returned success={result.success}, exit={result.exit_code}")

# Opt-in: the same command raises, with the result attached.
try:
await sandbox.exec("ls /definitely-missing", raise_on_error=True)
except SandboxCommandError as e:
print(f"✓ Raised SandboxCommandError: exit={e.result.exit_code}")
print(f" stderr: {e.result.stderr.strip()}")
return 0
finally:
await sandbox.delete()
print("✓ Cleaned up")


if __name__ == "__main__":
sys.exit(asyncio.run(main()))
Loading
Loading