From ebbac48db6c3e2000ee53a3d4d39824fecfb4ae8 Mon Sep 17 00:00:00 2001 From: nicoche <78445450+nicoche@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:42:54 +0200 Subject: [PATCH] Allow more granularity to run examples usage: 00_run_all.py [-h] [--flows FLOWS] [--timeout TIMEOUT] [--flow-timeout FLOW TIMEOUT] Run example flows options: -h, --help show this help message and exit --flows FLOWS Comma-separated flow names to run (e.g., '01_create_sandbox,02_create_sandbox_with_timing'). Prefix with '!' to skip (e.g., '!03_*,!05_*') --timeout TIMEOUT Default timeout per flow in seconds (default: 60) --flow-timeout FLOW TIMEOUT Override timeout for specific flow (e.g., --flow-timeout 16_create_sandbox_with_auto_delete_simple.py 180) --- examples/00_run_all.py | 107 +++++++++++- examples/00_run_all_async.py | 154 ++++++++++++++---- examples/01_create_sandbox.py | 5 +- examples/01_create_sandbox_async.py | 5 +- examples/02_create_sandbox_with_timing.py | 5 +- .../02_create_sandbox_with_timing_async.py | 5 +- examples/03_basic_commands.py | 6 +- examples/03_basic_commands_async.py | 6 +- examples/04_streaming_output.py | 6 +- examples/04_streaming_output_async.py | 6 +- examples/05_environment_variables.py | 6 +- examples/05_environment_variables_async.py | 6 +- examples/06_working_directory.py | 6 +- examples/06_working_directory_async.py | 6 +- examples/07_file_operations.py | 6 +- examples/07_file_operations_async.py | 6 +- examples/08_directory_operations.py | 6 +- examples/08_directory_operations_async.py | 6 +- examples/09_binary_files.py | 6 +- examples/09_binary_files_async.py | 6 +- examples/10_batch_operations.py | 6 +- examples/10_batch_operations_async.py | 6 +- examples/11_upload_download.py | 6 +- examples/11_upload_download_async.py | 6 +- examples/12_file_manipulation.py | 6 +- examples/12_file_manipulation_async.py | 6 +- examples/13_background_processes.py | 6 +- examples/13_background_processes_async.py | 6 +- examples/14_expose_port.py | 6 +- examples/14_expose_port_async.py | 6 +- examples/15_get_sandbox.py | 4 + examples/15_get_sandbox_async.py | 4 + ..._create_sandbox_with_auto_delete_simple.py | 102 ++++++++++++ ... => 17_create_sandbox_with_auto_delete.py} | 5 +- ...=> 18_create_sandbox_with_existing_app.py} | 5 +- 35 files changed, 479 insertions(+), 66 deletions(-) create mode 100644 examples/16_create_sandbox_with_auto_delete_simple.py rename examples/{16_create_sandbox_with_auto_delete.py => 17_create_sandbox_with_auto_delete.py} (98%) rename examples/{17_create_sandbox_with_existing_app.py => 18_create_sandbox_with_existing_app.py} (94%) diff --git a/examples/00_run_all.py b/examples/00_run_all.py index 01ec7598..8a144697 100644 --- a/examples/00_run_all.py +++ b/examples/00_run_all.py @@ -1,30 +1,59 @@ #!/usr/bin/env python3 """Run all synchronous example scripts in order""" +import argparse +import os +import string import subprocess import sys import time from pathlib import Path - def main(): + parser = argparse.ArgumentParser(description="Run example flows") + parser.add_argument( + "--flows", + default="all", + help="Comma-separated flow names to run (e.g., '01_create_sandbox,02_create_sandbox_with_timing'). Prefix with '!' to skip (e.g., '!03_*,!05_*')", + ) + parser.add_argument( + "--timeout", + type=int, + default=60, + help="Default timeout per flow in seconds (default: 60)", + ) + parser.add_argument( + "--flow-timeout", + action="append", + nargs=2, + metavar=("FLOW", "TIMEOUT"), + help="Override timeout for specific flow (e.g., --flow-timeout 16_create_sandbox_with_auto_delete_simple.py 180)", + ) + args = parser.parse_args() + # Get the examples directory examples_dir = Path(__file__).parent # Find all Python files, excluding this script and async variants - example_files = sorted( + all_example_files = sorted( [ f for f in examples_dir.glob("*.py") - if f.name not in ["00_run_all.py", "00_run_all.py"] + if f.name not in ["00_run_all.py", "00_run_all_async.py"] and not f.name.endswith("_async.py") ] ) + # Filter flows based on specification + example_files = filter_flows(all_example_files, args.flows) + if not example_files: - print("No example files found to run") + print("No example files match the specified flows") return 0 + # Build flow timeout mapping + flow_timeouts = build_flow_timeouts(args.flow_timeout) + print(f"Found {len(example_files)} example(s) to run\n") print("=" * 70) @@ -37,14 +66,15 @@ def main(): print("-" * 70) start_time = time.time() + timeout = flow_timeouts.get(example_name, args.timeout) try: - # Run the example script + print(f"Run example script with timeout: {timeout}s") result = subprocess.run( [sys.executable, str(example_file)], capture_output=True, text=True, - timeout=60, # 60 second timeout per script + timeout=timeout, ) elapsed_time = time.time() - start_time @@ -131,6 +161,71 @@ def main(): return 0 +def filter_flows(all_files, flows_spec): + """ + Filter example files based on flow specification. + + Args: + all_files: List of all example Path objects + flows_spec: Comma-separated flow names. Prefix with '!' to skip. + Use 'all' to include all flows. + + Returns: + List of filtered Path objects + """ + if flows_spec.lower() == "all": + return all_files + + include_patterns = [] + skip_patterns = [] + + if flows_spec.startswith("!"): + skip_patterns = [p.strip() for p in flows_spec[1:].split(",") if p.strip()] + else: + include_patterns = [p.strip() for p in flows_spec.split(",") if p.strip()] + + filtered = [] + for f in all_files: + name = f.name + included = True + + if include_patterns: + included = any( + name == p or name.startswith(p.rstrip("*")) + for p in include_patterns + ) + + if skip_patterns: + skipped = any( + name == p or name.startswith(p.rstrip("*")) + for p in skip_patterns + ) + if skipped: + included = False + + if included: + filtered.append(f) + + return filtered + + +def build_flow_timeouts(flow_timeout_args): + """ + Build a mapping of flow names to custom timeouts. + + Args: + flow_timeout_args: List of (flow_name, timeout) tuples from argparse + + Returns: + Dict mapping flow names to timeout values + """ + flow_timeouts = {} + if flow_timeout_args: + for flow_name, timeout in flow_timeout_args: + flow_timeouts[flow_name] = int(timeout) + return flow_timeouts + + def print_summary(results, total_time): """Print execution summary""" diff --git a/examples/00_run_all_async.py b/examples/00_run_all_async.py index 6007e7c4..7e664cbb 100644 --- a/examples/00_run_all_async.py +++ b/examples/00_run_all_async.py @@ -1,35 +1,100 @@ #!/usr/bin/env python3 """Run all asynchronous example scripts in order""" +import argparse import asyncio -import os +import string import subprocess import sys import time from pathlib import Path -async def run_example(example_file): +def filter_flows(all_files, flows_spec): + """ + Filter example files based on flow specification. + + Args: + all_files: List of all example Path objects + flows_spec: Comma-separated flow names. Prefix with '!' to skip. + Use 'all' to include all flows. + + Returns: + List of filtered Path objects + """ + if flows_spec.lower() == "all": + return all_files + + include_patterns = [] + skip_patterns = [] + + if flows_spec.startswith("!"): + skip_patterns = [p.strip() for p in flows_spec[1:].split(",") if p.strip()] + else: + include_patterns = [p.strip() for p in flows_spec.split(",") if p.strip()] + + filtered = [] + for f in all_files: + name = f.name + included = True + + if include_patterns: + included = any( + name == p or name.startswith(p.rstrip("*")) + for p in include_patterns + ) + + if skip_patterns: + skipped = any( + name == p or name.startswith(p.rstrip("*")) + for p in skip_patterns + ) + if skipped: + included = False + + if included: + filtered.append(f) + + return filtered + + +def build_flow_timeouts(flow_timeout_args): + """ + Build a mapping of flow names to custom timeouts. + + Args: + flow_timeout_args: List of (flow_name, timeout) tuples from argparse + + Returns: + Dict mapping flow names to timeout values + """ + flow_timeouts = {} + if flow_timeout_args: + for flow_name, timeout in flow_timeout_args: + flow_timeouts[flow_name] = int(timeout) + return flow_timeouts + + +async def run_example(example_file, timeout): """Run a single example script and return results""" example_name = example_file.name print(f"\nā–¶ Running: {example_name}") print("-" * 70) - + start_time = time.time() - + try: - # Run the example script process = await asyncio.create_subprocess_exec( sys.executable, str(example_file), stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) - + try: stdout, stderr = await asyncio.wait_for( process.communicate(), - timeout=60 # 60 second timeout per script + timeout=timeout, ) except asyncio.TimeoutError: process.kill() @@ -42,13 +107,13 @@ async def run_example(example_file): "time": elapsed_time, "error": "Script exceeded 60 second timeout" } - + elapsed_time = time.time() - start_time - + # Print output if stdout: print(stdout.decode()) - + # Check for errors if process.returncode != 0: print(f"\nāŒ ERROR in {example_name}") @@ -69,11 +134,11 @@ async def run_example(example_file): "status": "PASSED", "time": elapsed_time } - + except Exception as e: elapsed_time = time.time() - start_time print(f"\nāŒ EXCEPTION in {example_name}: {e}") - + return { "name": example_name, "status": "ERROR", @@ -83,30 +148,59 @@ async def run_example(example_file): async def main(): + parser = argparse.ArgumentParser(description="Run async example flows") + parser.add_argument( + "--flows", + default="all", + help="Comma-separated flow names to run (e.g., '01_create_sandbox_async,02_*'). Prefix with '!' to skip (e.g., '!03_*,!05_*')", + ) + parser.add_argument( + "--timeout", + type=int, + default=60, + help="Default timeout per flow in seconds (default: 60)", + ) + parser.add_argument( + "--flow-timeout", + action="append", + nargs=2, + metavar=("FLOW", "TIMEOUT"), + help="Override timeout for specific flow (e.g., --flow-timeout 16_create_sandbox_with_auto_delete_simple.py 180)", + ) + args = parser.parse_args() + # Get the examples directory examples_dir = Path(__file__).parent - + # Find all async Python files, excluding this script - example_files = sorted([ + all_example_files = sorted([ f for f in examples_dir.glob("*_async.py") if f.name != "00_run_all_async.py" ]) - + + # Filter flows based on specification + example_files = filter_flows(all_example_files, args.flows) + if not example_files: - print("No async example files found to run") + print("No async example files match the specified flows") return 0 - + + # Build flow timeout mapping + flow_timeouts = build_flow_timeouts(args.flow_timeout) + print(f"Found {len(example_files)} async example(s) to run\n") print("=" * 70) - + total_start = time.time() results = [] - + # Run examples sequentially to maintain order and stop on first error for example_file in example_files: - result = await run_example(example_file) + example_name = example_file.name + timeout = flow_timeouts.get(example_name, args.timeout) + result = await run_example(example_file, timeout, suffix) results.append(result) - + # Break on error if result["status"] in ["FAILED", "TIMEOUT", "ERROR"]: print("\n" + "=" * 70) @@ -114,15 +208,15 @@ async def main(): print("=" * 70) print_summary(results, time.time() - total_start) return 1 - + total_time = time.time() - total_start - + # Print summary print("\n" + "=" * 70) print("ALL ASYNC EXAMPLES COMPLETED SUCCESSFULLY") print("=" * 70) print_summary(results, total_time) - + return 0 @@ -130,7 +224,7 @@ def print_summary(results, total_time): """Print execution summary""" print("\nšŸ“Š EXECUTION SUMMARY") print("-" * 70) - + for result in results: status_symbol = { "PASSED": "āœ“", @@ -138,16 +232,16 @@ def print_summary(results, total_time): "TIMEOUT": "ā±", "ERROR": "āŒ" }.get(result["status"], "?") - + print(f"{status_symbol} {result['name']:40s} {result['time']:>6.2f}s {result['status']}") - + if "error" in result: error_preview = result["error"].split("\n")[0][:50] print(f" Error: {error_preview}") - + print("-" * 70) print(f"Total execution time: {total_time:.2f}s") - + passed = sum(1 for r in results if r["status"] == "PASSED") total = len(results) print(f"Results: {passed}/{total} passed") diff --git a/examples/01_create_sandbox.py b/examples/01_create_sandbox.py index 1ca091c2..b3bd82f8 100644 --- a/examples/01_create_sandbox.py +++ b/examples/01_create_sandbox.py @@ -2,6 +2,8 @@ """Create and manage a sandbox""" import os +import random +import string from koyeb import Sandbox @@ -13,10 +15,11 @@ def main(): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: sandbox = Sandbox.create( image="koyeb/sandbox", - name="example-sandbox", + name=f"example-sandbox-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/01_create_sandbox_async.py b/examples/01_create_sandbox_async.py index dcdf0199..de86d918 100644 --- a/examples/01_create_sandbox_async.py +++ b/examples/01_create_sandbox_async.py @@ -3,6 +3,8 @@ import asyncio import os +import random +import string from koyeb import AsyncSandbox @@ -14,10 +16,11 @@ async def main(): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: sandbox = await AsyncSandbox.create( image="koyeb/sandbox", - name="example-sandbox", + name=f"example-sandbox-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/02_create_sandbox_with_timing.py b/examples/02_create_sandbox_with_timing.py index d711cdc7..bff1d363 100644 --- a/examples/02_create_sandbox_with_timing.py +++ b/examples/02_create_sandbox_with_timing.py @@ -3,6 +3,8 @@ import argparse import os +import random +import string import time from collections import defaultdict from datetime import datetime @@ -75,13 +77,14 @@ def main(run_long_tests=False): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: # Create sandbox with timing print(" → Creating sandbox...") create_start = time.time() sandbox = Sandbox.create( image="koyeb/sandbox", - name="example-sandbox-timed", + name=f"example-sandbox-timed-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/02_create_sandbox_with_timing_async.py b/examples/02_create_sandbox_with_timing_async.py index 90bce722..8f3b2b8d 100644 --- a/examples/02_create_sandbox_with_timing_async.py +++ b/examples/02_create_sandbox_with_timing_async.py @@ -4,6 +4,8 @@ import argparse import asyncio import os +import random +import string import time from collections import defaultdict from datetime import datetime @@ -79,13 +81,14 @@ async def main(run_long_tests=False): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: # Create sandbox with timing print(" → Creating sandbox...") create_start = time.time() sandbox = await AsyncSandbox.create( image="koyeb/sandbox", - name="example-sandbox-timed", + name=f"example-sandbox-timed-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/03_basic_commands.py b/examples/03_basic_commands.py index 6305b44a..7b7bdf58 100644 --- a/examples/03_basic_commands.py +++ b/examples/03_basic_commands.py @@ -3,6 +3,9 @@ import os + +import random +import string from koyeb import Sandbox @@ -13,10 +16,11 @@ def main(): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: sandbox = Sandbox.create( image="koyeb/sandbox", - name="basic-commands", + name=f"basic-commands-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/03_basic_commands_async.py b/examples/03_basic_commands_async.py index 92c75749..a9ea8e82 100644 --- a/examples/03_basic_commands_async.py +++ b/examples/03_basic_commands_async.py @@ -4,6 +4,9 @@ import asyncio import os + +import random +import string from koyeb import AsyncSandbox @@ -14,10 +17,11 @@ async def main(): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: sandbox = await AsyncSandbox.create( image="koyeb/sandbox", - name="basic-commands", + name=f"basic-commands-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/04_streaming_output.py b/examples/04_streaming_output.py index 9ce5e2d2..ae8a9f3c 100644 --- a/examples/04_streaming_output.py +++ b/examples/04_streaming_output.py @@ -3,6 +3,9 @@ import os + +import random +import string from koyeb import Sandbox @@ -13,10 +16,11 @@ def main(): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: sandbox = Sandbox.create( image="koyeb/sandbox", - name="streaming", + name=f"streaming-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/04_streaming_output_async.py b/examples/04_streaming_output_async.py index 98b0270c..e59062aa 100644 --- a/examples/04_streaming_output_async.py +++ b/examples/04_streaming_output_async.py @@ -4,6 +4,9 @@ import asyncio import os + +import random +import string from koyeb import AsyncSandbox @@ -14,10 +17,11 @@ async def main(): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: sandbox = await AsyncSandbox.create( image="koyeb/sandbox", - name="streaming", + name=f"streaming-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/05_environment_variables.py b/examples/05_environment_variables.py index cfb1c8a8..fb525d29 100644 --- a/examples/05_environment_variables.py +++ b/examples/05_environment_variables.py @@ -3,6 +3,9 @@ import os + +import random +import string from koyeb import Sandbox @@ -13,10 +16,11 @@ def main(): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: sandbox = Sandbox.create( image="koyeb/sandbox", - name="env-vars", + name=f"env-vars-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/05_environment_variables_async.py b/examples/05_environment_variables_async.py index a91cd1af..670ca79c 100644 --- a/examples/05_environment_variables_async.py +++ b/examples/05_environment_variables_async.py @@ -4,6 +4,9 @@ import asyncio import os + +import random +import string from koyeb import AsyncSandbox @@ -14,10 +17,11 @@ async def main(): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: sandbox = await AsyncSandbox.create( image="koyeb/sandbox", - name="env-vars", + name=f"env-vars-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/06_working_directory.py b/examples/06_working_directory.py index ae190d54..0119e324 100644 --- a/examples/06_working_directory.py +++ b/examples/06_working_directory.py @@ -3,6 +3,9 @@ import os + +import random +import string from koyeb import Sandbox @@ -13,10 +16,11 @@ def main(): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: sandbox = Sandbox.create( image="koyeb/sandbox", - name="working-dir", + name=f"working-dir-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/06_working_directory_async.py b/examples/06_working_directory_async.py index 967d9329..aa6b7f93 100644 --- a/examples/06_working_directory_async.py +++ b/examples/06_working_directory_async.py @@ -4,6 +4,9 @@ import asyncio import os + +import random +import string from koyeb import AsyncSandbox @@ -14,10 +17,11 @@ async def main(): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: sandbox = await AsyncSandbox.create( image="koyeb/sandbox", - name="working-dir", + name=f"working-dir-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/07_file_operations.py b/examples/07_file_operations.py index 2593a465..56f74bca 100644 --- a/examples/07_file_operations.py +++ b/examples/07_file_operations.py @@ -3,6 +3,9 @@ import os + +import random +import string from koyeb import Sandbox @@ -13,10 +16,11 @@ def main(): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: sandbox = Sandbox.create( image="koyeb/sandbox", - name="file-ops", + name=f"file-ops-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/07_file_operations_async.py b/examples/07_file_operations_async.py index 2b683ce1..511daacc 100644 --- a/examples/07_file_operations_async.py +++ b/examples/07_file_operations_async.py @@ -4,6 +4,9 @@ import asyncio import os + +import random +import string from koyeb import AsyncSandbox @@ -14,10 +17,11 @@ async def main(): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: sandbox = await AsyncSandbox.create( image="koyeb/sandbox", - name="file-ops", + name=f"file-ops-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/08_directory_operations.py b/examples/08_directory_operations.py index 2ca11beb..50368038 100644 --- a/examples/08_directory_operations.py +++ b/examples/08_directory_operations.py @@ -3,6 +3,9 @@ import os + +import random +import string from koyeb import Sandbox @@ -13,10 +16,11 @@ def main(): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: sandbox = Sandbox.create( image="koyeb/sandbox", - name="directory-ops", + name=f"directory-ops-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/08_directory_operations_async.py b/examples/08_directory_operations_async.py index 91e92ca2..26314ca6 100644 --- a/examples/08_directory_operations_async.py +++ b/examples/08_directory_operations_async.py @@ -4,6 +4,9 @@ import asyncio import os + +import random +import string from koyeb import AsyncSandbox @@ -14,10 +17,11 @@ async def main(): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: sandbox = await AsyncSandbox.create( image="koyeb/sandbox", - name="directory-ops", + name=f"directory-ops-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/09_binary_files.py b/examples/09_binary_files.py index 375a8de4..ffd9725c 100644 --- a/examples/09_binary_files.py +++ b/examples/09_binary_files.py @@ -3,6 +3,9 @@ import os + +import random +import string from koyeb import Sandbox @@ -13,10 +16,11 @@ def main(): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: sandbox = Sandbox.create( image="koyeb/sandbox", - name="binary-files", + name=f"binary-files-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/09_binary_files_async.py b/examples/09_binary_files_async.py index 1d451039..ef837d25 100644 --- a/examples/09_binary_files_async.py +++ b/examples/09_binary_files_async.py @@ -4,6 +4,9 @@ import asyncio import os + +import random +import string from koyeb import AsyncSandbox @@ -14,10 +17,11 @@ async def main(): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: sandbox = await AsyncSandbox.create( image="koyeb/sandbox", - name="binary-files", + name=f"binary-files-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/10_batch_operations.py b/examples/10_batch_operations.py index 075c472d..bf7ca1ce 100644 --- a/examples/10_batch_operations.py +++ b/examples/10_batch_operations.py @@ -3,6 +3,9 @@ import os + +import random +import string from koyeb import Sandbox @@ -13,10 +16,11 @@ def main(): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: sandbox = Sandbox.create( image="koyeb/sandbox", - name="batch-ops", + name=f"batch-ops-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/10_batch_operations_async.py b/examples/10_batch_operations_async.py index 2b8ae99d..93d5d018 100644 --- a/examples/10_batch_operations_async.py +++ b/examples/10_batch_operations_async.py @@ -4,6 +4,9 @@ import asyncio import os + +import random +import string from koyeb import AsyncSandbox @@ -14,10 +17,11 @@ async def main(): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: sandbox = await AsyncSandbox.create( image="koyeb/sandbox", - name="batch-ops", + name=f"batch-ops-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/11_upload_download.py b/examples/11_upload_download.py index e0703d49..561d7ff8 100644 --- a/examples/11_upload_download.py +++ b/examples/11_upload_download.py @@ -4,6 +4,9 @@ import os import tempfile + +import random +import string from koyeb import Sandbox @@ -14,10 +17,11 @@ def main(): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: sandbox = Sandbox.create( image="koyeb/sandbox", - name="upload-download", + name=f"upload-download-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/11_upload_download_async.py b/examples/11_upload_download_async.py index cf159a69..870b610f 100644 --- a/examples/11_upload_download_async.py +++ b/examples/11_upload_download_async.py @@ -5,6 +5,9 @@ import os import tempfile + +import random +import string from koyeb import AsyncSandbox @@ -15,10 +18,11 @@ async def main(): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: sandbox = await AsyncSandbox.create( image="koyeb/sandbox", - name="upload-download", + name=f"upload-download-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/12_file_manipulation.py b/examples/12_file_manipulation.py index 3443caa5..8e761dac 100644 --- a/examples/12_file_manipulation.py +++ b/examples/12_file_manipulation.py @@ -3,6 +3,9 @@ import os + +import random +import string from koyeb import Sandbox @@ -13,10 +16,11 @@ def main(): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: sandbox = Sandbox.create( image="koyeb/sandbox", - name="file-manip", + name=f"file-manip-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/12_file_manipulation_async.py b/examples/12_file_manipulation_async.py index 520cb8aa..53b6da16 100644 --- a/examples/12_file_manipulation_async.py +++ b/examples/12_file_manipulation_async.py @@ -4,6 +4,9 @@ import asyncio import os + +import random +import string from koyeb import AsyncSandbox @@ -14,10 +17,11 @@ async def main(): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: sandbox = await AsyncSandbox.create( image="koyeb/sandbox", - name="file-manip", + name=f"file-manip-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/13_background_processes.py b/examples/13_background_processes.py index a373ee36..9def7a48 100755 --- a/examples/13_background_processes.py +++ b/examples/13_background_processes.py @@ -4,6 +4,9 @@ import os import time + +import random +import string from koyeb import Sandbox @@ -14,10 +17,11 @@ def main(): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: sandbox = Sandbox.create( image="koyeb/sandbox", - name="background-processes", + name=f"background-processes-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/13_background_processes_async.py b/examples/13_background_processes_async.py index 169990de..f0abfa7c 100755 --- a/examples/13_background_processes_async.py +++ b/examples/13_background_processes_async.py @@ -4,6 +4,9 @@ import asyncio import os + +import random +import string from koyeb import AsyncSandbox @@ -14,10 +17,11 @@ async def main(): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: sandbox = await AsyncSandbox.create( image="koyeb/sandbox", - name="background-processes", + name=f"background-processes-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/14_expose_port.py b/examples/14_expose_port.py index af91ec6a..8a5283db 100755 --- a/examples/14_expose_port.py +++ b/examples/14_expose_port.py @@ -6,6 +6,9 @@ import requests + +import random +import string from koyeb import Sandbox @@ -16,10 +19,11 @@ def main(): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: sandbox = Sandbox.create( image="koyeb/sandbox", - name="expose-port", + name=f"expose-port-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/14_expose_port_async.py b/examples/14_expose_port_async.py index 50ab62f6..d6339c11 100755 --- a/examples/14_expose_port_async.py +++ b/examples/14_expose_port_async.py @@ -6,6 +6,9 @@ import requests + +import random +import string from koyeb import AsyncSandbox @@ -16,10 +19,11 @@ async def main(): return sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: sandbox = await AsyncSandbox.create( image="koyeb/sandbox", - name="expose-port", + name=f"expose-port-{suffix}", wait_ready=True, api_token=api_token, ) diff --git a/examples/15_get_sandbox.py b/examples/15_get_sandbox.py index c295b909..20ec1c0b 100644 --- a/examples/15_get_sandbox.py +++ b/examples/15_get_sandbox.py @@ -3,6 +3,9 @@ import os + +import random +import string from koyeb import Sandbox @@ -13,6 +16,7 @@ def main(): return original_sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) retrieved_sandbox = None try: diff --git a/examples/15_get_sandbox_async.py b/examples/15_get_sandbox_async.py index 465256e0..7f0eeff2 100644 --- a/examples/15_get_sandbox_async.py +++ b/examples/15_get_sandbox_async.py @@ -4,6 +4,9 @@ import asyncio import os + +import random +import string from koyeb import AsyncSandbox @@ -14,6 +17,7 @@ async def main(): return original_sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) retrieved_sandbox = None try: diff --git a/examples/16_create_sandbox_with_auto_delete_simple.py b/examples/16_create_sandbox_with_auto_delete_simple.py new file mode 100644 index 00000000..1f7f06a4 --- /dev/null +++ b/examples/16_create_sandbox_with_auto_delete_simple.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""Create a single sandbox with auto-delete lifecycle setting and wait for it to be deleted""" + +import os +import time + + +import random +import string +from koyeb import Sandbox +from koyeb.sandbox.utils import get_api_client + + +def service_exists(api_token: str, service_id: str) -> bool: + """Check if a service still exists""" + try: + _, services_api, _, _, _ = get_api_client(api_token) + services_api.get_service(service_id) + return True + except Exception: + return False + + +def main(): + print("=" * 70) + print(" SIMPLE AUTO-DELETE SANDBOX DEMO") + print("=" * 70) + print() + print("This example creates a single sandbox with auto-delete after creation.") + print() + + api_token = os.getenv("KOYEB_API_TOKEN") + if not api_token: + print("Error: KOYEB_API_TOKEN not set") + return + + sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) + + try: + delete_after_delay = 60 # Delete 60s after creation + + print("→ Creating sandbox with auto-delete...") + print(f" - delete_after_delay: {delete_after_delay}s (delete after creation)") + print(f" → Expected deletion: ~{delete_after_delay}s after creation") + print() + + create_start = time.time() + sandbox = Sandbox.create( + image="koyeb/sandbox", + name=f"auto-delete-test-simple-{suffix}", + wait_ready=True, + api_token=api_token, + region="fra", + delete_after_delay=delete_after_delay, + ) + create_duration = time.time() - create_start + print(f"āœ“ Created in {create_duration:.1f}s") + print(f" Service ID: {sandbox.service_id}") + + # Quick health check + print() + print("→ Verifying sandbox is healthy...") + assert sandbox.is_healthy(), "Sandbox should be healthy" + result = sandbox.exec("echo 'Sandbox ready'") + print(f" āœ“ {result.stdout.strip()}") + print() + + # Wait for auto-deletion + print("→ Waiting for sandbox to be auto-deleted...") + start = time.time() + timeout = 300 + + while time.time() - start < timeout: + if not service_exists(api_token, sandbox.service_id): + duration = time.time() - start + print(f"āœ“ Sandbox was auto-deleted after {duration:.1f}s") + sandbox = None + break + time.sleep(5) + elapsed = time.time() - start + print(f" ... still waiting ({elapsed:.0f}s elapsed)") + else: + print(f"āœ— Timeout waiting for sandbox to be deleted") + + except Exception as e: + print(f"\nāœ— Error occurred: {e}") + import traceback + traceback.print_exc() + + finally: + if sandbox: + print() + print("→ Manually deleting sandbox (wasn't auto-deleted)...") + sandbox.delete() + + print() + print("āœ“ Demo completed") + + +if __name__ == "__main__": + main() diff --git a/examples/16_create_sandbox_with_auto_delete.py b/examples/17_create_sandbox_with_auto_delete.py similarity index 98% rename from examples/16_create_sandbox_with_auto_delete.py rename to examples/17_create_sandbox_with_auto_delete.py index a64875ee..b92c27cc 100644 --- a/examples/16_create_sandbox_with_auto_delete.py +++ b/examples/17_create_sandbox_with_auto_delete.py @@ -2,6 +2,8 @@ """Create sandboxes with auto-delete lifecycle settings and wait for them to be deleted""" import os +import random +import string import time from collections import defaultdict from datetime import datetime @@ -113,6 +115,7 @@ def main(): print("Error: KOYEB_API_TOKEN not set") return + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) sandbox1 = None sandbox2 = None @@ -177,7 +180,7 @@ def main(): create_start = time.time() sandbox2 = Sandbox.create( image="koyeb/sandbox", - name="auto-delete-test-2", + name=f"auto-delete-test-2-{suffix}", wait_ready=True, api_token=api_token, region="fra", diff --git a/examples/17_create_sandbox_with_existing_app.py b/examples/18_create_sandbox_with_existing_app.py similarity index 94% rename from examples/17_create_sandbox_with_existing_app.py rename to examples/18_create_sandbox_with_existing_app.py index 3e78807d..5ba2e760 100644 --- a/examples/17_create_sandbox_with_existing_app.py +++ b/examples/18_create_sandbox_with_existing_app.py @@ -2,6 +2,8 @@ """Create a sandbox using an existing app instead of creating a new one""" import os +import random +import string import time from koyeb import Sandbox @@ -17,6 +19,7 @@ def main(): app_id = None sandbox = None + suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) try: # Step 1: Create an app first @@ -47,7 +50,7 @@ def main(): print(f" Creating sandbox in app: {app_id}") sandbox = Sandbox.create( image="koyeb/sandbox", - name="sandbox-in-existing-app", + name=f"sandbox-in-existing-app-{suffix}", wait_ready=True, api_token=api_token, region="fra",