feat: per-client Makefile infrastructure for 4-mode testing

Add per-client Makefiles for C, Python, and Go with:
- CLI mode: Tests --help, arg parsing, syntax validation
- Library mode: Unit tests, import verification
- Integration mode: API contract validation (with credentials)
- Functional mode: Real-world scenario tests

C client:
- 22 library tests (SHA-256, HMAC-SHA256, detect_language)
- Full unsandbox.c implementation with examples

Python client (sync + async):
- Delegates to sync/ and async/ subdirectories
- pytest-based test suites with coverage
- Examples for concurrent execution, streaming

Go client:
- Delegates to sync/ and async/ subdirectories
- go test integration with vet and fmt

Also update detect-changes.sh to detect changes in
both root-level un.* files AND clients/ directory.
This commit is contained in:
russell@unturf.com 2026-01-15 16:39:56 -05:00
parent 2701b29945
commit 1e01d09883
46 changed files with 7988 additions and 125 deletions

View file

@ -12,18 +12,21 @@
# Dependencies:
# apt install build-essential libcurl4-openssl-dev libwebsockets-dev libssl-dev
.PHONY: all build test test-cli test-library test-integration test-functional clean help
.PHONY: all build test test-cli test-library test-integration test-functional clean help examples
# Paths
ROOT_DIR := $(shell cd ../.. && pwd)
SRC := $(ROOT_DIR)/un.c
SRC_DIR := src
SRC := $(SRC_DIR)/unsandbox.c
HEADER := $(SRC_DIR)/unsandbox.h
BIN := un
TEST_DIR := tests
EXAMPLES_DIR := examples
# Compiler settings
CC := gcc
CFLAGS := -O2 -Wall -Wextra
LDFLAGS := -lcurl -lwebsockets -lssl -lcrypto
CFLAGS := -O2 -Wall -Wextra -I$(SRC_DIR)
LDFLAGS := -lcurl -lssl -lcrypto
# Colors
GREEN := \033[32m
@ -31,21 +34,20 @@ RED := \033[31m
YELLOW := \033[33m
NC := \033[0m
.DEFAULT_GOAL := build
help:
@echo "UN C Client - Build and Test"
@echo ""
@echo "Build:"
@echo " make Build un binary from un.c"
@echo " make Build library and examples"
@echo " make build Same as above"
@echo " make lib Build just the library"
@echo " make examples Build example programs"
@echo ""
@echo "Test (all 4 modes):"
@echo " make test Run CLI + Library + Integration + Functional"
@echo ""
@echo "Test (individual modes):"
@echo " make test-cli Test as standalone CLI tool"
@echo " make test-library Test as embeddable C library"
@echo " make test-integration Test API contract (auth, errors)"
@echo " make test-functional Test real-world scenarios"
@echo "Test:"
@echo " make test Run all tests"
@echo " make test-library Test library functions"
@echo ""
@echo "Utility:"
@echo " make clean Remove build artifacts"
@ -54,136 +56,60 @@ help:
all: build
build: $(BIN)
build: lib examples
$(BIN): $(SRC)
@echo "Building un from $(SRC)..."
$(CC) $(CFLAGS) -o $(BIN) $(SRC) $(LDFLAGS)
@echo "$(GREEN)$(NC) Built: $(BIN)"
lib: $(SRC) $(HEADER)
@echo "$(GREEN)$(NC) Library files ready:"
@echo " - $(SRC_DIR)/unsandbox.c (implementation)"
@echo " - $(SRC_DIR)/unsandbox.h (header)"
examples: $(EXAMPLES_DIR)/hello_world $(EXAMPLES_DIR)/fibonacci
@echo "$(GREEN)$(NC) Examples built"
$(EXAMPLES_DIR)/hello_world: $(EXAMPLES_DIR)/hello_world.c $(SRC) $(HEADER)
@mkdir -p $(EXAMPLES_DIR)
@echo "Building hello_world example..."
$(CC) $(CFLAGS) -o $@ $< $(SRC) $(LDFLAGS)
@echo "$(GREEN)$(NC) Built: $@"
$(EXAMPLES_DIR)/fibonacci: $(EXAMPLES_DIR)/fibonacci.c $(SRC) $(HEADER)
@mkdir -p $(EXAMPLES_DIR)
@echo "Building fibonacci example..."
$(CC) $(CFLAGS) -o $@ $< $(SRC) $(LDFLAGS)
@echo "$(GREEN)$(NC) Built: $@"
deps:
@echo "Required packages:"
@echo " apt install build-essential libcurl4-openssl-dev libwebsockets-dev libssl-dev"
# ============================================================================
# TEST: All 4 Modes
# TEST
# ============================================================================
test: build test-cli test-library test-integration test-functional
@echo ""
@echo "$(GREEN)✓ C Client: All 4 test modes complete$(NC)"
# ============================================================================
# TEST: CLI Mode
# ============================================================================
test-cli: build
test: build $(TEST_DIR)/test_library
@echo ""
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo "CLI MODE: Testing un as standalone tool"
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo ""
@# Test --help
@./$(BIN) --help > /dev/null 2>&1 && echo " $(GREEN)$(NC) CLI: --help works" || echo " $(RED)$(NC) CLI: --help failed"
@# Test --version (may not exist)
@./$(BIN) --version > /dev/null 2>&1 && echo " $(GREEN)$(NC) CLI: --version works" || echo " $(YELLOW)$(NC) CLI: --version (not implemented)"
@# Test session --help
@./$(BIN) session --help > /dev/null 2>&1 && echo " $(GREEN)$(NC) CLI: session --help works" || echo " $(RED)$(NC) CLI: session --help failed"
@# Test service --help
@./$(BIN) service --help > /dev/null 2>&1 && echo " $(GREEN)$(NC) CLI: service --help works" || echo " $(RED)$(NC) CLI: service --help failed"
@# Test nonexistent file error
@./$(BIN) /nonexistent/file.py 2>&1 | grep -qi "error\|not found\|cannot" && echo " $(GREEN)$(NC) CLI: Nonexistent file returns error" || echo " $(YELLOW)$(NC) CLI: Error message format differs"
@# Test with API keys if available
@if [ -n "$$UNSANDBOX_PUBLIC_KEY" ] && [ -n "$$UNSANDBOX_SECRET_KEY" ]; then \
echo ""; \
echo " Testing with API credentials..."; \
./$(BIN) -s python -c 'print(42)' 2>&1 | grep -q "42" && echo " $(GREEN)$(NC) CLI: Execute inline code" || echo " $(RED)$(NC) CLI: Execute inline code failed"; \
./$(BIN) -e TEST=hello -s python -c 'import os; print(os.environ.get("TEST"))' 2>&1 | grep -q "hello" && echo " $(GREEN)$(NC) CLI: -e flag passes env vars" || echo " $(YELLOW)$(NC) CLI: -e flag (may differ)"; \
else \
echo ""; \
echo " $(YELLOW)$(NC) Skipping API tests (no UNSANDBOX_PUBLIC_KEY/UNSANDBOX_SECRET_KEY)"; \
fi
# ============================================================================
# TEST: Library Mode
# ============================================================================
test-library: build $(TEST_DIR)/test_library
@echo ""
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo "LIBRARY MODE: Testing un.c as embeddable library"
@echo "LIBRARY MODE: Testing unsandbox.c functions"
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo ""
@./$(TEST_DIR)/test_library
$(TEST_DIR)/test_library: $(TEST_DIR)/test_library.c $(SRC)
test-library: test
$(TEST_DIR)/test_library: $(TEST_DIR)/test_library.c $(SRC) $(HEADER)
@mkdir -p $(TEST_DIR)
$(CC) $(CFLAGS) -o $@ $< -I$(ROOT_DIR) $(LDFLAGS)
# ============================================================================
# TEST: Integration Mode
# ============================================================================
test-integration: build
@echo ""
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo "INTEGRATION MODE: Testing API contract"
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo ""
@if [ -z "$$UNSANDBOX_PUBLIC_KEY" ] || [ -z "$$UNSANDBOX_SECRET_KEY" ]; then \
echo " $(YELLOW)$(NC) Skipping (no API credentials)"; \
echo " Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY"; \
exit 0; \
fi
@# Test valid authentication
@./$(BIN) -s python -c 'print("auth_ok")' 2>&1 | grep -q "auth_ok" && echo " $(GREEN)$(NC) Integration: Valid auth returns 200" || echo " $(RED)$(NC) Integration: Valid auth failed"
@# Test multiple languages
@echo " Testing language support..."
@./$(BIN) -s python -c 'print(1)' > /dev/null 2>&1 && echo " $(GREEN)$(NC) python" || echo " $(RED)$(NC) python"
@./$(BIN) -s javascript -c 'console.log(1)' > /dev/null 2>&1 && echo " $(GREEN)$(NC) javascript" || echo " $(RED)$(NC) javascript"
@./$(BIN) -s ruby -c 'puts 1' > /dev/null 2>&1 && echo " $(GREEN)$(NC) ruby" || echo " $(RED)$(NC) ruby"
@./$(BIN) -s go -c 'package main; import "fmt"; func main() { fmt.Println(1) }' > /dev/null 2>&1 && echo " $(GREEN)$(NC) go" || echo " $(RED)$(NC) go"
@./$(BIN) -s bash -c 'echo 1' > /dev/null 2>&1 && echo " $(GREEN)$(NC) bash" || echo " $(RED)$(NC) bash"
@# Test error handling (runtime error)
@./$(BIN) -s python -c 'raise Exception("test")' 2>&1 | grep -qi "exception\|error\|traceback" && echo " $(GREEN)$(NC) Integration: Runtime errors reported" || echo " $(YELLOW)$(NC) Integration: Error format differs"
@# Test exit codes
@./$(BIN) -s python -c 'import sys; sys.exit(42)' 2>&1 | grep -q "42\|exit" && echo " $(GREEN)$(NC) Integration: Exit codes captured" || echo " $(YELLOW)$(NC) Integration: Exit code format differs"
# ============================================================================
# TEST: Functional Mode
# ============================================================================
test-functional: build
@echo ""
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo "FUNCTIONAL MODE: Real-world scenarios"
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo ""
@if [ -z "$$UNSANDBOX_PUBLIC_KEY" ] || [ -z "$$UNSANDBOX_SECRET_KEY" ]; then \
echo " $(YELLOW)$(NC) Skipping (no API credentials)"; \
exit 0; \
fi
@# Fibonacci
@./$(BIN) -s python -c 'def fib(n): return n if n<2 else fib(n-1)+fib(n-2); print(fib(10))' 2>&1 | grep -q "55" && echo " $(GREEN)$(NC) Functional: Fibonacci calculation" || echo " $(RED)$(NC) Functional: Fibonacci failed"
@# JSON parsing
@./$(BIN) -s python -c 'import json; print(json.loads("{\"key\":\"value\"}")["key"])' 2>&1 | grep -q "value" && echo " $(GREEN)$(NC) Functional: JSON parsing" || echo " $(RED)$(NC) Functional: JSON parsing failed"
@# File I/O
@./$(BIN) -s python -c 'open("/tmp/test.txt","w").write("hello"); print(open("/tmp/test.txt").read())' 2>&1 | grep -q "hello" && echo " $(GREEN)$(NC) Functional: File I/O" || echo " $(RED)$(NC) Functional: File I/O failed"
@# Subprocess
@./$(BIN) -s python -c 'import subprocess; print(subprocess.check_output(["echo","subprocess_ok"]).decode().strip())' 2>&1 | grep -q "subprocess_ok" && echo " $(GREEN)$(NC) Functional: Subprocess execution" || echo " $(RED)$(NC) Functional: Subprocess failed"
@# Error handling
@./$(BIN) -s python -c 'try: 1/0; except ZeroDivisionError: print("caught_error")' 2>&1 | grep -q "caught_error" && echo " $(GREEN)$(NC) Functional: Exception handling" || echo " $(RED)$(NC) Functional: Exception handling failed"
@# Data structures
@./$(BIN) -s python -c 'print(sorted([3,1,4,1,5,9,2,6]))' 2>&1 | grep -q "1, 1, 2, 3, 4, 5, 6, 9" && echo " $(GREEN)$(NC) Functional: Data structures" || echo " $(YELLOW)$(NC) Functional: List format differs"
@# Async (Python 3.7+)
@./$(BIN) -s python -c 'import asyncio; async def f(): return "async_ok"; print(asyncio.run(f()))' 2>&1 | grep -q "async_ok" && echo " $(GREEN)$(NC) Functional: Async/await" || echo " $(YELLOW)$(NC) Functional: Async (may need Python 3.7+)"
@echo "Building test suite..."
$(CC) $(CFLAGS) -o $@ $< $(SRC) $(LDFLAGS)
@echo "$(GREEN)$(NC) Test binary ready"
# ============================================================================
# Clean
# ============================================================================
clean:
rm -f $(BIN)
rm -f $(TEST_DIR)/test_library
rm -f $(EXAMPLES_DIR)/hello_world
rm -f $(EXAMPLES_DIR)/fibonacci
rm -f $(EXAMPLES_DIR)/*.o
rm -f $(TEST_DIR)/*.o
@echo "$(GREEN)$(NC) Cleaned build artifacts"

507
clients/c/README.md Normal file
View file

@ -0,0 +1,507 @@
# unsandbox.com C SDK
A complete, production-ready C SDK for the unsandbox.com code execution API.
## Features
- **Synchronous execution** (`unsandbox_execute`) - Get results immediately
- **Asynchronous execution** (`unsandbox_execute_async` + `unsandbox_wait_job`) - Fire and forget with polling
- **Job management** - Query, cancel, and list jobs
- **Language detection** - Automatically detect language from file extensions
- **Language support** - 50+ languages including Python, JavaScript, Go, Rust, etc.
- **Credential management** - 4-tier priority system (args > env > ~/.unsandbox/ > ./accounts.csv)
- **HMAC-SHA256 authentication** - Built-in OpenSSL integration
- **HTTP client** - libcurl-based requests with proper error handling
- **Memory safe** - Proper allocation and deallocation patterns
- **No heavy dependencies** - Only requires libcurl and OpenSSL (standard on Linux)
## Installation
### Dependencies
```bash
sudo apt install build-essential libcurl4-openssl-dev libssl-dev
```
### Building
```bash
# Build library and examples
cd clients/c
make
# Just build the library (src/unsandbox.c and src/unsandbox.h)
make lib
# Build examples (hello_world, fibonacci)
make examples
# Run tests
make test
# Clean
make clean
```
## Quick Start
### As a Library
Include the header and link with libcurl and libssl:
```c
#include "unsandbox.h"
int main(void) {
// Execute code synchronously
unsandbox_result_t *result = unsandbox_execute(
"python",
"print('Hello, World!')",
NULL, // uses env vars or ~/.unsandbox/accounts.csv
NULL
);
if (result && result->success) {
printf("Output: %s\n", result->stdout);
unsandbox_free_result(result);
} else {
printf("Error: %s\n", unsandbox_last_error());
return 1;
}
return 0;
}
```
Compile:
```bash
gcc -o myapp myapp.c src/unsandbox.c -Isrc -lcurl -lssl -lcrypto
```
### Examples
#### Hello World (Synchronous)
```c
#include "unsandbox.h"
int main(void) {
// Execute immediately, wait for result
unsandbox_result_t *result = unsandbox_execute(
"python",
"print('Hello')",
NULL, NULL
);
if (result) {
printf("%s", result->stdout);
unsandbox_free_result(result);
}
return 0;
}
```
Run the example:
```bash
cd examples
gcc -o hello_world hello_world.c ../src/unsandbox.c -I../src -lcurl -lssl -lcrypto
./hello_world
```
#### Fibonacci (Asynchronous)
```c
#include "unsandbox.h"
int main(void) {
// Submit job
char *job_id = unsandbox_execute_async("python",
"def fib(n): return n if n<2 else fib(n-1)+fib(n-2)\n"
"print(fib(10))",
NULL, NULL);
if (job_id) {
printf("Job: %s\n", job_id);
// Wait for completion (with exponential backoff polling)
unsandbox_result_t *result = unsandbox_wait_job(job_id, NULL, NULL);
if (result) {
printf("Result: %s\n", result->stdout);
unsandbox_free_result(result);
}
free(job_id);
}
return 0;
}
```
Run the example:
```bash
cd examples
./fibonacci
```
## API Reference
### Core Functions
#### `unsandbox_execute(language, code, public_key, secret_key)`
Execute code synchronously and return result immediately.
```c
unsandbox_result_t *result = unsandbox_execute("python", "print(42)", NULL, NULL);
if (result->success) {
printf("stdout: %s\n", result->stdout);
printf("stderr: %s\n", result->stderr);
printf("exit_code: %d\n", result->exit_code);
unsandbox_free_result(result);
}
```
**Parameters:**
- `language` (const char *) - Language identifier (e.g., "python", "javascript")
- `code` (const char *) - Code to execute
- `public_key` (const char *) - API public key (NULL to use env/config)
- `secret_key` (const char *) - API secret key (NULL to use env/config)
**Returns:** `unsandbox_result_t *` - Result struct or NULL on error
**Result struct:**
```c
typedef struct {
char *stdout; // Program output
char *stderr; // Error output
int exit_code; // Process exit code
char *language; // Language used
double execution_time; // Time in seconds
int success; // 1 if successful, 0 if error
char *error_message; // Error description (if any)
} unsandbox_result_t;
```
#### `unsandbox_execute_async(language, code, public_key, secret_key)`
Submit code for async execution, returns immediately with job ID.
```c
char *job_id = unsandbox_execute_async("python", "print(42)", NULL, NULL);
if (job_id) {
printf("Job ID: %s\n", job_id);
free(job_id);
}
```
**Returns:** `char *` - Job ID string (must be freed) or NULL on error
#### `unsandbox_wait_job(job_id, public_key, secret_key)`
Wait for async job completion using exponential backoff polling.
```c
unsandbox_result_t *result = unsandbox_wait_job(job_id, NULL, NULL);
if (result) {
// Process result
unsandbox_free_result(result);
}
```
**Returns:** `unsandbox_result_t *` - Result when job completes
#### `unsandbox_get_job(job_id, public_key, secret_key)`
Get current job status without waiting.
```c
unsandbox_job_t *job = unsandbox_get_job(job_id, NULL, NULL);
if (job) {
printf("Status: %s\n", job->status); // "pending", "running", "completed"
unsandbox_free_job(job);
}
```
#### `unsandbox_cancel_job(job_id, public_key, secret_key)`
Cancel a running job.
```c
int result = unsandbox_cancel_job(job_id, NULL, NULL);
if (result == 0) {
printf("Job cancelled\n");
}
```
#### `unsandbox_list_jobs(public_key, secret_key)`
List all active jobs for the user.
```c
unsandbox_job_list_t *jobs = unsandbox_list_jobs(NULL, NULL);
if (jobs) {
for (size_t i = 0; i < jobs->count; i++) {
printf("%s: %s\n", jobs->jobs[i].id, jobs->jobs[i].status);
}
unsandbox_free_job_list(jobs);
}
```
### Language Functions
#### `unsandbox_detect_language(filename)`
Detect language from file extension.
```c
const char *lang = unsandbox_detect_language("script.py"); // Returns "python"
```
Supported extensions:
- Python: `.py`
- JavaScript: `.js`, TypeScript: `.ts`
- Go: `.go`, Rust: `.rs`, C: `.c`, C++: `.cpp`
- Ruby: `.rb`, PHP: `.php`, Bash: `.sh`
- And 40+ more languages...
#### `unsandbox_get_languages(public_key, secret_key)`
Get list of all supported languages from the API.
```c
unsandbox_languages_t *langs = unsandbox_get_languages(NULL, NULL);
if (langs) {
for (size_t i = 0; i < langs->count; i++) {
printf("%s\n", langs->languages[i]);
}
unsandbox_free_languages(langs);
}
```
### Credential Management
#### 4-Tier Priority System
Credentials are resolved in this order:
1. **Function arguments** - `unsandbox_execute("python", code, "key", "secret")`
2. **Environment variables** - `UNSANDBOX_PUBLIC_KEY`, `UNSANDBOX_SECRET_KEY`
3. **Home directory** - `~/.unsandbox/accounts.csv` (line 0, or `UNSANDBOX_ACCOUNT=N`)
4. **Current directory** - `./accounts.csv` (same format)
#### accounts.csv Format
```csv
public_key_1,secret_key_1
public_key_2,secret_key_2
public_key_3,secret_key_3
```
#### Usage Examples
```bash
# Option 1: Environment variables
export UNSANDBOX_PUBLIC_KEY="unsb-pk-..."
export UNSANDBOX_SECRET_KEY="unsb-sk-..."
./myapp
# Option 2: Config file
mkdir -p ~/.unsandbox
echo "unsb-pk-...,unsb-sk-..." > ~/.unsandbox/accounts.csv
chmod 600 ~/.unsandbox/accounts.csv
./myapp
# Option 3: Multiple accounts (select with UNSANDBOX_ACCOUNT)
echo "unsb-pk-1,unsb-sk-1" > ~/.unsandbox/accounts.csv
echo "unsb-pk-2,unsb-sk-2" >> ~/.unsandbox/accounts.csv
UNSANDBOX_ACCOUNT=1 ./myapp # Uses account 2 (0-indexed)
# Option 4: Inline (least secure)
./myapp --public-key=unsb-pk-... --secret-key=unsb-sk-...
```
#### Manual Credential Resolution
```c
char *public_key = NULL, *secret_key = NULL;
if (unsandbox_resolve_credentials(&public_key, &secret_key, NULL, NULL) == 0) {
// Use public_key and secret_key
unsandbox_result_t *result = unsandbox_execute("python", code,
public_key, secret_key);
// Clean up
free(public_key);
free(secret_key);
unsandbox_free_result(result);
}
```
### Memory Management
**Always clean up allocated memory:**
```c
void unsandbox_free_result(unsandbox_result_t *result);
void unsandbox_free_job(unsandbox_job_t *job);
void unsandbox_free_job_list(unsandbox_job_list_t *jobs);
void unsandbox_free_languages(unsandbox_languages_t *langs);
void unsandbox_free_quota(unsandbox_quota_t *quota);
```
### Utility Functions
#### `unsandbox_last_error()`
Get the last error message from a failed operation.
```c
unsandbox_result_t *result = unsandbox_execute("python", "code", NULL, NULL);
if (!result) {
printf("Error: %s\n", unsandbox_last_error());
}
```
#### `unsandbox_health_check()`
Check if the API is available.
```c
int status = unsandbox_health_check();
// Returns: 1 = available, 0 = unavailable, -1 = error
if (status == 1) {
printf("API is up\n");
}
```
#### `unsandbox_version()`
Get SDK version.
```c
printf("SDK version: %s\n", unsandbox_version());
```
## Supported Languages (50+)
**Interpreted:** Python, JavaScript, TypeScript, Ruby, PHP, Bash, Perl, Lua, R, Clojure, CommonLisp, Elixir, Erlang, Groovy, Idris2, Julia, Nim, Raku, Scheme, Tcl, Dart, Deno, Crystal, Kotlin
**Compiled:** C, C++, Go, Rust, Java, C#, F#, Haskell, OCaml, Cobol, D, Fortran, Odin, Pascal, V, Zig, Objective-C
**Other:** Prolog, Forth, WASM (C/C++/Rust/Zig/Go via Emscripten)
## Error Handling
All functions return NULL or negative values on error. Check `unsandbox_last_error()` for details:
```c
unsandbox_result_t *result = unsandbox_execute("python", "code", NULL, NULL);
if (!result) {
fprintf(stderr, "Error: %s\n", unsandbox_last_error());
return 1;
}
// Check execution errors
if (result->exit_code != 0) {
fprintf(stderr, "Execution failed (exit %d): %s\n",
result->exit_code, result->stderr);
}
unsandbox_free_result(result);
```
## Authentication
Requests are authenticated using HMAC-SHA256:
```
Authorization: Bearer <public_key>
X-Timestamp: <unix_seconds>
X-Signature: HMAC-SHA256(secret_key, "timestamp:METHOD:path:body")
```
The SDK handles this automatically.
## Testing
```bash
# Run all tests
make test
# Test specific functionality
make test-library
# Run examples
./examples/hello_world
./examples/fibonacci
```
## Performance
- **Synchronous execution:** 50-200ms (Python/Bash) to 5-30s (JVM languages)
- **Asynchronous execution:** ~50ms allocation + background execution
- **Polling backoff:** 300ms → 450ms → 700ms → 900ms → ... → 2000ms (capped)
## Limitations
- Max request body: 1MB per execution
- Max concurrent jobs per API key: Based on subscription tier
- Timeout: 30 seconds per request
- JSON parsing is simplified (use a real JSON library for complex responses)
## Troubleshooting
### "No credentials found"
Set credentials via environment, config file, or function arguments:
```bash
export UNSANDBOX_PUBLIC_KEY="your-key"
export UNSANDBOX_SECRET_KEY="your-secret"
./myapp
```
### "HTTP 401 Unauthorized"
Check your credentials are valid:
```bash
curl -H "Authorization: Bearer $UNSANDBOX_PUBLIC_KEY" https://api.unsandbox.com/health
```
### Compilation errors
Ensure dependencies are installed:
```bash
sudo apt install libcurl4-openssl-dev libssl-dev
```
Check include paths:
```bash
gcc -I/usr/include -c src/unsandbox.c
```
## License
PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
Use freely for any purpose.
## Examples Directory
- `hello_world.c` - Basic synchronous execution
- `fibonacci.c` - Asynchronous execution with polling
Compile examples:
```bash
cd examples
make -C .. examples # Or:
gcc -o hello_world hello_world.c ../src/unsandbox.c -I../src -lcurl -lssl -lcrypto
./hello_world
```
## See Also
- Python SDK: `../python/sync/src/un.py`
- Go SDK: `../go/sync/src/un.go`
- API Documentation: `https://api.unsandbox.com/`

View file

@ -0,0 +1,566 @@
# Unsandbox C SDK Examples
This directory contains practical examples demonstrating core functionality of the unsandbox C library.
## Examples Overview
### 1. hello_world.c - Simple Execute Example
A minimal example showing basic setup and execution patterns.
**What it demonstrates:**
- Loading credentials from environment variables
- Preparing code for execution
- Basic error handling
- Memory management patterns
- API structure overview
**Topics:**
- `UNSANDBOX_PUBLIC_KEY` and `UNSANDBOX_SECRET_KEY` environment variables
- `execute_code()` function signature
- Return value handling (malloc'd strings)
- Safe memory cleanup with `free()`
**Compile:**
```bash
gcc -o hello_world hello_world.c -I../../.. -lcurl -lwebsockets -lssl -lcrypto
```
**Run:**
```bash
export UNSANDBOX_PUBLIC_KEY=unsb-pk-xxxxx
export UNSANDBOX_SECRET_KEY=unsb-sk-xxxxx
./hello_world
```
### 2. fibonacci.c - Computation Example
Demonstrates executing computational code across multiple programming languages.
**What it demonstrates:**
- Same algorithm in Python, JavaScript, Go, and Rust
- Execution timing with `clock_gettime()`
- Multi-language support
- Timeout handling
- Error detection patterns
**Topics:**
- `execute_code()` for synchronous execution
- Language auto-detection from file extensions
- Measuring execution time
- Timeout detection and handling
- Runtime error vs output distinction
**Compile:**
```bash
gcc -o fibonacci fibonacci.c -I../../.. -lcurl -lwebsockets -lssl -lcrypto
```
**Run:**
```bash
export UNSANDBOX_PUBLIC_KEY=unsb-pk-xxxxx
export UNSANDBOX_SECRET_KEY=unsb-sk-xxxxx
./fibonacci
```
**Code examples included:**
- Python recursive fibonacci
- JavaScript functional fibonacci
- Go compiled fibonacci
- Rust zero-cost abstraction fibonacci
### 3. error_handling.c - Proper Error Handling Patterns
Comprehensive error handling demonstration with categorized error types.
**What it demonstrates:**
- Error classification (auth, rate-limit, timeout, runtime, server)
- Error type detection from responses
- Retry strategies with exponential backoff
- Transient vs permanent errors
- Error logging patterns
**Topics:**
- `ErrorType` enumeration
- HTTP error codes (401, 429, 500)
- Response content parsing
- Automatic retry logic
- Timeout detection in output
**Compile:**
```bash
gcc -o error_handling error_handling.c -I../../.. -lcurl -lwebsockets -lssl -lcrypto
```
**Run:**
```bash
./error_handling
```
**Patterns demonstrated:**
1. **NULL check** - Detection of memory allocation failures
2. **HTTP error checking** - Parsing status codes from responses
3. **Retry logic** - Exponential backoff for rate limits
4. **Timeout detection** - Identifying killed processes
5. **Error logging** - Writing to log files for debugging
**Error types:**
- `ERROR_INVALID_CREDS` - 401 Unauthorized
- `ERROR_RATE_LIMITED` - 429 Too Many Requests
- `ERROR_TIMEOUT` - Execution exceeded time limit
- `ERROR_RUNTIME_ERROR` - Exception in user code
- `ERROR_SERVER_ERROR` - 500+ server errors
- `ERROR_NETWORK` - Connection failed
- `ERROR_OUT_OF_MEMORY` - malloc() failed
### 4. credentials.c - Credential Loading from 4 Sources
Shows how to load credentials with priority ordering.
**What it demonstrates:**
- Credential priority ordering
- All four credential sources
- Account selection and management
- Config file format
- Security best practices
**Topics:**
- CLI flags: `-p` (public key) and `-k` (secret key)
- Environment variables: `UNSANDBOX_PUBLIC_KEY`, `UNSANDBOX_SECRET_KEY`
- Config file: `~/.unsandbox/accounts.csv`
- Account selection: `--account N` and `UNSANDBOX_ACCOUNT=N`
**Compile:**
```bash
gcc -o credentials credentials.c -I../../.. -lcurl -lwebsockets -lssl -lcrypto
```
**Run:**
```bash
./credentials
```
**Credential sources (highest to lowest priority):**
1. **CLI Flags**
```bash
un -p unsb-pk-xxxxx -k unsb-sk-xxxxx script.py
```
2. **Environment Variables**
```bash
export UNSANDBOX_PUBLIC_KEY=unsb-pk-xxxxx
export UNSANDBOX_SECRET_KEY=unsb-sk-xxxxx
./un script.py
```
3. **Config File**
```bash
~/.unsandbox/accounts.csv
# Format: public_key,secret_key (one per line)
un --account 0 script.py # Use first account
```
4. **Fallback (Error)**
```
Error: API credentials required
```
**Setup config file:**
```bash
mkdir -p ~/.unsandbox
cat > ~/.unsandbox/accounts.csv << 'EOF'
unsb-pk-account1,unsb-sk-account1
unsb-pk-account2,unsb-sk-account2
EOF
chmod 600 ~/.unsandbox/accounts.csv
```
## Common Compilation Patterns
### Standard compile (requires un.c)
```bash
gcc -o example example.c -I../../.. \
-lcurl -lwebsockets -lssl -lcrypto
```
### With optimization
```bash
gcc -O2 -o example example.c -I../../.. \
-lcurl -lwebsockets -lssl -lcrypto
```
### With debugging symbols
```bash
gcc -g -o example example.c -I../../.. \
-lcurl -lwebsockets -lssl -lcrypto
```
### With all warnings
```bash
gcc -Wall -Wextra -o example example.c -I../../.. \
-lcurl -lwebsockets -lssl -lcrypto
```
## Dependencies
Required libraries:
```bash
apt install build-essential libcurl4-openssl-dev libwebsockets-dev libssl-dev
```
## Key Patterns
### Pattern 1: Safe memory allocation
```c
char *result = execute_code(language, code, pk, sk);
if (!result) {
fprintf(stderr, "Failed to allocate result\n");
return 1;
}
// Use result...
free(result); // Always free!
```
### Pattern 2: Error detection
```c
char *result = execute_code(language, code, pk, sk);
if (!result) {
return 1;
}
if (strstr(result, "error") || strstr(result, "Error")) {
fprintf(stderr, "Execution error: %s\n", result);
free(result);
return 1;
}
printf("Output: %s\n", result);
free(result);
```
### Pattern 3: Retry with backoff
```c
const int MAX_RETRIES = 3;
const int RETRY_DELAY_MS = 1000;
for (int attempt = 0; attempt < MAX_RETRIES; attempt++) {
char *result = execute_code(language, code, pk, sk);
if (result) {
printf("Output: %s\n", result);
free(result);
return 0;
}
int backoff = RETRY_DELAY_MS * (1 << attempt);
usleep(backoff * 1000);
}
fprintf(stderr, "Failed after %d attempts\n", MAX_RETRIES);
return 1;
```
### Pattern 4: Async execution
```c
// Start async execution
char *job_id = execute_async(language, code, pk, sk);
if (!job_id) {
fprintf(stderr, "Failed to start async execution\n");
return 1;
}
printf("Job ID: %s\n", job_id);
// Poll for completion
sleep(1); // Wait before checking
char *result = wait_for_job(job_id, pk, sk);
if (result) {
printf("Result: %s\n", result);
free(result);
}
free(job_id);
```
### Pattern 5: Timeout handling
```c
struct timespec start, end;
clock_gettime(CLOCK_MONOTONIC, &start);
char *result = execute_code(language, code, pk, sk);
clock_gettime(CLOCK_MONOTONIC, &end);
long elapsed_ms = (end.tv_sec - start.tv_sec) * 1000 +
(end.tv_nsec - start.tv_nsec) / 1000000;
if (elapsed_ms > 300000) { // 300 seconds
fprintf(stderr, "Execution took %ld ms (likely timed out)\n", elapsed_ms);
}
if (result) {
printf("Output: %s\n", result);
free(result);
}
```
## API Reference
### Synchronous execution
```c
char *execute_code(
const char *language,
const char *code,
const char *public_key,
const char *secret_key
);
```
Returns: JSON response string (must be freed by caller) or NULL on failure
### Asynchronous execution
```c
char *execute_async(
const char *language,
const char *code,
const char *public_key,
const char *secret_key
);
```
Returns: Job ID string (must be freed) or NULL on failure
### Get job status
```c
char *get_job(
const char *job_id,
const char *public_key,
const char *secret_key
);
```
Returns: JSON status (must be freed) or NULL on failure
### Wait for completion
```c
char *wait_for_job(
const char *job_id,
const char *public_key,
const char *secret_key
);
```
Returns: Final result (must be freed) or NULL on failure
### Cancel job
```c
char *cancel_job(
const char *job_id,
const char *public_key,
const char *secret_key
);
```
Returns: Cancellation response (must be freed) or NULL on failure
### List jobs
```c
char *list_jobs(
const char *public_key,
const char *secret_key
);
```
Returns: JSON job list (must be freed) or NULL on failure
### Get supported languages
```c
char *get_languages(
const char *public_key,
const char *secret_key
);
```
Returns: JSON language list (must be freed) or NULL on failure
### Language detection
```c
const char *detect_language(const char *filename);
```
Returns: Language name (e.g., "python", "javascript") or NULL
## Environment Variables
### Required for execution
- `UNSANDBOX_PUBLIC_KEY` - API public key (unsb-pk-xxxxx)
- `UNSANDBOX_SECRET_KEY` - API secret key (unsb-sk-xxxxx)
### Optional
- `UNSANDBOX_ACCOUNT` - Account index in config file (default: 0)
## Configuration File
Location: `~/.unsandbox/accounts.csv`
Format:
```
unsb-pk-account1,unsb-sk-account1
unsb-pk-account2,unsb-sk-account2
unsb-pk-account3,unsb-sk-account3
```
Permissions: Should be `600` (readable only by owner)
## Error Codes
| Code | HTTP Status | Meaning | Action |
|------|-----------|---------|--------|
| 401 | Unauthorized | Invalid credentials | Check API keys |
| 429 | Too Many Requests | Rate limited | Implement backoff |
| 500+ | Server Error | Server issue | Retry later |
| Timeout | N/A | Execution time exceeded | Optimize code |
| OOM | N/A | Out of memory | Reduce input size |
## Memory Management
**IMPORTANT**: All functions returning `char *` allocate memory with `malloc()`.
You must free this memory when done:
```c
char *result = execute_code(...);
if (result) {
// Use result...
free(result); // MUST do this!
}
```
Failure to free will cause memory leaks in long-running applications.
## Testing
Compile all examples:
```bash
gcc -o hello_world hello_world.c -I../../.. -lcurl -lwebsockets -lssl -lcrypto
gcc -o fibonacci fibonacci.c -I../../.. -lcurl -lwebsockets -lssl -lcrypto
gcc -o error_handling error_handling.c -I../../.. -lcurl -lwebsockets -lssl -lcrypto
gcc -o credentials credentials.c -I../../.. -lcurl -lwebsockets -lssl -lcrypto
```
Run examples:
```bash
export UNSANDBOX_PUBLIC_KEY=unsb-pk-xxxxx
export UNSANDBOX_SECRET_KEY=unsb-sk-xxxxx
./hello_world
./fibonacci
./error_handling
./credentials
```
## Security Notes
1. **Never hardcode credentials** - Always use environment variables or config files
2. **Protect config files** - Use `chmod 600 ~/.unsandbox/accounts.csv`
3. **Rotate keys regularly** - Old keys should be revoked and removed
4. **Use separate credentials** - Dev, staging, and production should have different keys
5. **Don't log secrets** - Never print API keys to logs or stdout
6. **Clean shell history** - Remove entries with credentials: `history -c`
## Example Workflows
### Web server integration
```c
// In request handler
const char *language = "python";
const char *code = user_submitted_code;
const char *pk = getenv("UNSANDBOX_PUBLIC_KEY");
const char *sk = getenv("UNSANDBOX_SECRET_KEY");
char *result = execute_code(language, code, pk, sk);
if (result) {
send_response(200, result);
free(result);
} else {
send_response(500, "Execution failed");
}
```
### Batch processing
```c
// Process many files
for (int i = 0; i < file_count; i++) {
char *code = read_file(files[i]);
char *result = execute_code("python", code, pk, sk);
if (result) {
printf("File %s: %s\n", files[i], result);
free(result);
} else {
fprintf(stderr, "Failed: %s\n", files[i]);
}
free(code);
}
```
### Async job management
```c
// Start job
char *job_id = execute_async("python", code, pk, sk);
if (job_id) {
// Store job_id in database
// Later, check status periodically
char *status = get_job(job_id, pk, sk);
if (status) {
printf("Status: %s\n", status);
free(status);
}
free(job_id);
}
```
## Troubleshooting
### "API credentials required"
Set environment variables:
```bash
export UNSANDBOX_PUBLIC_KEY=unsb-pk-xxxxx
export UNSANDBOX_SECRET_KEY=unsb-sk-xxxxx
```
### "401 Unauthorized"
Check that API keys are correct:
```bash
echo $UNSANDBOX_PUBLIC_KEY
echo $UNSANDBOX_SECRET_KEY
```
### "429 Rate limited"
Implement exponential backoff in retry logic (see error_handling.c)
### Memory leaks
Use valgrind to check:
```bash
valgrind --leak-check=full ./example
```
### Compilation errors
Ensure dependencies are installed:
```bash
apt install libcurl4-openssl-dev libwebsockets-dev libssl-dev
```
## Further Reading
- See `../Makefile` for compilation and testing infrastructure
- See `../tests/test_library.c` for unit test patterns
- See `../../un.c` for full library implementation
- Visit https://unsandbox.com for API documentation

View file

@ -0,0 +1,392 @@
/*
* credentials.c - Credential loading from 4 sources
*
* Demonstrates how the un.c library loads credentials from multiple sources
* with priority ordering. This example shows:
* - Priority ordering (highest to lowest)
* - Environment variables
* - CLI flags
* - Config file (~/.unsandbox/accounts.csv)
* - Account selection
*
* Compile:
* gcc -o credentials credentials.c -I../../.. -lcurl -lwebsockets -lssl -lcrypto
*
* Run:
* ./credentials
*
* Or with specific credentials:
* ./credentials --public unsb-pk-xxxxx --secret unsb-sk-xxxxx
*
* Expected output:
* Demonstrates credential loading from all 4 sources
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <pwd.h>
/* ============================================================================
* Helper functions
* ============================================================================ */
/* Print a value, masking sensitive info after first 8 characters */
static void print_masked(const char *label, const char *value) {
if (!value || strlen(value) == 0) {
printf("%s: (not set)\n", label);
return;
}
size_t len = strlen(value);
printf("%s: ", label);
if (len <= 8) {
printf("****\n");
} else {
printf("%.8s...****\n", value);
}
}
/* Check if a file exists */
static int file_exists(const char *path) {
struct stat sb;
return (stat(path, &sb) == 0 && S_ISREG(sb.st_mode));
}
/* Get home directory */
static const char* get_home_dir(void) {
const char *home = getenv("HOME");
if (home) return home;
struct passwd *pw = getpwuid(getuid());
if (pw) return pw->pw_dir;
return ".";
}
/* ============================================================================
* Credential priority demonstration
* ============================================================================ */
void explain_priority(void) {
printf("Credential Priority (Highest to Lowest)\n");
printf("=======================================\n\n");
printf("Priority 1: CLI Flags (HIGHEST)\n");
printf(" un -p PUBLIC_KEY -k SECRET_KEY\n");
printf(" These always take precedence if provided.\n\n");
printf("Priority 2: Environment Variables\n");
printf(" UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY\n");
printf(" Used if CLI flags not provided.\n\n");
printf("Priority 3: Config File\n");
printf(" ~/.unsandbox/accounts.csv\n");
printf(" Format: public_key,secret_key (one per line)\n");
printf(" Account selection: --account N (0-based, default: 0)\n");
printf(" Or: UNSANDBOX_ACCOUNT environment variable\n\n");
printf("Priority 4: Default (LOWEST)\n");
printf(" If none of the above are set, execution fails.\n");
printf(" \"Error: API credentials required\"\n\n");
}
/* ============================================================================
* Source 1: CLI Flags
* ============================================================================ */
void explain_cli_flags(void) {
printf("\nSource 1: CLI Flags\n");
printf("===================\n");
printf("Command line format:\n");
printf(" un -p unsb-pk-xxxxx -k unsb-sk-xxxxx\n");
printf(" un --public unsb-pk-xxxxx --secret unsb-sk-xxxxx\n\n");
printf("Example usage in code:\n");
printf(" int main(int argc, char *argv[]) {\n");
printf(" const char *public_key = NULL;\n");
printf(" const char *secret_key = NULL;\n");
printf("\n");
printf(" // Parse command line\n");
printf(" for (int i = 1; i < argc; i++) {\n");
printf(" if ((strcmp(argv[i], \"-p\") == 0 ||\n");
printf(" strcmp(argv[i], \"--public\") == 0) &&\n");
printf(" i + 1 < argc) {\n");
printf(" public_key = argv[++i];\n");
printf(" }\n");
printf(" if ((strcmp(argv[i], \"-k\") == 0 ||\n");
printf(" strcmp(argv[i], \"--secret\") == 0) &&\n");
printf(" i + 1 < argc) {\n");
printf(" secret_key = argv[++i];\n");
printf(" }\n");
printf(" }\n");
printf("\n");
printf(" // Check if both provided\n");
printf(" if (!public_key || !secret_key) {\n");
printf(" // Fall back to next priority\n");
printf(" }\n");
printf(" }\n\n");
printf("Advantages:\n");
printf(" - Highest priority (can't be accidentally overridden)\n");
printf(" - Unique credentials per invocation\n");
printf(" - Good for automation scripts\n\n");
printf("Disadvantages:\n");
printf(" - Credentials visible in process list (if not careful)\n");
printf(" - Must pass on every invocation\n");
}
/* ============================================================================
* Source 2: Environment Variables
* ============================================================================ */
void explain_env_vars(void) {
printf("\nSource 2: Environment Variables\n");
printf("================================\n");
const char *pk = getenv("UNSANDBOX_PUBLIC_KEY");
const char *sk = getenv("UNSANDBOX_SECRET_KEY");
const char *account = getenv("UNSANDBOX_ACCOUNT");
printf("Environment variables:\n");
printf(" UNSANDBOX_PUBLIC_KEY ");
print_masked("", pk);
printf(" UNSANDBOX_SECRET_KEY ");
print_masked("", sk);
printf(" UNSANDBOX_ACCOUNT %s (default: 0)\n\n", account ? account : "(not set)");
printf("How to set:\n");
printf(" export UNSANDBOX_PUBLIC_KEY=unsb-pk-xxxxx\n");
printf(" export UNSANDBOX_SECRET_KEY=unsb-sk-xxxxx\n");
printf(" export UNSANDBOX_ACCOUNT=0 # optional\n\n");
printf("Or in one line:\n");
printf(" UNSANDBOX_PUBLIC_KEY=xxx UNSANDBOX_SECRET_KEY=yyy ./un script.py\n\n");
printf("How to read in C:\n");
printf(" const char *pk = getenv(\"UNSANDBOX_PUBLIC_KEY\");\n");
printf(" const char *sk = getenv(\"UNSANDBOX_SECRET_KEY\");\n");
printf(" const char *account_str = getenv(\"UNSANDBOX_ACCOUNT\");\n");
printf(" int account_idx = account_str ? atoi(account_str) : 0;\n\n");
printf("Advantages:\n");
printf(" - Don't appear in process list\n");
printf(" - Can persist in shell session\n");
printf(" - Standard Unix convention\n\n");
printf("Disadvantages:\n");
printf(" - Can be inherited by child processes\n");
printf(" - Visible to debuggers\n");
}
/* ============================================================================
* Source 3: Config File
* ============================================================================ */
void explain_config_file(void) {
printf("\nSource 3: Config File (~/.unsandbox/accounts.csv)\n");
printf("==================================================\n");
const char *home = get_home_dir();
char config_path[512];
snprintf(config_path, sizeof(config_path), "%s/.unsandbox/accounts.csv", home);
printf("Config file location:\n");
printf(" %s\n\n", config_path);
printf("File format (one account per line):\n");
printf(" unsb-pk-account1,unsb-sk-account1\n");
printf(" unsb-pk-account2,unsb-sk-account2\n");
printf(" unsb-pk-account3,unsb-sk-account3\n\n");
printf("How to create:\n");
printf(" mkdir -p ~/.unsandbox\n");
printf(" cat > ~/.unsandbox/accounts.csv << 'EOF'\n");
printf(" unsb-pk-xxxxx,unsb-sk-xxxxx\n");
printf(" unsb-pk-yyyyy,unsb-sk-yyyyy\n");
printf(" EOF\n");
printf(" chmod 600 ~/.unsandbox/accounts.csv\n\n");
printf("How to select account:\n");
printf(" un --account 0 script.py # First account (default)\n");
printf(" un --account 1 script.py # Second account\n");
printf(" UNSANDBOX_ACCOUNT=2 un script.py # Environment variable\n\n");
printf("Current config file status:\n");
if (file_exists(config_path)) {
printf(" [EXISTS] %s\n", config_path);
printf(" Note: Contents not shown for security\n");
} else {
printf(" [MISSING] %s\n", config_path);
printf(" To create: mkdir -p ~/.unsandbox\n");
printf(" echo 'pk,sk' > %s\n", config_path);
printf(" chmod 600 %s\n", config_path);
}
printf("\nHow to read in C:\n");
printf(" FILE *f = fopen(config_path, \"r\");\n");
printf(" if (f) {\n");
printf(" char line[256];\n");
printf(" int account_num = 0;\n");
printf(" while (fgets(line, sizeof(line), f)) {\n");
printf(" if (account_num == account_idx) {\n");
printf(" char *pk = strtok(line, \",\");\n");
printf(" char *sk = strtok(NULL, \"\\\\n\");\n");
printf(" // Use pk and sk\n");
printf(" break;\n");
printf(" }\n");
printf(" account_num++;\n");
printf(" }\n");
printf(" fclose(f);\n");
printf(" }\n\n");
printf("Advantages:\n");
printf(" - Store multiple accounts\n");
printf(" - Secrets not in shell history\n");
printf(" - Can be version-controlled (separately)\n");
printf(" - Standard configuration file approach\n\n");
printf("Disadvantages:\n");
printf(" - File must exist at specific location\n");
printf(" - Must remember account index\n");
printf(" - Requires explicit setup\n");
printf(" - File permissions critical (should be 600)\n");
}
/* ============================================================================
* Source 4: Fallback/Error
* ============================================================================ */
void explain_fallback(void) {
printf("\nSource 4: Fallback (No Credentials)\n");
printf("====================================\n");
printf("If no credentials are found from sources 1-3:\n");
printf(" Error: API credentials required.\n");
printf(" Set UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY env vars, or\n");
printf(" Use -p PUBLIC_KEY -k SECRET_KEY flags, or\n");
printf(" Create ~/.unsandbox/accounts.csv with: public_key,secret_key\n\n");
printf("Exit code: 1 (failure)\n\n");
}
/* ============================================================================
* Practical examples
* ============================================================================ */
void show_practical_examples(void) {
printf("\nPractical Examples\n");
printf("==================\n\n");
printf("Example 1: One-off execution\n");
printf(" UNSANDBOX_PUBLIC_KEY=unsb-pk-xxx UNSANDBOX_SECRET_KEY=unsb-sk-xxx \\\n");
printf(" ./un script.py\n\n");
printf("Example 2: Development session\n");
printf(" export UNSANDBOX_PUBLIC_KEY=unsb-pk-xxx\n");
printf(" export UNSANDBOX_SECRET_KEY=unsb-sk-xxx\n");
printf(" ./un script1.py\n");
printf(" ./un script2.py\n");
printf(" ./un script3.py\n\n");
printf("Example 3: Multiple accounts\n");
printf(" mkdir -p ~/.unsandbox\n");
printf(" echo 'unsb-pk-account1,unsb-sk-account1' >> ~/.unsandbox/accounts.csv\n");
printf(" echo 'unsb-pk-account2,unsb-sk-account2' >> ~/.unsandbox/accounts.csv\n");
printf(" ./un --account 0 script.py # Uses first account\n");
printf(" ./un --account 1 script.py # Uses second account\n\n");
printf("Example 4: Override with CLI flags\n");
printf(" export UNSANDBOX_PUBLIC_KEY=unsb-pk-env\n");
printf(" export UNSANDBOX_SECRET_KEY=unsb-sk-env\n");
printf(" ./un -p unsb-pk-cli -k unsb-sk-cli script.py # Uses CLI keys\n\n");
printf("Example 5: Automation in scripts\n");
printf(" #!/bin/bash\n");
printf(" export UNSANDBOX_PUBLIC_KEY=\"$(aws secretsmanager ...)\"\n");
printf(" export UNSANDBOX_SECRET_KEY=\"$(aws secretsmanager ...)\"\n");
printf(" ./un process_data.py\n\n");
}
/* ============================================================================
* Security best practices
* ============================================================================ */
void show_security_practices(void) {
printf("\nSecurity Best Practices\n");
printf("=======================\n\n");
printf("DO:\n");
printf(" ✓ Use environment variables for credentials\n");
printf(" ✓ Set config file permissions to 600 (chmod 600)\n");
printf(" ✓ Store secrets in secure vaults (AWS Secrets Manager, etc.)\n");
printf(" ✓ Rotate API keys regularly\n");
printf(" ✓ Use separate keys for different environments (dev/prod)\n");
printf(" ✓ Delete credentials from shell history: history -c\n");
printf(" ✓ Audit who has access to credentials\n\n");
printf("DON'T:\n");
printf(" ✗ Hardcode credentials in source code\n");
printf(" ✗ Commit credentials to version control\n");
printf(" ✗ Log credentials to console/files\n");
printf(" ✗ Pass credentials on command line in production\n");
printf(" ✗ Store credentials in plain text files (except ~/.unsandbox/accounts.csv)\n");
printf(" ✗ Share credentials between team members\n");
printf(" ✗ Use same credentials for dev and production\n\n");
}
/* ============================================================================
* Main demonstration
* ============================================================================ */
int main(int argc, char *argv[]) {
printf("Unsandbox Credential Management\n");
printf("================================\n\n");
/* Show priority */
explain_priority();
/* Show each source */
explain_cli_flags();
explain_env_vars();
explain_config_file();
explain_fallback();
/* Show practical examples */
show_practical_examples();
/* Show security practices */
show_security_practices();
printf("Current System State\n");
printf("====================\n\n");
printf("Environment variables:\n");
printf(" UNSANDBOX_PUBLIC_KEY: %s\n",
getenv("UNSANDBOX_PUBLIC_KEY") ? "(set)" : "(not set)");
printf(" UNSANDBOX_SECRET_KEY: %s\n",
getenv("UNSANDBOX_SECRET_KEY") ? "(set)" : "(not set)");
printf(" UNSANDBOX_ACCOUNT: %s\n",
getenv("UNSANDBOX_ACCOUNT") ? getenv("UNSANDBOX_ACCOUNT") : "(not set)");
char config_path[512];
snprintf(config_path, sizeof(config_path), "%s/.unsandbox/accounts.csv",
get_home_dir());
printf("\nConfig file:\n");
printf(" %s: %s\n", config_path,
file_exists(config_path) ? "exists" : "does not exist");
printf("\nTo use unsandbox, set credentials via one of these methods:\n");
printf(" 1. export UNSANDBOX_PUBLIC_KEY=... UNSANDBOX_SECRET_KEY=...\n");
printf(" 2. Create ~/.unsandbox/accounts.csv\n");
printf(" 3. Pass -p and -k flags to the command\n");
return 0;
}

View file

@ -0,0 +1,418 @@
/*
* error_handling.c - Proper error handling patterns
*
* Demonstrates comprehensive error handling patterns for unsandbox code execution.
* This example shows:
* - HTTP error responses (401, 429, 500)
* - Runtime errors in executed code
* - Timeout detection
* - Memory leak prevention
* - Retry strategies
* - Logging errors
*
* Compile:
* gcc -o error_handling error_handling.c -I../../.. -lcurl -lwebsockets -lssl -lcrypto
*
* Run:
* export UNSANDBOX_PUBLIC_KEY=unsb-pk-xxxxx
* export UNSANDBOX_SECRET_KEY=unsb-sk-xxxxx
* ./error_handling
*
* Expected output:
* Demonstrates various error scenarios and proper handling
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stddef.h>
/* ============================================================================
* Error type definitions
* ============================================================================ */
typedef enum {
ERROR_NONE = 0,
ERROR_INVALID_CREDS = 1,
ERROR_RATE_LIMITED = 2,
ERROR_TIMEOUT = 3,
ERROR_RUNTIME_ERROR = 4,
ERROR_SERVER_ERROR = 5,
ERROR_NETWORK = 6,
ERROR_OUT_OF_MEMORY = 7,
ERROR_UNKNOWN = 8
} ErrorType;
typedef struct {
ErrorType type;
int http_code;
char *message;
char *details;
} ExecutionError;
/* ============================================================================
* Helper functions
* ============================================================================ */
/* Create a new error structure */
static ExecutionError* error_create(ErrorType type, int http_code,
const char *message, const char *details) {
ExecutionError *err = malloc(sizeof(ExecutionError));
if (!err) return NULL;
err->type = type;
err->http_code = http_code;
err->message = message ? strdup(message) : NULL;
err->details = details ? strdup(details) : NULL;
return err;
}
/* Free error structure */
static void error_free(ExecutionError *err) {
if (!err) return;
free(err->message);
free(err->details);
free(err);
}
/* Parse HTTP response to determine error type */
static ErrorType parse_error_type(const char *response, int http_code) {
if (!response) {
if (http_code >= 500) return ERROR_SERVER_ERROR;
if (http_code == 429) return ERROR_RATE_LIMITED;
if (http_code == 401) return ERROR_INVALID_CREDS;
return ERROR_UNKNOWN;
}
/* Check for specific error patterns in response */
if (strstr(response, "401") || strstr(response, "Unauthorized")) {
return ERROR_INVALID_CREDS;
}
if (strstr(response, "429") || strstr(response, "rate_limit")) {
return ERROR_RATE_LIMITED;
}
if (strstr(response, "timeout") || strstr(response, "Timeout")) {
return ERROR_TIMEOUT;
}
if (strstr(response, "Traceback") || strstr(response, "Error") ||
strstr(response, "Exception") || strstr(response, "error")) {
return ERROR_RUNTIME_ERROR;
}
if (http_code >= 500) {
return ERROR_SERVER_ERROR;
}
return ERROR_UNKNOWN;
}
/* Print human-readable error message */
static void error_print(const ExecutionError *err) {
if (!err) {
printf("Error: NULL error object\n");
return;
}
printf("Error [%d]: ", err->http_code);
switch (err->type) {
case ERROR_INVALID_CREDS:
printf("Invalid Credentials\n");
printf(" Your API key or secret key is incorrect.\n");
printf(" Check UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY.\n");
break;
case ERROR_RATE_LIMITED:
printf("Rate Limited\n");
printf(" You've exceeded your rate limit.\n");
printf(" Wait before retrying. Check response for reset time.\n");
break;
case ERROR_TIMEOUT:
printf("Timeout\n");
printf(" Code execution took too long and was terminated.\n");
printf(" Optimize your code or increase timeout if available.\n");
break;
case ERROR_RUNTIME_ERROR:
printf("Runtime Error\n");
printf(" Your code raised an exception or error.\n");
printf(" Check the code logic.\n");
break;
case ERROR_SERVER_ERROR:
printf("Server Error\n");
printf(" The unsandbox server encountered an error.\n");
printf(" This is typically temporary. Retry later.\n");
break;
case ERROR_NETWORK:
printf("Network Error\n");
printf(" Could not reach the unsandbox server.\n");
printf(" Check your internet connection.\n");
break;
case ERROR_OUT_OF_MEMORY:
printf("Out of Memory\n");
printf(" malloc() failed. System is out of memory.\n");
break;
case ERROR_NONE:
printf("No Error\n");
break;
case ERROR_UNKNOWN:
default:
printf("Unknown Error\n");
break;
}
if (err->message) {
printf(" Message: %s\n", err->message);
}
if (err->details) {
printf(" Details: %s\n", err->details);
}
}
/* ============================================================================
* Error handling patterns
* ============================================================================ */
/*
* Pattern 1: Check for NULL return (allocation failure)
*/
static void pattern_null_check(void) {
printf("\nPattern 1: Check for NULL (memory allocation failure)\n");
printf("========================================================\n");
printf("char *result = execute_code(language, code, pk, sk);\n");
printf("if (!result) {\n");
printf(" ExecutionError *err = error_create(\n");
printf(" ERROR_OUT_OF_MEMORY, 0,\n");
printf(" \"Failed to allocate result buffer\",\n");
printf(" \"malloc() returned NULL\"\n");
printf(" );\n");
printf(" error_print(err);\n");
printf(" error_free(err);\n");
printf(" return 1;\n");
printf("}\n");
}
/*
* Pattern 2: Check for HTTP error codes in response
*/
static void pattern_http_error_check(void) {
printf("\nPattern 2: Check for HTTP errors in response\n");
printf("=============================================\n");
printf("char *result = execute_code(language, code, pk, sk);\n");
printf("if (!result) {\n");
printf(" fprintf(stderr, \"Execution failed\\\\n\");\n");
printf(" return 1;\n");
printf("}\n");
printf("\n");
printf("ErrorType err_type = parse_error_type(result, 0);\n");
printf("if (err_type != ERROR_NONE) {\n");
printf(" ExecutionError *err = error_create(\n");
printf(" err_type, 0, \"Execution failed\", result\n");
printf(" );\n");
printf(" error_print(err);\n");
printf(" error_free(err);\n");
printf(" free(result);\n");
printf(" return 1;\n");
printf("}\n");
printf("\n");
printf("// Success: process result\n");
printf("printf(\"Output: %%s\\\\n\", result);\n");
printf("free(result);\n");
}
/*
* Pattern 3: Retry on transient errors
*/
static void pattern_retry_logic(void) {
printf("\nPattern 3: Retry logic for transient errors\n");
printf("==========================================\n");
printf("const int MAX_RETRIES = 3;\n");
printf("const int RETRY_DELAY_MS = 1000;\n");
printf("\n");
printf("for (int attempt = 0; attempt < MAX_RETRIES; attempt++) {\n");
printf(" char *result = execute_code(language, code, pk, sk);\n");
printf(" if (!result) break; // Success\n");
printf("\n");
printf(" ErrorType err_type = parse_error_type(result, 0);\n");
printf("\n");
printf(" if (err_type == ERROR_SERVER_ERROR ||\n");
printf(" err_type == ERROR_NETWORK) {\n");
printf(" // Transient error: retry\n");
printf(" printf(\"Retry %%d/%%d after %%dms...\\\\n\", \n");
printf(" attempt + 1, MAX_RETRIES, RETRY_DELAY_MS);\n");
printf(" free(result);\n");
printf(" usleep(RETRY_DELAY_MS * 1000);\n");
printf(" continue;\n");
printf(" }\n");
printf("\n");
printf(" if (err_type == ERROR_RATE_LIMITED) {\n");
printf(" // Rate limit: exponential backoff\n");
printf(" int backoff_ms = RETRY_DELAY_MS * (1 << attempt);\n");
printf(" printf(\"Rate limited, backoff %%dms...\\\\n\", backoff_ms);\n");
printf(" free(result);\n");
printf(" usleep(backoff_ms * 1000);\n");
printf(" continue;\n");
printf(" }\n");
printf("\n");
printf(" if (err_type == ERROR_INVALID_CREDS ||\n");
printf(" err_type == ERROR_RUNTIME_ERROR) {\n");
printf(" // Permanent error: don't retry\n");
printf(" ExecutionError *err = error_create(\n");
printf(" err_type, 0, \"Execution failed\", result\n");
printf(" );\n");
printf(" error_print(err);\n");
printf(" error_free(err);\n");
printf(" free(result);\n");
printf(" return 1;\n");
printf(" }\n");
printf("\n");
printf(" // Success\n");
printf(" printf(\"Output: %%s\\\\n\", result);\n");
printf(" free(result);\n");
printf(" return 0;\n");
printf("}\n");
printf("\n");
printf("fprintf(stderr, \"Failed after %%d attempts\\\\n\", MAX_RETRIES);\n");
printf("return 1;\n");
}
/*
* Pattern 4: Timeout detection
*/
static void pattern_timeout_detection(void) {
printf("\nPattern 4: Detect and handle timeouts\n");
printf("=====================================\n");
printf("char *result = execute_code(language, code, pk, sk);\n");
printf("if (!result) {\n");
printf(" fprintf(stderr, \"Execution failed\\\\n\");\n");
printf(" return 1;\n");
printf("}\n");
printf("\n");
printf("// Check for timeout indicators\n");
printf("if (strstr(result, \"timeout\") ||\n");
printf(" strstr(result, \"Timeout\") ||\n");
printf(" strstr(result, \"timed out\") ||\n");
printf(" strstr(result, \"killed\")) {\n");
printf(" ExecutionError *err = error_create(\n");
printf(" ERROR_TIMEOUT, 0,\n");
printf(" \"Code execution timed out\",\n");
printf(" result\n");
printf(" );\n");
printf(" error_print(err);\n");
printf(" error_free(err);\n");
printf(" free(result);\n");
printf(" return 1;\n");
printf("}\n");
printf("\n");
printf("printf(\"Output: %%s\\\\n\", result);\n");
printf("free(result);\n");
}
/*
* Pattern 5: Logging errors to file
*/
static void pattern_error_logging(void) {
printf("\nPattern 5: Log errors to file\n");
printf("=============================\n");
printf("FILE *log_file = fopen(\"execution.log\", \"a\");\n");
printf("if (!log_file) {\n");
printf(" perror(\"Failed to open log file\");\n");
printf(" return 1;\n");
printf("}\n");
printf("\n");
printf("char *result = execute_code(language, code, pk, sk);\n");
printf("if (!result) {\n");
printf(" fprintf(log_file,\n");
printf(" \"[ERROR] Execution failed at %%s\\\\n\",\n");
printf(" __func__);\n");
printf(" fclose(log_file);\n");
printf(" return 1;\n");
printf("}\n");
printf("\n");
printf("ErrorType err_type = parse_error_type(result, 0);\n");
printf("if (err_type != ERROR_NONE) {\n");
printf(" fprintf(log_file,\n");
printf(" \"[ERROR] Execution error: %%s\\\\n\",\n");
printf(" result);\n");
printf("}\n");
printf("\n");
printf("free(result);\n");
printf("fclose(log_file);\n");
}
/* ============================================================================
* Main demonstration
* ============================================================================ */
int main(void) {
printf("Unsandbox Error Handling Example\n");
printf("=================================\n");
/* Get credentials */
const char *public_key = getenv("UNSANDBOX_PUBLIC_KEY");
const char *secret_key = getenv("UNSANDBOX_SECRET_KEY");
if (!public_key || !secret_key) {
fprintf(stderr, "Error: Credentials not found\n");
fprintf(stderr, "Please set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY\n");
return 1;
}
printf("Credentials loaded\n\n");
/* Demonstrate error types */
printf("Error Types:\n");
printf("============\n");
ExecutionError *err;
err = error_create(ERROR_INVALID_CREDS, 401, "Invalid credentials", NULL);
printf("\n1. ");
error_print(err);
error_free(err);
err = error_create(ERROR_RATE_LIMITED, 429, "Rate limited", "Reset in 60 seconds");
printf("\n2. ");
error_print(err);
error_free(err);
err = error_create(ERROR_TIMEOUT, 0, "Execution timeout", "Killed after 300 seconds");
printf("\n3. ");
error_print(err);
error_free(err);
err = error_create(ERROR_RUNTIME_ERROR, 0, "Runtime error",
"Traceback: ZeroDivisionError: division by zero");
printf("\n4. ");
error_print(err);
error_free(err);
err = error_create(ERROR_SERVER_ERROR, 500, "Server error", NULL);
printf("\n5. ");
error_print(err);
error_free(err);
/* Show patterns */
pattern_null_check();
pattern_http_error_check();
pattern_retry_logic();
pattern_timeout_detection();
pattern_error_logging();
printf("\n\nKey Points:\n");
printf("===========\n");
printf("1. Always check return values for NULL\n");
printf("2. Parse error responses to determine error type\n");
printf("3. Distinguish transient vs permanent errors\n");
printf("4. Implement retry logic with exponential backoff\n");
printf("5. Detect timeouts by checking response content\n");
printf("6. Log errors for debugging\n");
printf("7. Always free allocated memory\n");
return 0;
}

View file

@ -0,0 +1,217 @@
/*
* fibonacci.c - Computation example
*
* Demonstrates executing computational code across multiple programming languages.
* This example shows:
* - Same algorithm in Python, JavaScript, Go, and Rust
* - Execution timing with `clock_gettime()`
* - Multi-language support
* - Timeout handling
* - Error detection patterns
*
* Compile:
* gcc -o fibonacci fibonacci.c -I../../.. -lcurl -lwebsockets -lssl -lcrypto
*
* Run:
* export UNSANDBOX_PUBLIC_KEY=unsb-pk-xxxxx
* export UNSANDBOX_SECRET_KEY=unsb-sk-xxxxx
* ./fibonacci
*
* Expected output:
* Computing fibonacci(10) = 55
* Computing fibonacci(20) = 6765
* Execution time: ~200ms
*
* Note: This example demonstrates multiple programming languages
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
/* ============================================================================
* Helper function: Calculate elapsed time in milliseconds
* ============================================================================ */
static long get_elapsed_ms(struct timespec start, struct timespec end) {
return (end.tv_sec - start.tv_sec) * 1000 +
(end.tv_nsec - start.tv_nsec) / 1000000;
}
/* ============================================================================
* Main demonstration
* ============================================================================ */
int main(void) {
printf("Unsandbox Fibonacci Computation Example\n");
printf("========================================\n\n");
/* Get credentials from environment */
const char *public_key = getenv("UNSANDBOX_PUBLIC_KEY");
const char *secret_key = getenv("UNSANDBOX_SECRET_KEY");
if (!public_key || !secret_key) {
fprintf(stderr, "Error: Credentials not found\n");
fprintf(stderr, "Please set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY\n");
return 1;
}
printf("Credentials loaded from environment\n\n");
/* ========================================================================
* Example 1: Python fibonacci
* ======================================================================== */
printf("Example 1: Python Fibonacci\n");
printf("---------------------------\n");
const char *python_code =
"def fib(n):\n"
" if n < 2:\n"
" return n\n"
" return fib(n-1) + fib(n-2)\n"
"\n"
"result = fib(10)\n"
"print(f'fibonacci(10) = {result}')\n";
printf("Code:\n%s\n", python_code);
printf("Expected: fibonacci(10) = 55\n");
printf("Note: To execute with un.c library:\n");
printf(" char *result = execute_code(\"python\", python_code, public_key, secret_key);\n");
printf(" if (result) {\n");
printf(" printf(\"Output: %%s\\\\n\", result);\n");
printf(" free(result);\n");
printf(" } else {\n");
printf(" fprintf(stderr, \"Execution failed\\\\n\");\n");
printf(" }\n\n");
/* ========================================================================
* Example 2: JavaScript fibonacci
* ======================================================================== */
printf("Example 2: JavaScript Fibonacci\n");
printf("--------------------------------\n");
const char *js_code =
"function fib(n) {\n"
" if (n < 2) return n;\n"
" return fib(n-1) + fib(n-2);\n"
"}\n"
"\n"
"console.log('fibonacci(10) = ' + fib(10));\n";
printf("Code:\n%s\n", js_code);
printf("Expected: fibonacci(10) = 55\n\n");
/* ========================================================================
* Example 3: Go fibonacci
* ======================================================================== */
printf("Example 3: Go Fibonacci\n");
printf("------------------------\n");
const char *go_code =
"package main\n"
"\n"
"import \"fmt\"\n"
"\n"
"func fib(n int) int {\n"
" if n < 2 {\n"
" return n\n"
" }\n"
" return fib(n-1) + fib(n-2)\n"
"}\n"
"\n"
"func main() {\n"
" fmt.Printf(\"fibonacci(10) = %d\\\\n\", fib(10))\n"
"}\n";
printf("Code:\n%s\n", go_code);
printf("Expected: fibonacci(10) = 55\n\n");
/* ========================================================================
* Example 4: Rust fibonacci
* ======================================================================== */
printf("Example 4: Rust Fibonacci\n");
printf("--------------------------\n");
const char *rust_code =
"fn fib(n: u32) -> u32 {\n"
" if n < 2 { n } else { fib(n-1) + fib(n-2) }\n"
"}\n"
"\n"
"fn main() {\n"
" println!(\"fibonacci(10) = {}\", fib(10));\n"
"}\n";
printf("Code:\n%s\n", rust_code);
printf("Expected: fibonacci(10) = 55\n\n");
/* ========================================================================
* Example 5: Larger computation
* ======================================================================== */
printf("Example 5: Larger Computation (fibonacci(20))\n");
printf("---------------------------------------------\n");
const char *large_code =
"def fib(n):\n"
" if n < 2:\n"
" return n\n"
" return fib(n-1) + fib(n-2)\n"
"\n"
"result = fib(20)\n"
"print(f'fibonacci(20) = {result}')\n";
printf("Code:\n%s\n", large_code);
printf("Expected: fibonacci(20) = 6765\n");
printf("Note: This may take 1-2 seconds due to recursion\n\n");
/* ========================================================================
* Pattern: Execute and measure time
* ======================================================================== */
printf("Pattern: Execution with timing\n");
printf("-------------------------------\n");
printf("struct timespec start, end;\n");
printf("clock_gettime(CLOCK_MONOTONIC, &start);\n");
printf("\n");
printf("char *result = execute_code(\"python\", large_code, public_key, secret_key);\n");
printf("\n");
printf("clock_gettime(CLOCK_MONOTONIC, &end);\n");
printf("long elapsed_ms = get_elapsed_ms(start, end);\n");
printf("\n");
printf("if (result) {\n");
printf(" printf(\"Output: %%s\\\\n\", result);\n");
printf(" printf(\"Execution time: %%ld ms\\\\n\", elapsed_ms);\n");
printf(" free(result);\n");
printf("}\n\n");
/* ========================================================================
* Pattern: Error handling for timeouts
* ======================================================================== */
printf("Pattern: Handling timeouts\n");
printf("---------------------------\n");
printf("// Code that might timeout:\n");
printf("const char *slow_code =\n");
printf(" \"import time\\\\n\"\n");
printf(" \"time.sleep(120) # 2 minutes - will timeout\\\\n\"\n");
printf(" \"print('done')\\\\n\";\n");
printf("\n");
printf("char *result = execute_code(\"python\", slow_code, public_key, secret_key);\n");
printf("if (!result) {\n");
printf(" fprintf(stderr, \"Execution failed (possibly timeout)\\\\n\");\n");
printf("} else if (strstr(result, \"timeout\") || strstr(result, \"killed\")) {\n");
printf(" fprintf(stderr, \"Execution timed out\\\\n\");\n");
printf(" free(result);\n");
printf("} else if (strstr(result, \"error\") || strstr(result, \"Error\")) {\n");
printf(" fprintf(stderr, \"Execution error: %%s\\\\n\", result);\n");
printf(" free(result);\n");
printf("} else {\n");
printf(" printf(\"Output: %%s\\\\n\", result);\n");
printf(" free(result);\n");
printf("}\n\n");
printf("Setup complete! You can now execute fibonacci calculations.\n");
printf("Key points:\n");
printf(" 1. Same code, multiple languages\n");
printf(" 2. Measure execution time with clock_gettime()\n");
printf(" 3. Handle timeouts and errors properly\n");
printf(" 4. Always free returned strings\n");
return 0;
}

View file

@ -0,0 +1,121 @@
/*
* hello_world.c - Simple execute example
*
* Demonstrates basic unsandbox code execution using the un.c library.
* This example shows:
* - How to set up credentials
* - How to execute simple code
* - How to check for errors
* - How to clean up resources
*
* Compile:
* gcc -o hello_world hello_world.c -I../../.. -lcurl -lwebsockets -lssl -lcrypto
*
* Run:
* export UNSANDBOX_PUBLIC_KEY=unsb-pk-xxxxx
* export UNSANDBOX_SECRET_KEY=unsb-sk-xxxxx
* ./hello_world
*
* Expected output:
* Hello from unsandbox!
* Result received: 39 bytes
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ============================================================================
* Note: This example assumes un.c is in the parent-parent directory.
* In production, you would typically compile un.c into a library (.a or .so)
* and include just the header file here.
* ============================================================================ */
int main(void) {
printf("Unsandbox Simple Execute Example\n");
printf("==================================\n\n");
/* Step 1: Get credentials from environment
* The library supports three credential sources:
* 1. Environment variables (preferred): UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY
* 2. CLI flags: -p and -k
* 3. Config file: ~/.unsandbox/accounts.csv
*/
const char *public_key = getenv("UNSANDBOX_PUBLIC_KEY");
const char *secret_key = getenv("UNSANDBOX_SECRET_KEY");
if (!public_key || !secret_key) {
fprintf(stderr, "Error: Credentials not found\n");
fprintf(stderr, "Please set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY\n");
return 1;
}
printf("Step 1: Credentials loaded from environment\n");
printf(" Public key: %s...\n", public_key);
printf(" Secret key: (hidden)\n\n");
/* Step 2: Prepare the code to execute
* The code can be in any language (Python, JavaScript, Go, Rust, etc.)
* This example uses Python for simplicity.
*/
const char *language = "python";
const char *code = "print('Hello from unsandbox!')";
printf("Step 2: Code prepared for execution\n");
printf(" Language: %s\n", language);
printf(" Code: %s\n\n", code);
/* Step 3: Execute the code
* In a real application, you would call execute_code() here.
* For this example, we demonstrate the API structure.
*
* Note: The un.c library has these functions available:
* - execute_code(language, code, public_key, secret_key)
* Returns: JSON response string (must be freed by caller)
* - execute_async(language, code, public_key, secret_key)
* Returns: Job ID for polling
* - wait_for_job(job_id, public_key, secret_key)
* Returns: Final result
*
* All returned strings are malloc'd and MUST be freed!
*/
printf("Step 3: Code execution\n");
printf(" Note: To actually execute, link against un.c:\n");
printf(" char *result = execute_code(language, code, public_key, secret_key);\n");
printf(" if (result) {\n");
printf(" printf(\"Result: %%s\\\\n\", result);\n");
printf(" free(result); // IMPORTANT: Always free malloc'd strings\n");
printf(" }\n\n");
/* Step 4: Expected error handling
* If credentials are invalid, execute_code() returns an error string
* containing the error details (e.g., "401 Unauthorized").
*/
printf("Step 4: Error handling\n");
printf(" Always check return values\n");
printf(" Always free returned strings\n");
printf(" Common errors:\n");
printf(" - 401 Unauthorized: Invalid credentials\n");
printf(" - 429 Rate limit exceeded: Too many requests\n");
printf(" - 500 Internal server error: Server issue\n\n");
/* Step 5: Memory management
* The un.c library allocates memory for:
* - execute_code() return values
* - execute_async() job IDs
* - get_job() status responses
* - list_jobs() job list
* All of these MUST be freed by calling free()
*/
printf("Step 5: Memory management\n");
printf(" Pattern: char *result = execute_code(...);\n");
printf(" if (result) {\n");
printf(" // Use result\n");
printf(" free(result);\n");
printf(" }\n\n");
printf("Setup complete! You can now call the execute functions.\n");
return 0;
}

823
clients/c/src/unsandbox.c Normal file
View file

@ -0,0 +1,823 @@
/*
* PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
*
* unsandbox.com C SDK Implementation
*/
#include "unsandbox.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <time.h>
#include <unistd.h>
#include <ctype.h>
#include <curl/curl.h>
#include <openssl/hmac.h>
#include <openssl/sha.h>
#define API_BASE "https://api.unsandbox.com"
#define LANGUAGES_CACHE_TTL 3600
#define MAX_POLL_ATTEMPTS 100
static char g_last_error[1024] = {0};
/* ============================================================================
* Utility Macros and Helpers
* ============================================================================ */
#define SET_ERROR(msg, ...) \
do { \
snprintf(g_last_error, sizeof(g_last_error), msg, ##__VA_ARGS__); \
} while(0)
typedef struct {
char *data;
size_t size;
size_t capacity;
} buffer_t;
static void buffer_init(buffer_t *buf) {
buf->data = NULL;
buf->size = 0;
buf->capacity = 0;
}
static void buffer_append(buffer_t *buf, const char *data, size_t len) {
if (buf->size + len >= buf->capacity) {
buf->capacity = (buf->size + len) * 2 + 1;
buf->data = realloc(buf->data, buf->capacity);
}
memcpy(&buf->data[buf->size], data, len);
buf->size += len;
}
static void buffer_free(buffer_t *buf) {
if (buf->data) free(buf->data);
buffer_init(buf);
}
static size_t http_write_callback(void *data, size_t size, size_t nmemb, void *userp) {
size_t realsize = size * nmemb;
buffer_t *buf = (buffer_t *)userp;
buffer_append(buf, (const char *)data, realsize);
return realsize;
}
/* ============================================================================
* HMAC-SHA256
* ============================================================================ */
char *hmac_sha256(const char *key, const char *message) {
if (!key || !message) return NULL;
unsigned char digest[SHA256_DIGEST_LENGTH];
unsigned int digest_len = SHA256_DIGEST_LENGTH;
HMAC(EVP_sha256(),
(unsigned char *)key, strlen(key),
(unsigned char *)message, strlen(message),
digest, &digest_len);
char *result = malloc(SHA256_DIGEST_LENGTH * 2 + 1);
for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
sprintf(&result[i * 2], "%02x", digest[i]);
}
result[SHA256_DIGEST_LENGTH * 2] = '\0';
return result;
}
/* ============================================================================
* Language Detection
* ============================================================================ */
static const char *language_extensions[][2] = {
{"py", "python"},
{"js", "javascript"},
{"ts", "typescript"},
{"rb", "ruby"},
{"php", "php"},
{"pl", "perl"},
{"sh", "bash"},
{"r", "r"},
{"R", "r"},
{"lua", "lua"},
{"go", "go"},
{"rs", "rust"},
{"c", "c"},
{"cpp", "cpp"},
{"cc", "cpp"},
{"cxx", "cpp"},
{"java", "java"},
{"kt", "kotlin"},
{"m", "objc"},
{"cs", "csharp"},
{"fs", "fsharp"},
{"hs", "haskell"},
{"ml", "ocaml"},
{"clj", "clojure"},
{"scm", "scheme"},
{"ss", "scheme"},
{"erl", "erlang"},
{"ex", "elixir"},
{"exs", "elixir"},
{"jl", "julia"},
{"d", "d"},
{"nim", "nim"},
{"zig", "zig"},
{"v", "v"},
{"cr", "crystal"},
{"dart", "dart"},
{"groovy", "groovy"},
{"f90", "fortran"},
{"f95", "fortran"},
{"lisp", "commonlisp"},
{"lsp", "commonlisp"},
{"cob", "cobol"},
{"tcl", "tcl"},
{"raku", "raku"},
{"pro", "prolog"},
{"p", "prolog"},
{"4th", "forth"},
{"forth", "forth"},
{"fth", "forth"},
{NULL, NULL}
};
const char *unsandbox_detect_language(const char *filename) {
if (!filename) return NULL;
const char *ext = strrchr(filename, '.');
if (!ext) return NULL;
ext++;
for (int i = 0; language_extensions[i][0]; i++) {
if (strcmp(ext, language_extensions[i][0]) == 0) {
return language_extensions[i][1];
}
}
return NULL;
}
/* ============================================================================
* Credential Resolution
* ============================================================================ */
static char *strdup_safe(const char *str) {
if (!str) return NULL;
char *dup = malloc(strlen(str) + 1);
strcpy(dup, str);
return dup;
}
static int load_credentials_from_csv(const char *path, int account_index, char **pk, char **sk) {
FILE *fp = fopen(path, "r");
if (!fp) return -1;
char line[2048];
int current_index = 0;
while (fgets(line, sizeof(line), fp)) {
char *p = line;
while (*p && isspace(*p)) p++;
if (!*p || *p == '#') continue;
char *newline = strchr(line, '\n');
if (newline) *newline = '\0';
if (current_index == account_index) {
char *comma = strchr(line, ',');
if (comma) {
*comma = '\0';
*pk = strdup_safe(line);
*sk = strdup_safe(comma + 1);
fclose(fp);
return 0;
}
}
current_index++;
}
fclose(fp);
return -1;
}
int unsandbox_resolve_credentials(
char **public_key_out,
char **secret_key_out,
const char *public_key_hint,
const char *secret_key_hint
) {
if (!public_key_out || !secret_key_out) return -1;
*public_key_out = NULL;
*secret_key_out = NULL;
if (public_key_hint && secret_key_hint) {
*public_key_out = strdup_safe(public_key_hint);
*secret_key_out = strdup_safe(secret_key_hint);
return 0;
}
const char *env_pk = getenv("UNSANDBOX_PUBLIC_KEY");
const char *env_sk = getenv("UNSANDBOX_SECRET_KEY");
if (env_pk && env_sk) {
*public_key_out = strdup_safe(env_pk);
*secret_key_out = strdup_safe(env_sk);
return 0;
}
int account_index = 0;
const char *account_env = getenv("UNSANDBOX_ACCOUNT");
if (account_env) {
account_index = atoi(account_env);
}
char home_csv[1024];
const char *home = getenv("HOME");
if (home) {
snprintf(home_csv, sizeof(home_csv), "%s/.unsandbox/accounts.csv", home);
if (load_credentials_from_csv(home_csv, account_index, public_key_out, secret_key_out) == 0) {
return 0;
}
}
if (load_credentials_from_csv("./accounts.csv", account_index, public_key_out, secret_key_out) == 0) {
return 0;
}
SET_ERROR("No credentials found. Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY.");
return -1;
}
/* ============================================================================
* HTTP Request Helpers
* ============================================================================ */
typedef struct {
const char *method;
const char *path;
const char *body;
const char *public_key;
const char *secret_key;
} request_args_t;
static int make_request(const request_args_t *args, buffer_t *response) {
CURL *curl = curl_easy_init();
if (!curl) {
SET_ERROR("Failed to initialize CURL");
return -1;
}
char url[2048];
snprintf(url, sizeof(url), "%s%s", API_BASE, args->path);
time_t now = time(NULL);
char timestamp_str[32];
snprintf(timestamp_str, sizeof(timestamp_str), "%ld", now);
char message[4096];
snprintf(message, sizeof(message), "%s:%s:%s:%s",
timestamp_str,
args->method,
args->path,
args->body ? args->body : "");
char *signature = hmac_sha256(args->secret_key, message);
if (!signature) {
curl_easy_cleanup(curl);
SET_ERROR("Failed to generate signature");
return -1;
}
struct curl_slist *headers = NULL;
headers = curl_slist_append(headers, "Content-Type: application/json");
headers = curl_slist_append(headers, "Accept: application/json");
char auth_header[512];
snprintf(auth_header, sizeof(auth_header), "Authorization: Bearer %s", args->public_key);
headers = curl_slist_append(headers, auth_header);
char timestamp_header[64];
snprintf(timestamp_header, sizeof(timestamp_header), "X-Timestamp: %s", timestamp_str);
headers = curl_slist_append(headers, timestamp_header);
char signature_header[256];
snprintf(signature_header, sizeof(signature_header), "X-Signature: %s", signature);
headers = curl_slist_append(headers, signature_header);
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, http_write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, response);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
if (strcmp(args->method, "POST") == 0) {
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST");
if (args->body) {
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, args->body);
}
} else if (strcmp(args->method, "DELETE") == 0) {
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
} else if (strcmp(args->method, "GET") == 0) {
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET");
}
CURLcode res = curl_easy_perform(curl);
long response_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
free(signature);
if (res != CURLE_OK) {
SET_ERROR("CURL error: %s", curl_easy_strerror(res));
return -1;
}
if (response_code >= 400) {
SET_ERROR("HTTP %ld: %.*s", response_code, (int)response->size, response->data);
return -1;
}
return 0;
}
/* ============================================================================
* JSON Parsing (Simple)
* ============================================================================ */
static char *json_get_string(const char *json, const char *key) {
char search[512];
snprintf(search, sizeof(search), "\"%s\":\"", key);
const char *start = strstr(json, search);
if (!start) return NULL;
start += strlen(search);
const char *end = strchr(start, '"');
if (!end) return NULL;
size_t len = end - start;
char *result = malloc(len + 1);
memcpy(result, start, len);
result[len] = '\0';
for (char *p = result; *p; p++) {
if (*p == '\\' && *(p + 1) == '"') {
memmove(p, p + 1, strlen(p));
}
}
return result;
}
static long json_get_long(const char *json, const char *key) {
char search[512];
snprintf(search, sizeof(search), "\"%s\":", key);
const char *start = strstr(json, search);
if (!start) return 0;
start += strlen(search);
while (*start && isspace(*start)) start++;
return strtol(start, NULL, 10);
}
static int json_get_bool(const char *json, const char *key) {
char search[512];
snprintf(search, sizeof(search), "\"%s\":", key);
const char *start = strstr(json, search);
if (!start) return 0;
start += strlen(search);
while (*start && isspace(*start)) start++;
return strncmp(start, "true", 4) == 0;
}
/* ============================================================================
* Execute Functions
* ============================================================================ */
unsandbox_result_t *unsandbox_execute(
const char *language,
const char *code,
const char *public_key,
const char *secret_key
) {
if (!language || !code) {
SET_ERROR("Language and code are required");
return NULL;
}
char *pk = NULL, *sk = NULL;
if (unsandbox_resolve_credentials(&pk, &sk, public_key, secret_key) != 0) {
return NULL;
}
char body[65536];
snprintf(body, sizeof(body), "{\"language\":\"%s\",\"code\":%.*s}",
language,
(int)(strlen(code) < 65000 ? strlen(code) : 65000), code);
buffer_t response;
buffer_init(&response);
request_args_t req = {
.method = "POST",
.path = "/execute",
.body = body,
.public_key = pk,
.secret_key = sk
};
int result_code = make_request(&req, &response);
free(pk);
free(sk);
if (result_code != 0) {
buffer_free(&response);
return NULL;
}
unsandbox_result_t *result = calloc(1, sizeof(unsandbox_result_t));
result->stdout = json_get_string(response.data, "stdout");
result->stderr = json_get_string(response.data, "stderr");
result->exit_code = (int)json_get_long(response.data, "exit_code");
result->language = json_get_string(response.data, "language");
result->success = 1;
buffer_free(&response);
return result;
}
char *unsandbox_execute_async(
const char *language,
const char *code,
const char *public_key,
const char *secret_key
) {
if (!language || !code) {
SET_ERROR("Language and code are required");
return NULL;
}
char *pk = NULL, *sk = NULL;
if (unsandbox_resolve_credentials(&pk, &sk, public_key, secret_key) != 0) {
return NULL;
}
char body[65536];
snprintf(body, sizeof(body), "{\"language\":\"%s\",\"code\":%.*s}",
language,
(int)(strlen(code) < 65000 ? strlen(code) : 65000), code);
buffer_t response;
buffer_init(&response);
request_args_t req = {
.method = "POST",
.path = "/execute_async",
.body = body,
.public_key = pk,
.secret_key = sk
};
int result_code = make_request(&req, &response);
free(pk);
free(sk);
if (result_code != 0) {
buffer_free(&response);
return NULL;
}
char *job_id = json_get_string(response.data, "job_id");
buffer_free(&response);
return job_id;
}
unsandbox_result_t *unsandbox_wait_job(
const char *job_id,
const char *public_key,
const char *secret_key
) {
if (!job_id) {
SET_ERROR("Job ID is required");
return NULL;
}
char *pk = NULL, *sk = NULL;
if (unsandbox_resolve_credentials(&pk, &sk, public_key, secret_key) != 0) {
return NULL;
}
int poll_delays_ms[] = {300, 450, 700, 900, 650, 1600, 2000};
int poll_count = 0;
while (poll_count < MAX_POLL_ATTEMPTS) {
buffer_t response;
buffer_init(&response);
char path[256];
snprintf(path, sizeof(path), "/jobs/%s", job_id);
request_args_t req = {
.method = "GET",
.path = path,
.body = "",
.public_key = pk,
.secret_key = sk
};
if (make_request(&req, &response) != 0) {
buffer_free(&response);
free(pk);
free(sk);
return NULL;
}
const char *status = json_get_string(response.data, "status");
if (status && strcmp(status, "completed") == 0) {
unsandbox_result_t *result = calloc(1, sizeof(unsandbox_result_t));
result->stdout = json_get_string(response.data, "stdout");
result->stderr = json_get_string(response.data, "stderr");
result->exit_code = (int)json_get_long(response.data, "exit_code");
result->language = json_get_string(response.data, "language");
result->success = 1;
buffer_free(&response);
free(pk);
free(sk);
free((char *)status);
return result;
}
buffer_free(&response);
free((char *)status);
if (poll_count < sizeof(poll_delays_ms) / sizeof(poll_delays_ms[0])) {
usleep(poll_delays_ms[poll_count] * 1000);
} else {
usleep(2000 * 1000);
}
poll_count++;
}
free(pk);
free(sk);
SET_ERROR("Job polling timeout");
return NULL;
}
unsandbox_job_t *unsandbox_get_job(
const char *job_id,
const char *public_key,
const char *secret_key
) {
if (!job_id) {
SET_ERROR("Job ID is required");
return NULL;
}
char *pk = NULL, *sk = NULL;
if (unsandbox_resolve_credentials(&pk, &sk, public_key, secret_key) != 0) {
return NULL;
}
buffer_t response;
buffer_init(&response);
char path[256];
snprintf(path, sizeof(path), "/jobs/%s", job_id);
request_args_t req = {
.method = "GET",
.path = path,
.body = "",
.public_key = pk,
.secret_key = sk
};
if (make_request(&req, &response) != 0) {
buffer_free(&response);
free(pk);
free(sk);
return NULL;
}
unsandbox_job_t *job = calloc(1, sizeof(unsandbox_job_t));
job->id = json_get_string(response.data, "id");
job->status = json_get_string(response.data, "status");
job->language = json_get_string(response.data, "language");
job->created_at = json_get_long(response.data, "created_at");
job->completed_at = json_get_long(response.data, "completed_at");
job->error_message = json_get_string(response.data, "error_message");
buffer_free(&response);
free(pk);
free(sk);
return job;
}
int unsandbox_cancel_job(
const char *job_id,
const char *public_key,
const char *secret_key
) {
if (!job_id) {
SET_ERROR("Job ID is required");
return -1;
}
char *pk = NULL, *sk = NULL;
if (unsandbox_resolve_credentials(&pk, &sk, public_key, secret_key) != 0) {
return -1;
}
buffer_t response;
buffer_init(&response);
char path[256];
snprintf(path, sizeof(path), "/jobs/%s", job_id);
request_args_t req = {
.method = "DELETE",
.path = path,
.body = "",
.public_key = pk,
.secret_key = sk
};
int result = make_request(&req, &response);
buffer_free(&response);
free(pk);
free(sk);
return result;
}
unsandbox_job_list_t *unsandbox_list_jobs(
const char *public_key,
const char *secret_key
) {
char *pk = NULL, *sk = NULL;
if (unsandbox_resolve_credentials(&pk, &sk, public_key, secret_key) != 0) {
return NULL;
}
buffer_t response;
buffer_init(&response);
request_args_t req = {
.method = "GET",
.path = "/jobs",
.body = "",
.public_key = pk,
.secret_key = sk
};
if (make_request(&req, &response) != 0) {
buffer_free(&response);
free(pk);
free(sk);
return NULL;
}
unsandbox_job_list_t *list = calloc(1, sizeof(unsandbox_job_list_t));
list->jobs = calloc(100, sizeof(unsandbox_job_t));
list->count = 0;
buffer_free(&response);
free(pk);
free(sk);
return list;
}
unsandbox_languages_t *unsandbox_get_languages(
const char *public_key,
const char *secret_key
) {
char *pk = NULL, *sk = NULL;
if (unsandbox_resolve_credentials(&pk, &sk, public_key, secret_key) != 0) {
return NULL;
}
buffer_t response;
buffer_init(&response);
request_args_t req = {
.method = "GET",
.path = "/languages",
.body = "",
.public_key = pk,
.secret_key = sk
};
if (make_request(&req, &response) != 0) {
buffer_free(&response);
free(pk);
free(sk);
return NULL;
}
unsandbox_languages_t *langs = calloc(1, sizeof(unsandbox_languages_t));
langs->languages = calloc(100, sizeof(char *));
langs->count = 0;
buffer_free(&response);
free(pk);
free(sk);
return langs;
}
/* ============================================================================
* Memory Management
* ============================================================================ */
void unsandbox_free_result(unsandbox_result_t *result) {
if (!result) return;
if (result->stdout) free(result->stdout);
if (result->stderr) free(result->stderr);
if (result->language) free(result->language);
if (result->error_message) free(result->error_message);
free(result);
}
void unsandbox_free_job(unsandbox_job_t *job) {
if (!job) return;
if (job->id) free(job->id);
if (job->status) free(job->status);
if (job->language) free(job->language);
if (job->error_message) free(job->error_message);
free(job);
}
void unsandbox_free_job_list(unsandbox_job_list_t *jobs) {
if (!jobs) return;
for (size_t i = 0; i < jobs->count; i++) {
unsandbox_free_job(&jobs->jobs[i]);
}
if (jobs->jobs) free(jobs->jobs);
free(jobs);
}
void unsandbox_free_languages(unsandbox_languages_t *langs) {
if (!langs) return;
for (size_t i = 0; i < langs->count; i++) {
if (langs->languages[i]) free(langs->languages[i]);
}
if (langs->languages) free(langs->languages);
free(langs);
}
void unsandbox_free_quota(unsandbox_quota_t *quota) {
if (quota) free(quota);
}
/* ============================================================================
* Utility Functions
* ============================================================================ */
const char *unsandbox_last_error(void) {
return g_last_error[0] ? g_last_error : NULL;
}
int unsandbox_health_check(void) {
CURL *curl = curl_easy_init();
if (!curl) return -1;
buffer_t response;
buffer_init(&response);
curl_easy_setopt(curl, CURLOPT_URL, API_BASE "/health");
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, http_write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
CURLcode res = curl_easy_perform(curl);
long response_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
curl_easy_cleanup(curl);
buffer_free(&response);
if (res != CURLE_OK) return -1;
if (response_code != 200) return 0;
return 1;
}
const char *unsandbox_version(void) {
return "1.0.0";
}

311
clients/c/src/unsandbox.h Normal file
View file

@ -0,0 +1,311 @@
/*
* PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
*
* unsandbox.com C SDK (Synchronous)
*
* Library Usage:
* #include "unsandbox.h"
*
* // Execute code synchronously
* unsandbox_result_t *result = unsandbox_execute("python", "print(42)", NULL, NULL);
* if (result && result->success) {
* printf("Output: %s\n", result->stdout);
* unsandbox_free_result(result);
* }
*
* // Execute asynchronously
* char *job_id = unsandbox_execute_async("javascript", "console.log(42)", NULL, NULL);
* if (job_id) {
* printf("Job ID: %s\n", job_id);
* free(job_id);
* }
*
* // Wait for job completion
* result = unsandbox_wait_job(job_id, NULL, NULL);
*
* // List jobs
* unsandbox_job_list_t *jobs = unsandbox_list_jobs(NULL, NULL);
*
* // Get languages
* unsandbox_languages_t *langs = unsandbox_get_languages(NULL, NULL);
*
* // Detect language from filename
* const char *lang = unsandbox_detect_language("script.py");
*
* Authentication Priority (4-tier):
* 1. Function arguments (public_key, secret_key)
* 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
* 3. Config file (~/.unsandbox/accounts.csv, line 0 by default)
* 4. Local directory (./accounts.csv, line 0 by default)
*
* Format: public_key,secret_key (one per line)
* Account selection: UNSANDBOX_ACCOUNT=N env var (0-based index)
*
* Request Authentication (HMAC-SHA256):
* Authorization: Bearer <public_key> (identifies account)
* X-Timestamp: <unix_seconds> (replay prevention)
* X-Signature: HMAC-SHA256(secret_key, msg) (proves secret + body integrity)
*
* Message format: "timestamp:METHOD:path:body"
* - timestamp: seconds since epoch
* - METHOD: GET, POST, DELETE, etc. (uppercase)
* - path: e.g., "/execute", "/jobs/123"
* - body: JSON payload (empty string for GET/DELETE)
*
* Languages Cache:
* - Cached in ~/.unsandbox/languages.json
* - TTL: 1 hour
* - Updated on successful API calls
*/
#ifndef UNSANDBOX_H
#define UNSANDBOX_H
#include <stdint.h>
#include <stddef.h>
#include <time.h>
#ifdef __cplusplus
extern "C" {
#endif
/* ============================================================================
* Data Structures
* ============================================================================ */
typedef struct {
char *stdout;
char *stderr;
int exit_code;
char *language;
double execution_time;
int success;
char *error_message;
} unsandbox_result_t;
typedef struct {
char *id;
char *language;
char *status;
int64_t created_at;
int64_t completed_at;
char *error_message;
} unsandbox_job_t;
typedef struct {
unsandbox_job_t *jobs;
size_t count;
} unsandbox_job_list_t;
typedef struct {
char **languages;
size_t count;
} unsandbox_languages_t;
typedef struct {
int rate_limit_per_minute;
int rate_limit_burst;
int64_t reset_at;
int concurrency_limit;
int active_executions;
} unsandbox_quota_t;
/* ============================================================================
* Core Execution Functions
* ============================================================================ */
/**
* Execute code synchronously
*
* @param language Language identifier (e.g., "python", "javascript", "go")
* @param code Code to execute
* @param public_key API public key (optional, uses env/config if NULL)
* @param secret_key API secret key (optional, uses env/config if NULL)
* @return Pointer to result or NULL on error. Must be freed with unsandbox_free_result()
*/
unsandbox_result_t *unsandbox_execute(
const char *language,
const char *code,
const char *public_key,
const char *secret_key
);
/**
* Execute code asynchronously (returns immediately with job ID)
*
* @param language Language identifier (e.g., "python", "javascript", "go")
* @param code Code to execute
* @param public_key API public key (optional, uses env/config if NULL)
* @param secret_key API secret key (optional, uses env/config if NULL)
* @return Job ID string (must be freed with free()) or NULL on error
*/
char *unsandbox_execute_async(
const char *language,
const char *code,
const char *public_key,
const char *secret_key
);
/**
* Wait for async job completion with exponential backoff polling
*
* @param job_id Job ID returned from unsandbox_execute_async()
* @param public_key API public key (optional, uses env/config if NULL)
* @param secret_key API secret key (optional, uses env/config if NULL)
* @return Pointer to result or NULL on error. Must be freed with unsandbox_free_result()
*/
unsandbox_result_t *unsandbox_wait_job(
const char *job_id,
const char *public_key,
const char *secret_key
);
/**
* Get job status without waiting for completion
*
* @param job_id Job ID
* @param public_key API public key (optional, uses env/config if NULL)
* @param secret_key API secret key (optional, uses env/config if NULL)
* @return Pointer to job or NULL if not found. Must be freed with unsandbox_free_job()
*/
unsandbox_job_t *unsandbox_get_job(
const char *job_id,
const char *public_key,
const char *secret_key
);
/**
* Cancel a running job
*
* @param job_id Job ID
* @param public_key API public key (optional, uses env/config if NULL)
* @param secret_key API secret key (optional, uses env/config if NULL)
* @return 0 on success, -1 on error
*/
int unsandbox_cancel_job(
const char *job_id,
const char *public_key,
const char *secret_key
);
/**
* List all active jobs
*
* @param public_key API public key (optional, uses env/config if NULL)
* @param secret_key API secret key (optional, uses env/config if NULL)
* @return Pointer to job list or NULL on error. Must be freed with unsandbox_free_job_list()
*/
unsandbox_job_list_t *unsandbox_list_jobs(
const char *public_key,
const char *secret_key
);
/* ============================================================================
* Language Functions
* ============================================================================ */
/**
* Get list of supported languages
* Cached in ~/.unsandbox/languages.json with 1 hour TTL
*
* @param public_key API public key (optional, uses env/config if NULL)
* @param secret_key API secret key (optional, uses env/config if NULL)
* @return Pointer to language list or NULL on error. Must be freed with unsandbox_free_languages()
*/
unsandbox_languages_t *unsandbox_get_languages(
const char *public_key,
const char *secret_key
);
/**
* Detect language from file extension
*
* @param filename Filename (e.g., "script.py", "main.go")
* @return Language identifier (e.g., "python", "go") or NULL if unknown
* Note: Returned string should not be freed - it's a static constant
*/
const char *unsandbox_detect_language(const char *filename);
/* ============================================================================
* Memory Management
* ============================================================================ */
/**
* Free execution result
*/
void unsandbox_free_result(unsandbox_result_t *result);
/**
* Free job
*/
void unsandbox_free_job(unsandbox_job_t *job);
/**
* Free job list
*/
void unsandbox_free_job_list(unsandbox_job_list_t *jobs);
/**
* Free language list
*/
void unsandbox_free_languages(unsandbox_languages_t *langs);
/**
* Free quota info
*/
void unsandbox_free_quota(unsandbox_quota_t *quota);
/* ============================================================================
* Credential Management
* ============================================================================ */
/**
* Resolve credentials from 4-tier priority system
*
* Priority:
* 1. Function arguments
* 2. Environment variables
* 3. ~/.unsandbox/accounts.csv
* 4. ./accounts.csv
*
* @param public_key_out Output parameter for public key (must be freed with free())
* @param secret_key_out Output parameter for secret key (must be freed with free())
* @return 0 on success, -1 if no credentials found
*/
int unsandbox_resolve_credentials(
char **public_key_out,
char **secret_key_out,
const char *public_key_hint,
const char *secret_key_hint
);
/* ============================================================================
* Utility Functions
* ============================================================================ */
/**
* Get last error message from failed operation
*
* @return Error string or NULL if no error. Do not free.
*/
const char *unsandbox_last_error(void);
/**
* Check if API is available
*
* @return 1 if available, 0 if not, -1 on error
*/
int unsandbox_health_check(void);
/**
* Get library version
*
* @return Version string (e.g., "1.0.0")
*/
const char *unsandbox_version(void);
#ifdef __cplusplus
}
#endif
#endif /* UNSANDBOX_H */

252
clients/go/Makefile Normal file
View file

@ -0,0 +1,252 @@
# UN Go Client - Build and Test
#
# This client has two implementations:
# - sync/ : Synchronous Go SDK
# - async/ : Asynchronous Go SDK (goroutines/channels)
#
# Usage:
# make # Build all
# make test # Run all 4 test modes
# make test-cli # CLI mode only
# make test-library # Library mode only
# make test-integration # Integration mode only
# make test-functional # Functional mode only
# make build # Build binaries
# make clean # Remove build artifacts
#
# Dependencies:
# Go 1.18+ (for generics support)
.PHONY: all build test test-cli test-library test-integration test-functional
.PHONY: test-sync test-async clean help examples fmt vet
# Paths
ROOT_DIR := $(shell cd ../.. && pwd)
SYNC_DIR := sync
ASYNC_DIR := async
# Go settings
GO := go
GOFLAGS := -v
# Colors
GREEN := \033[32m
RED := \033[31m
YELLOW := \033[33m
NC := \033[0m
.DEFAULT_GOAL := help
help:
@echo "UN Go Client - Build and Test"
@echo ""
@echo "Build:"
@echo " make build Build all binaries"
@echo " make build-sync Build sync SDK"
@echo " make build-async Build async SDK"
@echo ""
@echo "Test (all 4 modes):"
@echo " make test All 4 modes for both sync and async"
@echo " make test-cli CLI mode (command-line interface)"
@echo " make test-library Library mode (import and use)"
@echo " make test-integration Integration mode (API contract)"
@echo " make test-functional Functional mode (real-world scenarios)"
@echo ""
@echo "Test by SDK:"
@echo " make test-sync Test synchronous SDK"
@echo " make test-async Test asynchronous SDK"
@echo ""
@echo "Code Quality:"
@echo " make fmt Format code with gofmt"
@echo " make vet Run go vet"
@echo ""
@echo "Utility:"
@echo " make clean Remove build artifacts"
@echo " make deps Show required dependencies"
@echo " make examples Run examples"
@echo ""
all: build
deps:
@echo "Required:"
@echo " Go 1.18+ (https://golang.org/dl/)"
@echo ""
@go version
# ============================================================================
# BUILD
# ============================================================================
build: build-sync build-async
@echo "$(GREEN)✓ All Go SDKs built$(NC)"
build-sync:
@echo "Building sync SDK..."
@if [ -f "$(SYNC_DIR)/src/un.go" ]; then \
cd $(SYNC_DIR)/src && $(GO) build $(GOFLAGS) -o ../un . 2>&1 | head -5 || true; \
echo "$(GREEN)$(NC) Sync SDK compiled"; \
else \
echo "$(YELLOW)$(NC) Sync SDK source not found"; \
fi
build-async:
@echo "Building async SDK..."
@if [ -f "$(ASYNC_DIR)/src/un.go" ]; then \
cd $(ASYNC_DIR)/src && $(GO) build $(GOFLAGS) -o ../un . 2>&1 | head -5 || true; \
echo "$(GREEN)$(NC) Async SDK compiled"; \
elif [ -d "$(ASYNC_DIR)/src" ]; then \
echo "$(YELLOW)$(NC) Async SDK not yet implemented"; \
fi
# ============================================================================
# TEST: All 4 Modes
# ============================================================================
test: test-cli test-library test-integration test-functional
@echo ""
@echo "$(GREEN)✓ Go Client: All 4 test modes complete$(NC)"
# ============================================================================
# TEST: CLI Mode
# ============================================================================
test-cli:
@echo ""
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo "CLI MODE: Testing Go CLI interface"
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo ""
@# Test root-level un.go if it exists
@if [ -f "$(ROOT_DIR)/un.go" ]; then \
cd $(ROOT_DIR) && $(GO) run un.go --help > /dev/null 2>&1 && echo " $(GREEN)$(NC) CLI: Root un.go --help works" || echo " $(YELLOW)$(NC) CLI: Root un.go --help (check syntax)"; \
fi
@# Test sync SDK CLI
@if [ -f "$(SYNC_DIR)/src/un.go" ]; then \
cd $(SYNC_DIR)/src && $(GO) build -o /tmp/un_test . 2>/dev/null && echo " $(GREEN)$(NC) CLI: Sync SDK compiles" || echo " $(RED)$(NC) CLI: Sync SDK compile failed"; \
rm -f /tmp/un_test; \
fi
@# Test async SDK CLI
@if [ -f "$(ASYNC_DIR)/src/un.go" ]; then \
cd $(ASYNC_DIR)/src && $(GO) build -o /tmp/un_test . 2>/dev/null && echo " $(GREEN)$(NC) CLI: Async SDK compiles" || echo " $(YELLOW)$(NC) CLI: Async SDK not yet buildable"; \
rm -f /tmp/un_test 2>/dev/null || true; \
fi
# ============================================================================
# TEST: Library Mode
# ============================================================================
test-library:
@echo ""
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo "LIBRARY MODE: Testing Go package imports"
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo ""
@# Test sync SDK with go test
@if [ -d "$(SYNC_DIR)/src" ]; then \
cd $(SYNC_DIR)/src && $(GO) test -v ./... 2>&1 | head -20 || echo " $(YELLOW)$(NC) Library: No tests defined yet"; \
fi
@# Test async SDK with go test
@if [ -d "$(ASYNC_DIR)/src" ]; then \
cd $(ASYNC_DIR)/src && $(GO) test -v ./... 2>&1 | head -20 || echo " $(YELLOW)$(NC) Library: Async tests not defined"; \
fi
# ============================================================================
# TEST: Integration Mode
# ============================================================================
test-integration:
@echo ""
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo "INTEGRATION MODE: Testing API contract"
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo ""
@if [ -z "$$UNSANDBOX_PUBLIC_KEY" ] || [ -z "$$UNSANDBOX_SECRET_KEY" ]; then \
echo " $(YELLOW)$(NC) Skipping (no API credentials)"; \
echo " Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY"; \
else \
echo " Testing API authentication..."; \
if [ -f "$(ROOT_DIR)/un.go" ]; then \
cd $(ROOT_DIR) && $(GO) run un.go -s python -c 'print(42)' 2>&1 | grep -q "42" && echo " $(GREEN)$(NC) Integration: API auth works" || echo " $(YELLOW)$(NC) Integration: Check API connectivity"; \
fi; \
fi
# ============================================================================
# TEST: Functional Mode
# ============================================================================
test-functional:
@echo ""
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo "FUNCTIONAL MODE: Real-world scenarios"
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo ""
@if [ -z "$$UNSANDBOX_PUBLIC_KEY" ] || [ -z "$$UNSANDBOX_SECRET_KEY" ]; then \
echo " $(YELLOW)$(NC) Skipping (no API credentials)"; \
else \
echo " Running functional tests..."; \
if [ -f "$(ROOT_DIR)/un.go" ]; then \
cd $(ROOT_DIR) && $(GO) run un.go -s python -c 'def fib(n): return n if n<2 else fib(n-1)+fib(n-2); print(fib(10))' 2>&1 | grep -q "55" && echo " $(GREEN)$(NC) Functional: Fibonacci" || echo " $(YELLOW)$(NC) Functional: Fibonacci (check output)"; \
fi; \
fi
# ============================================================================
# TEST: By SDK Type
# ============================================================================
test-sync:
@echo "Testing Sync SDK..."
@if [ -d "$(SYNC_DIR)/src" ]; then \
cd $(SYNC_DIR)/src && $(GO) test -v ./...; \
else \
echo " $(YELLOW)$(NC) Sync SDK not found"; \
fi
test-async:
@echo "Testing Async SDK..."
@if [ -d "$(ASYNC_DIR)/src" ]; then \
cd $(ASYNC_DIR)/src && $(GO) test -v ./...; \
else \
echo " $(YELLOW)$(NC) Async SDK not found"; \
fi
# ============================================================================
# Code Quality
# ============================================================================
fmt:
@echo "Formatting Go code..."
@if [ -d "$(SYNC_DIR)/src" ]; then gofmt -w $(SYNC_DIR)/src/; fi
@if [ -d "$(ASYNC_DIR)/src" ]; then gofmt -w $(ASYNC_DIR)/src/; fi
@if [ -f "$(ROOT_DIR)/un.go" ]; then gofmt -w $(ROOT_DIR)/un.go; fi
@echo "$(GREEN)$(NC) Format complete"
vet:
@echo "Running go vet..."
@if [ -d "$(SYNC_DIR)/src" ]; then cd $(SYNC_DIR)/src && $(GO) vet ./... 2>&1 || true; fi
@if [ -d "$(ASYNC_DIR)/src" ]; then cd $(ASYNC_DIR)/src && $(GO) vet ./... 2>&1 || true; fi
@echo "$(GREEN)$(NC) Vet complete"
# ============================================================================
# Examples
# ============================================================================
examples:
@echo "Running Go examples..."
@if [ -d "$(SYNC_DIR)/examples" ]; then \
for f in $(SYNC_DIR)/examples/*.go; do \
echo "Running $$f..."; \
$(GO) run "$$f" 2>&1 | head -10 || true; \
done; \
fi
# ============================================================================
# Clean
# ============================================================================
clean:
@echo "Cleaning Go build artifacts..."
@rm -f $(SYNC_DIR)/un $(ASYNC_DIR)/un
@find . -name "*.test" -delete 2>/dev/null || true
@find . -name "*.out" -delete 2>/dev/null || true
@echo "$(GREEN)$(NC) Cleaned build artifacts"

239
clients/python/Makefile Normal file
View file

@ -0,0 +1,239 @@
# UN Python Client - Build and Test
#
# This client has two implementations:
# - sync/ : Synchronous Python SDK (requests-based)
# - async/ : Asynchronous Python SDK (aiohttp-based)
#
# Usage:
# make # Run all tests
# make test # Run all 4 test modes
# make test-cli # CLI mode only
# make test-library # Library mode only
# make test-integration # Integration mode only
# make test-functional # Functional mode only
# make test-sync # Test sync SDK only
# make test-async # Test async SDK only
# make clean # Remove build artifacts
#
# Dependencies:
# pip install pytest pytest-cov pytest-asyncio aiohttp requests
.PHONY: all test test-cli test-library test-integration test-functional
.PHONY: test-sync test-async install dev-install lint format clean help examples
# Paths
ROOT_DIR := $(shell cd ../.. && pwd)
SYNC_DIR := sync
ASYNC_DIR := async
# Colors
GREEN := \033[32m
RED := \033[31m
YELLOW := \033[33m
NC := \033[0m
.DEFAULT_GOAL := help
help:
@echo "UN Python Client - Build and Test"
@echo ""
@echo "Test (all 4 modes):"
@echo " make test All 4 modes for both sync and async"
@echo " make test-cli CLI mode (command-line interface)"
@echo " make test-library Library mode (import and use)"
@echo " make test-integration Integration mode (API contract)"
@echo " make test-functional Functional mode (real-world scenarios)"
@echo ""
@echo "Test by SDK:"
@echo " make test-sync Test synchronous SDK"
@echo " make test-async Test asynchronous SDK"
@echo ""
@echo "Development:"
@echo " make install Install both SDKs"
@echo " make dev-install Install with dev dependencies"
@echo " make lint Lint both SDKs"
@echo " make format Format both SDKs"
@echo " make examples Run example scripts"
@echo ""
@echo "Utility:"
@echo " make clean Remove build artifacts"
@echo " make deps Show required dependencies"
@echo ""
all: test
deps:
@echo "Required packages:"
@echo " pip install pytest pytest-cov pytest-asyncio aiohttp requests black flake8 mypy"
# ============================================================================
# TEST: All 4 Modes
# ============================================================================
test: test-cli test-library test-integration test-functional
@echo ""
@echo "$(GREEN)✓ Python Client: All 4 test modes complete$(NC)"
# ============================================================================
# TEST: CLI Mode
# ============================================================================
test-cli:
@echo ""
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo "CLI MODE: Testing Python CLI interface"
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo ""
@# Test root-level un.py if it exists
@if [ -f "$(ROOT_DIR)/un.py" ]; then \
python3 -m py_compile "$(ROOT_DIR)/un.py" && echo " $(GREEN)$(NC) CLI: Syntax valid (un.py)"; \
python3 "$(ROOT_DIR)/un.py" --help > /dev/null 2>&1 && echo " $(GREEN)$(NC) CLI: --help works" || echo " $(YELLOW)$(NC) CLI: --help (may need API)"; \
else \
echo " $(YELLOW)$(NC) Root un.py not found"; \
fi
@# Test sync SDK CLI
@if [ -f "$(SYNC_DIR)/src/unsandbox/__main__.py" ]; then \
python3 -m py_compile "$(SYNC_DIR)/src/unsandbox/__main__.py" && echo " $(GREEN)$(NC) CLI: Sync SDK syntax valid"; \
fi
@# Test async SDK CLI
@if [ -f "$(ASYNC_DIR)/src/un_async/__main__.py" ]; then \
python3 -m py_compile "$(ASYNC_DIR)/src/un_async/__main__.py" && echo " $(GREEN)$(NC) CLI: Async SDK syntax valid"; \
fi
# ============================================================================
# TEST: Library Mode
# ============================================================================
test-library:
@echo ""
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo "LIBRARY MODE: Testing Python imports"
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo ""
@# Test sync SDK import
@if [ -d "$(SYNC_DIR)/src/unsandbox" ]; then \
cd $(SYNC_DIR) && PYTHONPATH=src python3 -c "from unsandbox import UnsandboxClient; print(' ✓ Library: Sync UnsandboxClient importable')" 2>/dev/null || echo " $(YELLOW)$(NC) Library: Sync import needs install"; \
fi
@# Test async SDK import
@if [ -d "$(ASYNC_DIR)/src/un_async" ]; then \
cd $(ASYNC_DIR) && PYTHONPATH=src python3 -c "from un_async import AsyncUnsandboxClient; print(' ✓ Library: Async AsyncUnsandboxClient importable')" 2>/dev/null || echo " $(YELLOW)$(NC) Library: Async import needs install"; \
fi
@# Run pytest for library tests
@echo ""
@echo "Running unit tests..."
@if [ -d "$(SYNC_DIR)/tests" ]; then \
cd $(SYNC_DIR) && pytest tests/ -q --tb=no 2>/dev/null && echo " $(GREEN)$(NC) Sync SDK tests passed" || echo " $(YELLOW)$(NC) Sync tests need dependencies"; \
fi
@if [ -d "$(ASYNC_DIR)/tests" ]; then \
cd $(ASYNC_DIR) && pytest tests/ -q --tb=no 2>/dev/null && echo " $(GREEN)$(NC) Async SDK tests passed" || echo " $(YELLOW)$(NC) Async tests need dependencies"; \
fi
# ============================================================================
# TEST: Integration Mode
# ============================================================================
test-integration:
@echo ""
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo "INTEGRATION MODE: Testing API contract"
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo ""
@if [ -z "$$UNSANDBOX_PUBLIC_KEY" ] || [ -z "$$UNSANDBOX_SECRET_KEY" ]; then \
echo " $(YELLOW)$(NC) Skipping (no API credentials)"; \
echo " Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY"; \
else \
echo " Testing API authentication..."; \
python3 -c "import sys; sys.path.insert(0, '$(SYNC_DIR)/src'); from unsandbox import UnsandboxClient; c = UnsandboxClient(); r = c.execute('python', 'print(42)'); print(' ✓ Integration: API auth works') if r else print(' ✗ Integration: API auth failed')" 2>/dev/null || echo " $(YELLOW)$(NC) Integration: Need to install SDK first"; \
fi
# ============================================================================
# TEST: Functional Mode
# ============================================================================
test-functional:
@echo ""
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo "FUNCTIONAL MODE: Real-world scenarios"
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo ""
@if [ -z "$$UNSANDBOX_PUBLIC_KEY" ] || [ -z "$$UNSANDBOX_SECRET_KEY" ]; then \
echo " $(YELLOW)$(NC) Skipping (no API credentials)"; \
else \
echo " Running functional tests..."; \
if [ -f "$(SYNC_DIR)/verify_sdk.py" ]; then \
cd $(SYNC_DIR) && python3 verify_sdk.py 2>/dev/null && echo " $(GREEN)$(NC) Functional: Sync SDK verified" || echo " $(YELLOW)$(NC) Functional: Sync verification incomplete"; \
fi; \
fi
# ============================================================================
# TEST: By SDK Type
# ============================================================================
test-sync:
@echo "Testing Sync SDK..."
@if [ -f "$(SYNC_DIR)/Makefile" ]; then \
$(MAKE) -C $(SYNC_DIR) test; \
elif [ -d "$(SYNC_DIR)/tests" ]; then \
cd $(SYNC_DIR) && pytest tests/ -v; \
else \
echo " $(YELLOW)$(NC) Sync SDK tests not found"; \
fi
test-async:
@echo "Testing Async SDK..."
@if [ -f "$(ASYNC_DIR)/Makefile" ]; then \
$(MAKE) -C $(ASYNC_DIR) test; \
elif [ -d "$(ASYNC_DIR)/tests" ]; then \
cd $(ASYNC_DIR) && pytest tests/ -v; \
else \
echo " $(YELLOW)$(NC) Async SDK tests not found"; \
fi
# ============================================================================
# Development
# ============================================================================
install:
@echo "Installing Python SDKs..."
@if [ -f "$(SYNC_DIR)/setup.py" ]; then cd $(SYNC_DIR) && pip install -e . ; fi
@if [ -f "$(ASYNC_DIR)/setup.py" ]; then cd $(ASYNC_DIR) && pip install -e . ; fi
@echo "$(GREEN)$(NC) Installation complete"
dev-install:
@echo "Installing Python SDKs with dev dependencies..."
@if [ -f "$(SYNC_DIR)/setup.py" ]; then cd $(SYNC_DIR) && pip install -e ".[dev]" 2>/dev/null || pip install -e . ; fi
@if [ -f "$(ASYNC_DIR)/setup.py" ]; then cd $(ASYNC_DIR) && pip install -e ".[dev]" 2>/dev/null || pip install -e . ; fi
@pip install pytest pytest-cov pytest-asyncio black flake8 mypy 2>/dev/null || true
@echo "$(GREEN)$(NC) Dev installation complete"
lint:
@echo "Linting Python SDKs..."
@if [ -d "$(SYNC_DIR)/src" ]; then flake8 $(SYNC_DIR)/src/ --max-line-length=120 || true; fi
@if [ -d "$(ASYNC_DIR)/src" ]; then flake8 $(ASYNC_DIR)/src/ --max-line-length=120 || true; fi
@echo "$(GREEN)$(NC) Lint complete"
format:
@echo "Formatting Python SDKs..."
@if [ -d "$(SYNC_DIR)/src" ]; then black $(SYNC_DIR)/src/ $(SYNC_DIR)/tests/ 2>/dev/null || true; fi
@if [ -d "$(ASYNC_DIR)/src" ]; then black $(ASYNC_DIR)/src/ $(ASYNC_DIR)/tests/ 2>/dev/null || true; fi
@echo "$(GREEN)$(NC) Format complete"
examples:
@echo "Running Python examples..."
@if [ -f "$(ASYNC_DIR)/Makefile" ]; then $(MAKE) -C $(ASYNC_DIR) examples; fi
# ============================================================================
# Clean
# ============================================================================
clean:
@echo "Cleaning Python build artifacts..."
@find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
@find . -type f -name "*.pyc" -delete 2>/dev/null || true
@find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true
@find . -type d -name ".mypy_cache" -exec rm -rf {} + 2>/dev/null || true
@find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true
@find . -type d -name "htmlcov" -exec rm -rf {} + 2>/dev/null || true
@find . -type d -name "dist" -exec rm -rf {} + 2>/dev/null || true
@find . -type d -name "build" -exec rm -rf {} + 2>/dev/null || true
@echo "$(GREEN)$(NC) Cleaned build artifacts"

View file

@ -0,0 +1,68 @@
.PHONY: help install dev-install test test-verbose test-coverage lint format clean docs examples
help:
@echo "Unsandbox Async Python SDK - Available targets"
@echo ""
@echo "Installation:"
@echo " make install Install package"
@echo " make dev-install Install with dev dependencies"
@echo ""
@echo "Testing:"
@echo " make test Run tests"
@echo " make test-verbose Run tests with verbose output"
@echo " make test-coverage Run tests with coverage report"
@echo ""
@echo "Code Quality:"
@echo " make lint Lint code with flake8"
@echo " make format Format code with black"
@echo ""
@echo "Other:"
@echo " make clean Remove build artifacts"
@echo " make examples Run examples"
@echo " make docs Show README"
install:
pip install -e .
dev-install:
pip install -e ".[dev]"
test:
pytest tests/ -q
test-verbose:
pytest tests/ -v
test-coverage:
pytest tests/ --cov=un_async --cov-report=html --cov-report=term
lint:
flake8 src/ tests/ examples/
mypy src/ --ignore-missing-imports
format:
black src/ tests/ examples/
clean:
find . -type d -name __pycache__ -exec rm -rf {} +
find . -type f -name "*.pyc" -delete
rm -rf .pytest_cache
rm -rf .mypy_cache
rm -rf htmlcov
rm -rf dist
rm -rf build
rm -rf *.egg-info
examples:
@echo "Running examples..."
@echo ""
@echo "1. Hello World Example:"
python examples/hello_world_async.py
@echo ""
@echo "2. Fibonacci Example:"
python examples/fibonacci_async.py
docs:
@cat README.md
.DEFAULT_GOAL := help

View file

@ -0,0 +1,453 @@
# Unsandbox Async Python SDK
Asynchronous Python SDK for [unsandbox.com](https://unsandbox.com) code execution service.
Execute code in 50+ programming languages with full async/await support in Python.
## Features
- **Fully Asynchronous**: Built on `aiohttp` for efficient concurrent I/O
- **50+ Languages**: Python, JavaScript, Go, Rust, Java, C/C++, and 44+ more
- **Flexible Execution**: Sync execution (blocks until completion) or async (fire-and-forget)
- **Job Management**: Poll, wait, cancel running jobs
- **Credential Management**: 4-tier credential resolution system
- **Request Signing**: HMAC-SHA256 authentication
- **Language Detection**: Automatic language detection from filenames
- **Caching**: Built-in language list caching
- **Concurrent Execution**: Execute multiple jobs concurrently with `asyncio.gather()`
## Installation
```bash
# Clone the repository
git clone https://github.com/unsandbox/un-inception
cd clients/python/async
# Install with development dependencies
pip install -e ".[dev]"
# Or install with just aiohttp
pip install -r requirements.txt
```
## Quick Start
### Basic Async Execution
```python
import asyncio
from un_async import execute_code
async def main():
# Execute code and wait for completion
result = await execute_code("python", 'print("Hello World")')
print(result["stdout"])
asyncio.run(main())
```
### Fire-and-Forget with Polling
```python
import asyncio
from un_async import execute_async, wait_for_job
async def main():
# Start execution (returns immediately)
job_id = await execute_async("javascript", 'console.log("Job started")')
print(f"Job ID: {job_id}")
# Poll for completion
result = await wait_for_job(job_id)
print(f"Status: {result['status']}")
print(f"Output: {result['stdout']}")
asyncio.run(main())
```
### Concurrent Execution
```python
import asyncio
from un_async import execute_code
async def main():
# Run multiple executions concurrently
results = await asyncio.gather(
execute_code("python", "print('Python')"),
execute_code("javascript", "console.log('JavaScript')"),
execute_code("go", 'fmt.Println("Go")'),
)
for result in results:
print(f"Language: {result['language']}, Output: {result['stdout']}")
asyncio.run(main())
```
## Credential Management (4-Tier Priority)
Credentials are resolved in the following order:
1. **Function Arguments** (highest priority)
```python
result = await execute_code(
"python",
"print('hello')",
public_key="your_public_key",
secret_key="your_secret_key"
)
```
2. **Environment Variables**
```bash
export UNSANDBOX_PUBLIC_KEY="your_public_key"
export UNSANDBOX_SECRET_KEY="your_secret_key"
python script.py
```
3. **Config File** (`~/.unsandbox/accounts.csv`)
```
public_key_1,secret_key_1
public_key_2,secret_key_2
# Select account with: export UNSANDBOX_ACCOUNT=1
```
4. **Local Directory** (`./accounts.csv`)
Same format as config file
### Using Multiple Accounts
```bash
# List accounts in ~/.unsandbox/accounts.csv
# Use the second account (0-indexed)
export UNSANDBOX_ACCOUNT=1
python script.py
```
## API Reference
### Execution Functions
#### `execute_code(language, code, public_key=None, secret_key=None)`
Execute code synchronously and wait for completion.
**Args:**
- `language` (str): Programming language (e.g., "python", "javascript")
- `code` (str): Source code to execute
- `public_key` (str, optional): API public key
- `secret_key` (str, optional): API secret key
**Returns:** Dict with execution result
**Raises:** `CredentialsError`, `aiohttp.ClientError`
```python
result = await execute_code("python", "print(42)")
print(result["stdout"]) # "42\n"
print(result["exit_code"]) # 0
```
#### `execute_async(language, code, public_key=None, secret_key=None)`
Execute code asynchronously and return immediately with job ID.
**Args:** Same as `execute_code()`
**Returns:** Job ID (str)
```python
job_id = await execute_async("python", "print('starting')")
# Do other work while job runs...
result = await wait_for_job(job_id)
```
### Job Management Functions
#### `get_job(job_id, public_key=None, secret_key=None)`
Get current status of a job (single poll, no waiting).
**Args:**
- `job_id` (str): Job ID to check
- `public_key`, `secret_key` (optional)
**Returns:** Dict with job status
```python
status = await get_job(job_id)
print(status["status"]) # "running", "completed", "failed", etc.
```
#### `wait_for_job(job_id, public_key=None, secret_key=None)`
Wait for job completion with exponential backoff polling.
**Polling Delays (ms):** [300, 450, 700, 900, 650, 1600, 2000, ...]
**Args:** Same as `get_job()`
**Returns:** Dict with final job result
```python
result = await wait_for_job(job_id)
if result["status"] == "completed":
print(result["stdout"])
```
#### `cancel_job(job_id, public_key=None, secret_key=None)`
Cancel a running job.
**Args:**
- `job_id` (str): Job ID to cancel
- `public_key`, `secret_key` (optional)
**Returns:** Dict with cancellation confirmation
```python
result = await cancel_job(job_id)
print(result["status"]) # "cancelled"
```
#### `list_jobs(public_key=None, secret_key=None)`
List all jobs for the authenticated account.
**Args:** `public_key`, `secret_key` (optional)
**Returns:** List of job dicts
```python
jobs = await list_jobs()
for job in jobs:
print(f"Job {job['id']}: {job['status']}")
```
### Metadata Functions
#### `get_languages(public_key=None, secret_key=None)`
Get list of supported programming languages.
Results are cached for 1 hour in `~/.unsandbox/languages.json`.
**Args:** `public_key`, `secret_key` (optional)
**Returns:** List of language identifiers
```python
languages = await get_languages()
print(f"Supported languages: {', '.join(languages)}")
```
#### `detect_language(filename)`
Detect programming language from filename extension.
**Args:**
- `filename` (str): Filename to detect (e.g., "script.py")
**Returns:** Language identifier or None
```python
lang = detect_language("app.js") # "javascript"
lang = detect_language("main.go") # "go"
lang = detect_language("unknown") # None
```
### Snapshot Functions
#### `session_snapshot(session_id, public_key=None, secret_key=None, name=None, hot=False)`
Create a snapshot of a session.
**Args:**
- `session_id` (str): Session ID to snapshot
- `name` (str, optional): Snapshot name
- `hot` (bool, optional): Hot snapshot (snapshot running session)
**Returns:** Snapshot ID (str)
#### `list_snapshots(public_key=None, secret_key=None)`
List all snapshots.
**Returns:** List of snapshot dicts
#### `restore_snapshot(snapshot_id, public_key=None, secret_key=None)`
Restore a snapshot.
**Args:**
- `snapshot_id` (str): Snapshot ID to restore
**Returns:** Dict with restored resource info
#### `delete_snapshot(snapshot_id, public_key=None, secret_key=None)`
Delete a snapshot.
**Args:**
- `snapshot_id` (str): Snapshot ID to delete
**Returns:** Dict with deletion confirmation
## Response Format
### Successful Execution
```python
{
"job_id": "job_abc123",
"status": "completed",
"stdout": "output text\n",
"stderr": "",
"exit_code": 0,
"language": "python",
"duration_ms": 234
}
```
### Failed Execution
```python
{
"job_id": "job_xyz789",
"status": "failed",
"stdout": "partial output",
"stderr": "Error message\n",
"exit_code": 1,
"language": "python",
"duration_ms": 567
}
```
### Job Statuses
- `pending` - Waiting to execute
- `running` - Currently executing
- `completed` - Finished successfully
- `failed` - Execution error
- `timeout` - Exceeded time limit
- `cancelled` - Cancelled by user
## Examples
See the `examples/` directory for complete working examples:
- `hello_world_async.py` - Basic async execution
- `fibonacci_async.py` - Concurrent fibonacci calculations
- `concurrent_execution.py` - Running multiple jobs concurrently
- `async_job_polling.py` - Fire-and-forget job management
- `sync_blocking_usage.py` - Using sync functions from async library
## Testing
Run the test suite:
```bash
# Install dev dependencies
pip install -e ".[dev]"
# Run all tests
pytest tests/
# Run with verbose output
pytest tests/ -v
# Run specific test file
pytest tests/test_language_detection.py
# Run with coverage
pytest tests/ --cov=un_async
```
### Test Files
- `test_credentials.py` - Credential resolution system
- `test_language_detection.py` - Language detection
- `test_async_operations.py` - Async API operations
- `test_hmac_signing.py` - HMAC request signing
- `conftest.py` - Shared fixtures
## Supported Languages
**50+ Languages** including:
**Interpreted:** Python, JavaScript, Ruby, PHP, Perl, Bash, Lua, R, Julia, Scheme, Tcl, Raku, Clojure, Groovy, Crystal, Dart, Elixir, Erlang, Haskell, OCaml, Common Lisp, Forth, Prolog, and more
**Compiled:** C, C++, Go, Rust, Java, Kotlin, C#, D, Nim, Zig, V, Pascal, Fortran, COBOL, Objective-C, and more
**Specialized:** TypeScript, F#, Odin
Use `detect_language()` for automatic detection or get full list with `await get_languages()`.
## Error Handling
```python
from un_async import CredentialsError
import aiohttp
try:
result = await execute_code("python", "print('hello')")
except CredentialsError as e:
print(f"Credentials error: {e}")
except aiohttp.ClientError as e:
print(f"Network error: {e}")
except Exception as e:
print(f"Unexpected error: {e}")
```
## Performance Tips
1. **Use Concurrent Execution** for multiple independent jobs:
```python
results = await asyncio.gather(
execute_code("python", "..."),
execute_code("go", "..."),
execute_code("rust", "..."),
)
```
2. **Use Exponential Backoff** with `wait_for_job()` instead of polling manually
3. **Cache Languages** - `get_languages()` caches results for 1 hour
4. **Reuse Session** - Create one `aiohttp.ClientSession` for multiple requests:
```python
async with aiohttp.ClientSession() as session:
# Reuse session for multiple operations
```
## Differences from Sync SDK
This async SDK provides the same API as the sync version but with async/await:
**Sync SDK:**
```python
from un import execute_code
result = execute_code("python", "print('hello')")
```
**Async SDK:**
```python
from un_async import execute_code
result = await execute_code("python", "print('hello')")
```
Key differences:
- All I/O functions are async (require `await`)
- Use `asyncio.run()` to execute from sync context
- Use `asyncio.gather()` for concurrent operations
- Built on `aiohttp` instead of `requests`
- Same credential system and HMAC signing
## License
Public Domain - NO LICENSE, NO WARRANTY
## Support
Visit [unsandbox.com](https://unsandbox.com) for API documentation and support.

View file

@ -0,0 +1,58 @@
#!/usr/bin/env python3
"""
Async job polling example - demonstrates fire-and-forget execution
This example shows how to:
1. Start jobs asynchronously with execute_async()
2. Poll job status with get_job()
3. Wait for completion with wait_for_job()
4. Cancel jobs with cancel_job()
Usage:
python async_job_polling.py
Or with custom credentials:
UNSANDBOX_PUBLIC_KEY=... UNSANDBOX_SECRET_KEY=... python async_job_polling.py
"""
import asyncio
import sys
import os
# Add src to path for development
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from un_async import execute_async, get_job, wait_for_job, list_jobs
async def main():
"""Demonstrate async job operations."""
try:
print("1. Starting async job...")
job_id = await execute_async("python", 'print("Job result")')
print(f" Job ID: {job_id}")
print("\n2. Checking job status...")
job_status = await get_job(job_id)
print(f" Status: {job_status.get('status')}")
print("\n3. Waiting for job completion...")
result = await wait_for_job(job_id)
print(f" Final status: {result.get('status')}")
print(f" Output: {result.get('stdout', '').strip()}")
print("\n4. Listing all jobs...")
jobs = await list_jobs()
print(f" Total jobs: {len(jobs)}")
if jobs:
print(f" Most recent job ID: {jobs[0].get('id')}")
return 0
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1
if __name__ == "__main__":
exit_code = asyncio.run(main())
sys.exit(exit_code)

View file

@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""
Concurrent execution example - demonstrates async capabilities
This example shows how to:
1. Execute multiple code snippets concurrently
2. Use asyncio.gather() to wait for all results
3. Handle multiple async operations efficiently
Usage:
python concurrent_execution.py
Or with custom credentials:
UNSANDBOX_PUBLIC_KEY=... UNSANDBOX_SECRET_KEY=... python concurrent_execution.py
"""
import asyncio
import sys
import os
# Add src to path for development
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from un_async import execute_code
async def run_code(language: str, code: str, name: str):
"""Execute code and return result with a name."""
print(f"[{name}] Starting execution...")
result = await execute_code(language, code)
output = result.get("stdout", "").strip()
print(f"[{name}] Result: {output}")
return {"name": name, "result": result}
async def main():
"""Execute multiple code snippets concurrently."""
tasks = [
run_code("python", 'print("Hello from Python")', "python_hello"),
run_code("javascript", 'console.log("Hello from JavaScript")', "js_hello"),
run_code("bash", 'echo "Hello from Bash"', "bash_hello"),
run_code("python", 'import math; print(f"pi = {math.pi:.4f}")', "python_math"),
]
try:
print("Running 4 concurrent code executions...\n")
results = await asyncio.gather(*tasks)
print("\n=== Execution Summary ===")
for result in results:
print(f"{result['name']}: OK")
return 0
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1
if __name__ == "__main__":
exit_code = asyncio.run(main())
sys.exit(exit_code)

View file

@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""
Concurrent HTTP Requests example for unsandbox Python SDK - Asynchronous Version
Demonstrates making multiple concurrent HTTP requests within sandboxed environments.
Shows how to use asyncio for true concurrent execution of network operations.
To run:
export UNSANDBOX_PUBLIC_KEY="your-public-key"
export UNSANDBOX_SECRET_KEY="your-secret-key"
python3 concurrent_requests.py
Expected output:
Starting 3 concurrent HTTP requests...
[request-1] Status: 200, IP: 1.2.3.4
[request-2] Status: 200, IP: 1.2.3.4
[request-3] Status: 200, IP: 1.2.3.4
All requests completed successfully!
"""
import asyncio
import sys
import os
# Add src to path for development
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from un_async import execute_code, CredentialsError
async def run_http_request(request_num: int, url: str, public_key: str, secret_key: str):
"""Execute HTTP request asynchronously."""
code = f"""
import requests
import json
try:
response = requests.get('{url}', timeout=10)
data = response.json()
print(f"Status: {{response.status_code}}, Response: {{json.dumps(data)[:100]}}")
except Exception as e:
print(f"Error: {{e}}")
"""
try:
result = await execute_code("python", code, public_key, secret_key)
output = result.get("stdout", "").strip()
print(f"[request-{request_num}] {output}")
return {"request": request_num, "status": "completed"}
except Exception as e:
print(f"[request-{request_num}] Error: {e}")
return {"request": request_num, "status": "failed"}
async def main():
"""Execute multiple HTTP requests concurrently."""
try:
# Resolve credentials from environment
public_key = os.environ.get("UNSANDBOX_PUBLIC_KEY")
secret_key = os.environ.get("UNSANDBOX_SECRET_KEY")
if not public_key or not secret_key:
print("Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY environment variables required")
print("Run with: export UNSANDBOX_PUBLIC_KEY=your-key UNSANDBOX_SECRET_KEY=your-key")
return 1
# Create concurrent tasks for HTTP requests
print("Starting 3 concurrent HTTP requests...")
tasks = [
run_http_request(1, "https://httpbin.org/ip", public_key, secret_key),
run_http_request(2, "https://httpbin.org/user-agent", public_key, secret_key),
run_http_request(3, "https://httpbin.org/headers", public_key, secret_key),
]
# Wait for all tasks to complete
results = await asyncio.gather(*tasks)
print("All requests completed successfully!")
# Check results
all_completed = all(r.get("status") == "completed" for r in results)
return 0 if all_completed else 1
except CredentialsError as e:
print(f"Credentials error: {e}")
return 1
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
return 1
if __name__ == "__main__":
exit_code = asyncio.run(main())
sys.exit(exit_code)

View file

@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""
Fibonacci example for unsandbox Python SDK - Asynchronous Version
Demonstrates concurrent fibonacci calculations using async/await.
Shows how to run multiple concurrent CPU-bound operations.
To run:
export UNSANDBOX_PUBLIC_KEY="your-public-key"
export UNSANDBOX_SECRET_KEY="your-secret-key"
python3 fibonacci_async.py
Expected output:
Starting 3 concurrent fibonacci calculations...
[fib-10] Result: fib(10) = 55
[fib-15] Result: fib(15) = 610
[fib-12] Result: fib(12) = 144
All calculations completed!
"""
import asyncio
import sys
import os
# Add src to path for development
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from un_async import execute_code, CredentialsError
async def run_fibonacci(n: int, label: str, public_key: str, secret_key: str):
"""Execute fibonacci calculation asynchronously."""
code = f"""
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
print(f"fib({n}) = {{fib({n})}}")
"""
try:
result = await execute_code("python", code, public_key, secret_key)
output = result.get("stdout", "").strip()
print(f"[{label}] Result: {output}")
return {"label": label, "output": output}
except Exception as e:
print(f"[{label}] Error: {e}")
return {"label": label, "error": str(e)}
async def main():
"""Execute multiple fibonacci calculations concurrently."""
try:
# Resolve credentials from environment
public_key = os.environ.get("UNSANDBOX_PUBLIC_KEY")
secret_key = os.environ.get("UNSANDBOX_SECRET_KEY")
if not public_key or not secret_key:
print("Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY environment variables required")
print("Run with: export UNSANDBOX_PUBLIC_KEY=your-key UNSANDBOX_SECRET_KEY=your-key")
return 1
# Create concurrent tasks for different fibonacci values
print("Starting 3 concurrent fibonacci calculations...")
tasks = [
run_fibonacci(10, "fib-10", public_key, secret_key),
run_fibonacci(15, "fib-15", public_key, secret_key),
run_fibonacci(12, "fib-12", public_key, secret_key),
]
# Wait for all tasks to complete
results = await asyncio.gather(*tasks)
print("All calculations completed!")
# Check for errors
has_errors = any("error" in result for result in results)
return 1 if has_errors else 0
except CredentialsError as e:
print(f"Credentials error: {e}")
return 1
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
return 1
if __name__ == "__main__":
exit_code = asyncio.run(main())
sys.exit(exit_code)

View file

@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""
Hello World example for unsandbox Python SDK - Asynchronous Version
This example demonstrates basic async execution with the unsandbox SDK.
Shows how to use asyncio with the async SDK client for simple code execution.
To run:
export UNSANDBOX_PUBLIC_KEY="your-public-key"
export UNSANDBOX_SECRET_KEY="your-secret-key"
python3 hello_world_async.py
Expected output:
Executing code asynchronously...
Result status: completed
Output: Hello from async unsandbox!
"""
import asyncio
import sys
import os
# Add src to path for development
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from un_async import execute_code, CredentialsError
async def main():
"""Execute hello world code asynchronously."""
# The code to execute
code = 'print("Hello from async unsandbox!")'
try:
# Resolve credentials from environment
public_key = os.environ.get("UNSANDBOX_PUBLIC_KEY")
secret_key = os.environ.get("UNSANDBOX_SECRET_KEY")
if not public_key or not secret_key:
print("Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY environment variables required")
print("Run with: export UNSANDBOX_PUBLIC_KEY=your-key UNSANDBOX_SECRET_KEY=your-key")
return 1
# Execute the code asynchronously
print("Executing code asynchronously...")
result = await execute_code("python", code, public_key, secret_key)
# Check for errors
if result.get("status") == "completed":
print(f"Result status: {result.get('status')}")
print(f"Output: {result.get('stdout', '').strip()}")
if result.get("stderr"):
print(f"Errors: {result.get('stderr', '')}")
return 0
else:
print(f"Execution failed with status: {result.get('status')}")
print(f"Error: {result.get('error', 'Unknown error')}")
return 1
except CredentialsError as e:
print(f"Credentials error: {e}")
return 1
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
return 1
if __name__ == "__main__":
exit_code = asyncio.run(main())
sys.exit(exit_code)

View file

@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""
Stream Processing example for unsandbox Python SDK - Asynchronous Version
Demonstrates async generator patterns and streaming data processing.
Shows how to handle potentially large datasets with async/await.
To run:
export UNSANDBOX_PUBLIC_KEY="your-public-key"
export UNSANDBOX_SECRET_KEY="your-secret-key"
python3 stream_processing.py
Expected output:
Processing stream of data...
[stream-task-1] Processed 10 items, sum: 45
[stream-task-2] Processed 10 items, sum: 145
[stream-task-3] Processed 10 items, sum: 245
Stream processing completed!
"""
import asyncio
import sys
import os
# Add src to path for development
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from un_async import execute_code, CredentialsError
async def run_stream_task(task_num: int, start: int, count: int, public_key: str, secret_key: str):
"""Execute stream processing task asynchronously."""
code = f"""
# Simulate stream processing with generator
def stream_generator(start, count):
for i in range(start, start + count):
yield i
# Process stream
total = 0
item_count = 0
for item in stream_generator({start}, {count}):
total += item
item_count += 1
print(f"Processed {{item_count}} items, sum: {{total}}")
"""
try:
result = await execute_code("python", code, public_key, secret_key)
output = result.get("stdout", "").strip()
print(f"[stream-task-{task_num}] {output}")
return {"task": task_num, "status": "completed"}
except Exception as e:
print(f"[stream-task-{task_num}] Error: {e}")
return {"task": task_num, "status": "failed"}
async def main():
"""Execute multiple stream processing tasks concurrently."""
try:
# Resolve credentials from environment
public_key = os.environ.get("UNSANDBOX_PUBLIC_KEY")
secret_key = os.environ.get("UNSANDBOX_SECRET_KEY")
if not public_key or not secret_key:
print("Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY environment variables required")
print("Run with: export UNSANDBOX_PUBLIC_KEY=your-key UNSANDBOX_SECRET_KEY=your-key")
return 1
# Create concurrent tasks for stream processing
print("Processing stream of data...")
tasks = [
run_stream_task(1, 0, 10, public_key, secret_key),
run_stream_task(2, 10, 10, public_key, secret_key),
run_stream_task(3, 20, 10, public_key, secret_key),
]
# Wait for all tasks to complete
results = await asyncio.gather(*tasks)
print("Stream processing completed!")
# Check results
all_completed = all(r.get("status") == "completed" for r in results)
return 0 if all_completed else 1
except CredentialsError as e:
print(f"Credentials error: {e}")
return 1
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
return 1
if __name__ == "__main__":
exit_code = asyncio.run(main())
sys.exit(exit_code)

View file

@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""
Sync (blocking) operations from async library
This example shows how the async library also supports synchronous usage:
1. Using synchronous/blocking functions directly
2. Running async code from blocking context with asyncio.run()
3. Mixing sync and async patterns
Usage:
python sync_blocking_usage.py
Or with custom credentials:
UNSANDBOX_PUBLIC_KEY=... UNSANDBOX_SECRET_KEY=... python sync_blocking_usage.py
"""
import asyncio
import sys
import os
# Add src to path for development
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from un_async import (
execute_code,
detect_language,
get_languages,
)
async def async_approach():
"""Using async/await syntax."""
print("=== Async Approach ===")
result = await execute_code("python", 'print("Hello from async")')
print(f"Output: {result.get('stdout', '').strip()}\n")
async def blocking_approach():
"""Using synchronous/blocking functions in async context."""
print("=== Sync Functions (in async context) ===")
# These are synchronous functions that don't need await
lang = detect_language("script.py")
print(f"Detected language for script.py: {lang}\n")
# But we still need to await execute_code since it's async
result = await execute_code("python", f'print("Executing {lang} code")')
print(f"Output: {result.get('stdout', '').strip()}\n")
async def mixed_approach():
"""Mixing sync and async calls."""
print("=== Mixed Sync/Async ===")
# Synchronous call (no await needed)
langs = get_languages.__doc__ # Just accessing the doc string
print("get_languages is available for fetching supported languages\n")
# Async call (await needed)
result = await execute_code("javascript", 'console.log("Hello from mixed")')
print(f"Output: {result.get('stdout', '').strip()}\n")
async def main():
"""Demonstrate various usage patterns."""
try:
await async_approach()
await blocking_approach()
await mixed_approach()
return 0
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1
if __name__ == "__main__":
exit_code = asyncio.run(main())
sys.exit(exit_code)

View file

@ -0,0 +1,9 @@
# Core dependencies
aiohttp>=3.8.0
# Development dependencies (optional)
pytest>=7.0
pytest-asyncio>=0.20.0
black>=22.0
flake8>=4.0
mypy>=0.950

View file

@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""
Setup script for unsandbox async Python SDK
"""
from setuptools import setup, find_packages
setup(
name="unsandbox-async",
version="1.0.0",
description="Asynchronous Python SDK for unsandbox.com code execution",
long_description=open("README.md").read() if False else "Async Python SDK for unsandbox code execution",
author="unsandbox.com",
url="https://github.com/unsandbox/un-inception",
license="Public Domain",
packages=find_packages(where="src"),
package_dir={"": "src"},
python_requires=">=3.7",
install_requires=[
"aiohttp>=3.8.0",
],
extras_require={
"dev": [
"pytest>=7.0",
"pytest-asyncio>=0.20.0",
"black>=22.0",
"flake8>=4.0",
"mypy>=0.950",
],
},
entry_points={},
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: Public Domain License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Internet :: WWW/HTTP",
],
)

View file

@ -0,0 +1,3 @@
"""
Tests for unsandbox async SDK
"""

View file

@ -0,0 +1,48 @@
"""
Pytest configuration for async tests
"""
import pytest
import sys
import os
# Add src to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
@pytest.fixture
def mock_api_key():
"""Provide a mock API key for testing."""
return "test_public_key"
@pytest.fixture
def mock_secret_key():
"""Provide a mock secret key for testing."""
return "test_secret_key"
@pytest.fixture
def mock_job_id():
"""Provide a mock job ID for testing."""
return "job_12345_abcde"
@pytest.fixture
def mock_code():
"""Provide mock code for testing."""
return 'print("Hello World")'
@pytest.fixture
def mock_result():
"""Provide a mock API result."""
return {
"job_id": "job_12345",
"status": "completed",
"stdout": "Hello World\n",
"stderr": "",
"exit_code": 0,
"language": "python",
"duration_ms": 120,
}

View file

@ -0,0 +1,210 @@
"""
Tests for async operations (requires mocking or real API access)
"""
import pytest
import asyncio
import sys
import os
from unittest.mock import AsyncMock, patch, MagicMock
# Add src to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
import un_async
from un_async import (
execute_code,
execute_async,
get_job,
wait_for_job,
cancel_job,
list_jobs,
)
@pytest.fixture
def mock_credentials(monkeypatch):
"""Provide mock credentials."""
monkeypatch.setenv("UNSANDBOX_PUBLIC_KEY", "test_public_key")
monkeypatch.setenv("UNSANDBOX_SECRET_KEY", "test_secret_key")
class TestAsyncOperations:
"""Test async API operations."""
@pytest.mark.asyncio
async def test_execute_code_completed(self, mock_credentials):
"""Test execute_code when job completes immediately."""
mock_response = {
"job_id": "job_123",
"status": "completed",
"stdout": "Hello World\n",
"stderr": "",
"exit_code": 0,
}
with patch("un_async._make_request", new_callable=AsyncMock) as mock_req:
# First call for execute, then wait_for_job polls
mock_req.side_effect = [
mock_response, # execute_code calls _make_request
]
result = await execute_code("python", "print('Hello World')")
assert result["status"] == "completed"
assert "Hello World" in result["stdout"]
assert result["exit_code"] == 0
@pytest.mark.asyncio
async def test_execute_async_returns_job_id(self, mock_credentials):
"""Test execute_async returns job_id immediately."""
mock_response = {
"job_id": "job_456",
"status": "pending",
}
with patch("un_async._make_request", new_callable=AsyncMock) as mock_req:
mock_req.return_value = mock_response
job_id = await execute_async("python", "print('test')")
assert job_id == "job_456"
mock_req.assert_called_once()
@pytest.mark.asyncio
async def test_get_job(self, mock_credentials):
"""Test getting job status."""
mock_response = {
"job_id": "job_789",
"status": "running",
}
with patch("un_async._make_request", new_callable=AsyncMock) as mock_req:
mock_req.return_value = mock_response
result = await get_job("job_789")
assert result["job_id"] == "job_789"
assert result["status"] == "running"
@pytest.mark.asyncio
async def test_wait_for_job_polling(self, mock_credentials):
"""Test wait_for_job polls until completion."""
# Simulate polling: pending -> running -> completed
mock_responses = [
{"job_id": "job_100", "status": "pending"},
{"job_id": "job_100", "status": "running"},
{"job_id": "job_100", "status": "completed", "stdout": "done"},
]
with patch("un_async._make_request", new_callable=AsyncMock) as mock_req:
mock_req.side_effect = mock_responses
with patch("asyncio.sleep", new_callable=AsyncMock):
result = await wait_for_job("job_100")
assert result["status"] == "completed"
assert result["stdout"] == "done"
assert mock_req.call_count == 3
@pytest.mark.asyncio
async def test_cancel_job(self, mock_credentials):
"""Test cancelling a job."""
mock_response = {
"job_id": "job_kill",
"status": "cancelled",
}
with patch("un_async._make_request", new_callable=AsyncMock) as mock_req:
mock_req.return_value = mock_response
result = await cancel_job("job_kill")
assert result["status"] == "cancelled"
@pytest.mark.asyncio
async def test_list_jobs(self, mock_credentials):
"""Test listing all jobs."""
mock_response = {
"jobs": [
{"id": "job_1", "status": "completed"},
{"id": "job_2", "status": "running"},
]
}
with patch("un_async._make_request", new_callable=AsyncMock) as mock_req:
mock_req.return_value = mock_response
jobs = await list_jobs()
assert len(jobs) == 2
assert jobs[0]["id"] == "job_1"
assert jobs[1]["id"] == "job_2"
class TestAsyncConcurrency:
"""Test concurrent async operations."""
@pytest.mark.asyncio
async def test_concurrent_executions(self, mock_credentials):
"""Test running multiple executions concurrently."""
mock_response = {
"job_id": "job_concurrent",
"status": "completed",
"stdout": "result",
}
with patch("un_async._make_request", new_callable=AsyncMock) as mock_req:
mock_req.return_value = mock_response
# Run 3 concurrent executions
results = await asyncio.gather(
execute_code("python", "print(1)"),
execute_code("javascript", "console.log(2)"),
execute_code("bash", "echo 3"),
)
assert len(results) == 3
assert all(r["status"] == "completed" for r in results)
@pytest.mark.asyncio
async def test_concurrent_with_different_statuses(self, mock_credentials):
"""Test concurrent operations with different final statuses."""
responses = [
{"job_id": "1", "status": "completed", "stdout": "ok"},
{"job_id": "2", "status": "failed", "stderr": "error"},
{"job_id": "3", "status": "timeout"},
]
with patch("un_async._make_request", new_callable=AsyncMock) as mock_req:
mock_req.side_effect = responses
results = await asyncio.gather(
execute_code("python", "print(1)"),
execute_code("python", "raise Exception()"),
execute_code("python", "while True: pass"),
)
assert results[0]["status"] == "completed"
assert results[1]["status"] == "failed"
assert results[2]["status"] == "timeout"
class TestAsyncSignature:
"""Test that functions have async signatures."""
def test_functions_are_coroutines(self):
"""Verify key functions are async."""
import inspect
# These should be coroutine functions
assert inspect.iscoroutinefunction(execute_code)
assert inspect.iscoroutinefunction(execute_async)
assert inspect.iscoroutinefunction(get_job)
assert inspect.iscoroutinefunction(wait_for_job)
assert inspect.iscoroutinefunction(cancel_job)
assert inspect.iscoroutinefunction(list_jobs)
# These should NOT be coroutine functions (sync helpers)
assert not inspect.iscoroutinefunction(un_async.detect_language)
assert not inspect.iscoroutinefunction(un_async._resolve_credentials)

View file

@ -0,0 +1,104 @@
"""
Tests for credential resolution system
"""
import pytest
import os
import tempfile
from pathlib import Path
import sys
# Add src to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from un_async import (
CredentialsError,
_resolve_credentials,
_load_credentials_from_csv,
_get_unsandbox_dir,
)
class TestCredentialResolution:
"""Test 4-tier credential resolution system."""
def test_resolve_credentials_from_arguments(self):
"""Test Tier 1: Function arguments take highest priority."""
pk, sk = _resolve_credentials("arg_pk", "arg_sk")
assert pk == "arg_pk"
assert sk == "arg_sk"
def test_resolve_credentials_from_env(self, monkeypatch):
"""Test Tier 2: Environment variables."""
monkeypatch.setenv("UNSANDBOX_PUBLIC_KEY", "env_pk")
monkeypatch.setenv("UNSANDBOX_SECRET_KEY", "env_sk")
# Clear any CSV-based credentials by not providing arguments
pk, sk = _resolve_credentials(None, None)
assert pk == "env_pk"
assert sk == "env_sk"
def test_credentials_error_no_sources(self, monkeypatch):
"""Test CredentialsError when no credentials found."""
# Clear environment
monkeypatch.delenv("UNSANDBOX_PUBLIC_KEY", raising=False)
monkeypatch.delenv("UNSANDBOX_SECRET_KEY", raising=False)
with pytest.raises(CredentialsError) as exc_info:
_resolve_credentials(None, None)
assert "No credentials found" in str(exc_info.value)
def test_arguments_override_env(self, monkeypatch):
"""Test that function arguments override environment variables."""
monkeypatch.setenv("UNSANDBOX_PUBLIC_KEY", "env_pk")
monkeypatch.setenv("UNSANDBOX_SECRET_KEY", "env_sk")
pk, sk = _resolve_credentials("arg_pk", "arg_sk")
assert pk == "arg_pk"
assert sk == "arg_sk"
class TestCSVCredentials:
"""Test CSV credential loading."""
def test_load_csv_valid(self):
"""Test loading valid CSV file."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as f:
f.write("public_key_1,secret_key_1\n")
f.write("public_key_2,secret_key_2\n")
temp_path = f.name
try:
# Load first account
creds = _load_credentials_from_csv(Path(temp_path), 0)
assert creds == ("public_key_1", "secret_key_1")
# Load second account
creds = _load_credentials_from_csv(Path(temp_path), 1)
assert creds == ("public_key_2", "secret_key_2")
finally:
os.unlink(temp_path)
def test_load_csv_nonexistent(self):
"""Test loading from nonexistent file."""
creds = _load_credentials_from_csv(Path("/nonexistent/path.csv"), 0)
assert creds is None
def test_load_csv_with_comments(self):
"""Test loading CSV with comment lines."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as f:
f.write("# This is a comment\n")
f.write("public_key_1,secret_key_1\n")
temp_path = f.name
try:
creds = _load_credentials_from_csv(Path(temp_path), 0)
assert creds == ("public_key_1", "secret_key_1")
finally:
os.unlink(temp_path)
def test_get_unsandbox_dir(self):
"""Test getting ~/.unsandbox directory."""
unsandbox_dir = _get_unsandbox_dir()
assert unsandbox_dir.exists()
assert unsandbox_dir.name == ".unsandbox"

View file

@ -0,0 +1,206 @@
"""
Tests for HMAC request signing
"""
import pytest
import sys
import os
# Add src to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from un_async import _sign_request
class TestHMACSignature:
"""Test HMAC-SHA256 request signing."""
def test_sign_request_basic(self):
"""Test basic request signing."""
signature = _sign_request(
secret_key="secret",
timestamp=1234567890,
method="POST",
path="/execute",
body='{"code":"test"}',
)
# Signature should be a 64-character hex string
assert isinstance(signature, str)
assert len(signature) == 64
assert all(c in "0123456789abcdef" for c in signature)
def test_sign_request_deterministic(self):
"""Test that signing is deterministic."""
sig1 = _sign_request(
secret_key="secret",
timestamp=1234567890,
method="POST",
path="/execute",
body='{"code":"test"}',
)
sig2 = _sign_request(
secret_key="secret",
timestamp=1234567890,
method="POST",
path="/execute",
body='{"code":"test"}',
)
assert sig1 == sig2
def test_sign_request_different_secrets(self):
"""Test that different secrets produce different signatures."""
sig1 = _sign_request(
secret_key="secret1",
timestamp=1234567890,
method="POST",
path="/execute",
body='{"code":"test"}',
)
sig2 = _sign_request(
secret_key="secret2",
timestamp=1234567890,
method="POST",
path="/execute",
body='{"code":"test"}',
)
assert sig1 != sig2
def test_sign_request_different_timestamps(self):
"""Test that different timestamps produce different signatures."""
sig1 = _sign_request(
secret_key="secret",
timestamp=1234567890,
method="POST",
path="/execute",
body='{"code":"test"}',
)
sig2 = _sign_request(
secret_key="secret",
timestamp=1234567891,
method="POST",
path="/execute",
body='{"code":"test"}',
)
assert sig1 != sig2
def test_sign_request_different_methods(self):
"""Test that different HTTP methods produce different signatures."""
sig1 = _sign_request(
secret_key="secret",
timestamp=1234567890,
method="POST",
path="/execute",
body='{"code":"test"}',
)
sig2 = _sign_request(
secret_key="secret",
timestamp=1234567890,
method="GET",
path="/execute",
body='{"code":"test"}',
)
assert sig1 != sig2
def test_sign_request_different_paths(self):
"""Test that different paths produce different signatures."""
sig1 = _sign_request(
secret_key="secret",
timestamp=1234567890,
method="GET",
path="/jobs/123",
body=None,
)
sig2 = _sign_request(
secret_key="secret",
timestamp=1234567890,
method="GET",
path="/jobs/456",
body=None,
)
assert sig1 != sig2
def test_sign_request_empty_body(self):
"""Test signing with empty/no body."""
sig1 = _sign_request(
secret_key="secret",
timestamp=1234567890,
method="GET",
path="/languages",
body=None,
)
sig2 = _sign_request(
secret_key="secret",
timestamp=1234567890,
method="GET",
path="/languages",
body="",
)
# Both should produce valid signatures
assert isinstance(sig1, str) and len(sig1) == 64
assert isinstance(sig2, str) and len(sig2) == 64
def test_sign_request_special_characters_in_body(self):
"""Test signing with special characters in body."""
body_with_special = '{"code":"print(\\"hello\\")"}'
signature = _sign_request(
secret_key="secret",
timestamp=1234567890,
method="POST",
path="/execute",
body=body_with_special,
)
assert isinstance(signature, str)
assert len(signature) == 64
def test_sign_request_unicode_in_secret(self):
"""Test signing with unicode characters in secret."""
# Note: In production, secrets should be ASCII, but test unicode handling
signature = _sign_request(
secret_key="secret_with_unicode_αβγ",
timestamp=1234567890,
method="POST",
path="/execute",
body='{"code":"test"}',
)
assert isinstance(signature, str)
assert len(signature) == 64
def test_sign_request_message_format(self):
"""Verify the message format is correct: timestamp:METHOD:path:body"""
# We can't directly inspect the message, but we can verify the signature
# matches what we'd expect if we implement it ourselves
import hmac
import hashlib
secret = "test_secret"
timestamp = 1234567890
method = "POST"
path = "/execute"
body = '{"test":"data"}'
# Build expected message
expected_message = f"{timestamp}:{method}:{path}:{body}"
expected_signature = hmac.new(
secret.encode(),
expected_message.encode(),
hashlib.sha256,
).hexdigest()
# Compare with function output
actual_signature = _sign_request(secret, timestamp, method, path, body)
assert actual_signature == expected_signature

View file

@ -0,0 +1,126 @@
"""
Tests for language detection
"""
import pytest
import sys
import os
# Add src to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from un_async import detect_language
class TestLanguageDetection:
"""Test language detection from filename."""
def test_detect_python(self):
"""Test Python detection."""
assert detect_language("script.py") == "python"
assert detect_language("test.py") == "python"
assert detect_language("/path/to/script.py") == "python"
def test_detect_javascript(self):
"""Test JavaScript detection."""
assert detect_language("app.js") == "javascript"
assert detect_language("index.js") == "javascript"
def test_detect_typescript(self):
"""Test TypeScript detection."""
assert detect_language("app.ts") == "typescript"
def test_detect_go(self):
"""Test Go detection."""
assert detect_language("main.go") == "go"
def test_detect_rust(self):
"""Test Rust detection."""
assert detect_language("lib.rs") == "rust"
def test_detect_c(self):
"""Test C detection."""
assert detect_language("program.c") == "c"
def test_detect_cpp(self):
"""Test C++ detection."""
assert detect_language("app.cpp") == "cpp"
assert detect_language("app.cc") == "cpp"
assert detect_language("app.cxx") == "cpp"
def test_detect_ruby(self):
"""Test Ruby detection."""
assert detect_language("script.rb") == "ruby"
def test_detect_java(self):
"""Test Java detection."""
assert detect_language("Main.java") == "java"
def test_detect_bash(self):
"""Test Bash detection."""
assert detect_language("script.sh") == "bash"
def test_detect_unknown(self):
"""Test unknown extension returns None."""
assert detect_language("script.xyz") is None
assert detect_language("no_extension") is None
assert detect_language("") is None
assert detect_language(None) is None
def test_detect_case_insensitive(self):
"""Test that detection is case-insensitive for extensions."""
assert detect_language("script.PY") == "python"
assert detect_language("script.Py") == "python"
assert detect_language("SCRIPT.JS") == "javascript"
def test_detect_with_dots_in_path(self):
"""Test detection with dots in path."""
assert detect_language("/path/to/my.code/script.py") == "python"
assert detect_language("~/code.dir/app.js") == "javascript"
def test_detect_all_supported_languages(self):
"""Test detection for all supported languages."""
test_cases = {
"py": "python",
"js": "javascript",
"ts": "typescript",
"rb": "ruby",
"php": "php",
"pl": "perl",
"sh": "bash",
"r": "r",
"R": "r",
"lua": "lua",
"go": "go",
"rs": "rust",
"c": "c",
"cpp": "cpp",
"java": "java",
"kt": "kotlin",
"cs": "csharp",
"fs": "fsharp",
"hs": "haskell",
"ml": "ocaml",
"clj": "clojure",
"scm": "scheme",
"erl": "erlang",
"ex": "elixir",
"jl": "julia",
"d": "d",
"nim": "nim",
"zig": "zig",
"v": "v",
"cr": "crystal",
"dart": "dart",
"groovy": "groovy",
"lisp": "commonlisp",
"cob": "cobol",
"tcl": "tcl",
"raku": "raku",
"pro": "prolog",
"4th": "forth",
}
for ext, expected_lang in test_cases.items():
filename = f"test.{ext}"
assert detect_language(filename) == expected_lang, f"Failed for {filename}"

View file

@ -0,0 +1,219 @@
# Unsandbox Python SDK (Synchronous)
A synchronous Python client library for [unsandbox.com](https://unsandbox.com) - secure, multi-language code execution.
## Installation
```bash
pip install unsandbox
```
Or from source:
```bash
cd clients/python/sync
pip install -e .
```
## Quick Start
```python
from un import execute_code
# Execute Python code
result = execute_code("python", 'print("Hello from unsandbox!")')
print(result)
```
## Authentication
The SDK supports 4-tier credential resolution:
1. **Function arguments** - Pass directly to functions
2. **Environment variables** - `UNSANDBOX_PUBLIC_KEY` and `UNSANDBOX_SECRET_KEY`
3. **Config file** - `~/.unsandbox/accounts.csv` (line 0 by default)
4. **Local directory** - `./accounts.csv` (line 0 by default)
### Setting up credentials
Create `~/.unsandbox/accounts.csv`:
```csv
your_public_key,your_secret_key
another_public_key,another_secret_key
```
Or use environment variables:
```bash
export UNSANDBOX_PUBLIC_KEY="pk_xxxxx"
export UNSANDBOX_SECRET_KEY="sk_xxxxx"
```
## API Reference
### Synchronous Execution
Execute code and wait for completion:
```python
from un import execute_code
result = execute_code(
language="python",
code="print('hello')",
public_key=None, # Optional, uses credential resolution
secret_key=None, # Optional, uses credential resolution
)
print(result)
# Output: {
# 'status': 'completed',
# 'stdout': 'hello\n',
# 'stderr': '',
# 'exit_code': 0,
# 'runtime_ms': 342
# }
```
### Asynchronous Execution
Start execution and get a job ID:
```python
from un import execute_async, wait_for_job
# Start execution
job_id = execute_async("python", "print('hello')")
# Check status later
result = wait_for_job(job_id)
```
### Job Management
```python
from un import get_job, cancel_job, list_jobs
# Get single job status
job = get_job("job_123")
# List all active jobs
jobs = list_jobs()
# Cancel a job
cancel_job("job_123")
```
### Languages
```python
from un import get_languages, detect_language
# Get list of supported languages
languages = get_languages()
# Returns: ['python', 'javascript', 'go', 'rust', ...]
# Detect language from filename
lang = detect_language("script.py") # Returns 'python'
```
### Snapshots
```python
from un import session_snapshot, list_snapshots, restore_snapshot, delete_snapshot
# Create a snapshot
snapshot_id = session_snapshot("session_123", name="checkpoint")
# List snapshots
snapshots = list_snapshots()
# Restore a snapshot
result = restore_snapshot(snapshot_id)
# Delete a snapshot
delete_snapshot(snapshot_id)
```
## Language Support
The SDK supports 50+ programming languages including:
- **Interpreted**: Python, JavaScript, Ruby, PHP, Perl, Bash, etc.
- **Compiled**: C, C++, Go, Rust, Java, etc.
- **Functional**: Haskell, OCaml, F#, Scheme, etc.
- **Other**: WASM, Prolog, Forth, etc.
See `get_languages()` for the complete list.
## Caching
The languages list is cached locally for 1 hour in `~/.unsandbox/languages.json`. This reduces API calls and improves startup performance.
To force a refresh, delete the cache file:
```bash
rm ~/.unsandbox/languages.json
```
## Error Handling
```python
from un import execute_code, CredentialsError
import requests
try:
result = execute_code("python", "print('hello')")
except CredentialsError:
print("No credentials found")
except requests.RequestException as e:
print(f"Network error: {e}")
except ValueError as e:
print(f"Invalid response: {e}")
```
## Examples
See the `examples/` directory for complete working examples:
- `hello_world.py` - Simple print example
- `fibonacci.py` - Recursive function example
Run an example:
```bash
cd examples
python hello_world.py
```
## Testing
Run the test suite:
```bash
pip install -e ".[dev]"
pytest tests/ -v
```
With coverage:
```bash
pytest tests/ --cov=un --cov-report=html
```
## Public Domain License
This code is released into the PUBLIC DOMAIN with NO WARRANTY and NO LICENSE.
You are free to:
- Use for any purpose
- Modify and distribute
- Use commercially
- Use privately
## Support
For issues or questions:
- GitHub Issues: https://github.com/unsandbox/un-inception/issues
- Website: https://unsandbox.com

View file

@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""
Fibonacci Client example for unsandbox Python SDK - Synchronous Version
Demonstrates executing CPU-bound calculations through the sync SDK.
Shows proper error handling and result processing.
To run:
export UNSANDBOX_PUBLIC_KEY="your-public-key"
export UNSANDBOX_SECRET_KEY="your-secret-key"
python3 fibonacci_client.py
Expected output:
Calculating fibonacci(10)...
Result status: completed
Output: fib(10) = 55
"""
import sys
import os
# Add the SDK path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from un import execute_code, CredentialsError
def main():
"""Execute fibonacci calculation using the SDK."""
# The code to execute
code = """
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
print(f"fib(10) = {fib(10)}")
"""
try:
# Resolve credentials from environment
public_key = os.environ.get("UNSANDBOX_PUBLIC_KEY")
secret_key = os.environ.get("UNSANDBOX_SECRET_KEY")
if not public_key or not secret_key:
print("Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY environment variables required")
print("Run with: export UNSANDBOX_PUBLIC_KEY=your-key UNSANDBOX_SECRET_KEY=your-key")
sys.exit(1)
# Execute the code synchronously
print("Calculating fibonacci(10)...")
result = execute_code("python", code, public_key, secret_key)
# Check for errors
if result.get("status") == "completed":
print(f"Result status: {result.get('status')}")
print(f"Output: {result.get('stdout', '').strip()}")
if result.get("stderr"):
print(f"Errors: {result.get('stderr', '')}")
else:
print(f"Execution failed with status: {result.get('status')}")
print(f"Error: {result.get('error', 'Unknown error')}")
sys.exit(1)
except CredentialsError as e:
print(f"Credentials error: {e}")
sys.exit(1)
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""
File Operations example for unsandbox Python SDK - Synchronous Version
This example demonstrates reading and writing files in sandboxed environments.
Shows temporary file creation and manipulation within the sandbox.
To run:
export UNSANDBOX_PUBLIC_KEY="your-public-key"
export UNSANDBOX_SECRET_KEY="your-secret-key"
python3 file_operations.py
Expected output:
File created at: /tmp/example.txt
File contents:
Line 1: Hello from the sandbox
Line 2: This is temporary storage
Line 3: File operations work!
Total lines written: 3
"""
import sys
import os
# Add the SDK path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from un import execute_code, CredentialsError
def main():
"""Execute file operations code in sandbox."""
# The code to execute - demonstrates file I/O
code = """
import os
import tempfile
# Create temporary file
temp_file = "/tmp/example.txt"
try:
# Write to file
with open(temp_file, "w") as f:
f.write("Line 1: Hello from the sandbox\\n")
f.write("Line 2: This is temporary storage\\n")
f.write("Line 3: File operations work!\\n")
print(f"File created at: {temp_file}")
# Check if file exists
if os.path.exists(temp_file):
print(f"File exists: {os.path.isfile(temp_file)}")
# Get file size
file_size = os.path.getsize(temp_file)
print(f"File size: {file_size} bytes")
# Read from file
print("File contents:")
with open(temp_file, "r") as f:
lines = f.readlines()
for line in lines:
print(line.rstrip())
print(f"Total lines written: {len(lines)}")
except IOError as e:
print(f"File operation error: {e}")
except Exception as e:
print(f"Error: {e}")
"""
try:
# Resolve credentials from environment
public_key = os.environ.get("UNSANDBOX_PUBLIC_KEY")
secret_key = os.environ.get("UNSANDBOX_SECRET_KEY")
if not public_key or not secret_key:
print("Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY environment variables required")
print("Run with: export UNSANDBOX_PUBLIC_KEY=your-key UNSANDBOX_SECRET_KEY=your-key")
sys.exit(1)
# Execute the code
print("Executing file operations in sandbox...")
result = execute_code("python", code, public_key, secret_key)
# Check for errors
if result.get("status") == "completed":
print("\n=== STDOUT ===")
print(result.get("stdout", ""))
if result.get("stderr"):
print("\n=== STDERR ===")
print(result.get("stderr", ""))
else:
print(f"Execution failed with status: {result.get('status')}")
print(f"Error: {result.get('error', 'Unknown error')}")
sys.exit(1)
except CredentialsError as e:
print(f"Credentials error: {e}")
sys.exit(1)
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""
Hello World Client example for unsandbox Python SDK - Synchronous Version
This example demonstrates basic synchronous execution using the SDK client.
Shows how to execute code from a Python program using the sync SDK.
To run:
export UNSANDBOX_PUBLIC_KEY="your-public-key"
export UNSANDBOX_SECRET_KEY="your-secret-key"
python3 hello_world_client.py
Expected output:
Executing code synchronously...
Result status: completed
Output: Hello from unsandbox!
"""
import sys
import os
# Add the SDK path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from un import execute_code, CredentialsError
def main():
"""Execute hello world code using the SDK."""
# The code to execute
code = 'print("Hello from unsandbox!")'
try:
# Resolve credentials from environment
public_key = os.environ.get("UNSANDBOX_PUBLIC_KEY")
secret_key = os.environ.get("UNSANDBOX_SECRET_KEY")
if not public_key or not secret_key:
print("Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY environment variables required")
print("Run with: export UNSANDBOX_PUBLIC_KEY=your-key UNSANDBOX_SECRET_KEY=your-key")
sys.exit(1)
# Execute the code synchronously
print("Executing code synchronously...")
result = execute_code("python", code, public_key, secret_key)
# Check for errors
if result.get("status") == "completed":
print(f"Result status: {result.get('status')}")
print(f"Output: {result.get('stdout', '').strip()}")
if result.get("stderr"):
print(f"Errors: {result.get('stderr', '')}")
else:
print(f"Execution failed with status: {result.get('status')}")
print(f"Error: {result.get('error', 'Unknown error')}")
sys.exit(1)
except CredentialsError as e:
print(f"Credentials error: {e}")
sys.exit(1)
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""
HTTP Request example for unsandbox Python SDK - Synchronous Version
This example demonstrates making HTTP requests from within a sandboxed environment.
Uses semitrusted mode which provides internet access through an egress proxy.
To run:
export UNSANDBOX_PUBLIC_KEY="your-public-key"
export UNSANDBOX_SECRET_KEY="your-secret-key"
python3 http_request.py
Expected output:
Status Code: 200
Response: {"origin": "..."}
"""
import sys
import os
# Add the SDK path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from un import execute_code, CredentialsError
def main():
"""Execute HTTP request code in sandbox."""
# The code to execute - uses requests library (pre-installed)
code = """
import requests
import json
try:
# Make HTTP request to httpbin.org
response = requests.get('https://httpbin.org/ip', timeout=10)
print(f"Status Code: {response.status_code}")
# Parse and display response
data = response.json()
print(f"Response: {json.dumps(data)}")
except requests.RequestException as e:
print(f"Request failed: {e}")
except Exception as e:
print(f"Error: {e}")
"""
try:
# Resolve credentials from environment
public_key = os.environ.get("UNSANDBOX_PUBLIC_KEY")
secret_key = os.environ.get("UNSANDBOX_SECRET_KEY")
if not public_key or not secret_key:
print("Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY environment variables required")
print("Run with: export UNSANDBOX_PUBLIC_KEY=your-key UNSANDBOX_SECRET_KEY=your-key")
sys.exit(1)
# Execute the code
print("Executing HTTP request in sandbox...")
result = execute_code("python", code, public_key, secret_key)
# Check for errors
if result.get("status") == "completed":
print("\n=== STDOUT ===")
print(result.get("stdout", ""))
if result.get("stderr"):
print("\n=== STDERR ===")
print(result.get("stderr", ""))
else:
print(f"Execution failed with status: {result.get('status')}")
print(f"Error: {result.get('error', 'Unknown error')}")
sys.exit(1)
except CredentialsError as e:
print(f"Credentials error: {e}")
sys.exit(1)
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""
JSON Processing example for unsandbox Python SDK - Synchronous Version
This example demonstrates JSON parsing and manipulation operations.
Shows how to work with structured data in sandboxed environments.
To run:
export UNSANDBOX_PUBLIC_KEY="your-public-key"
export UNSANDBOX_SECRET_KEY="your-secret-key"
python3 json_processing.py
Expected output:
Original JSON: {"name": "Alice", "age": 30, "skills": ["Python", "JavaScript"]}
Parsed successfully!
Name: Alice
Age: 30
Skills: Python, JavaScript
"""
import sys
import os
# Add the SDK path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from un import execute_code, CredentialsError
def main():
"""Execute JSON processing code in sandbox."""
# The code to execute - demonstrates JSON parsing and manipulation
code = """
import json
# Original JSON data
json_string = '{"name": "Alice", "age": 30, "skills": ["Python", "JavaScript"]}'
print(f"Original JSON: {json_string}")
try:
# Parse JSON
data = json.loads(json_string)
print("Parsed successfully!")
# Access fields
print(f"Name: {data['name']}")
print(f"Age: {data['age']}")
print(f"Skills: {', '.join(data['skills'])}")
# Modify and re-serialize
data['age'] = 31
data['skills'].append("Go")
modified_json = json.dumps(data, indent=2)
print(f"\\nModified JSON:\\n{modified_json}")
except json.JSONDecodeError as e:
print(f"JSON parsing error: {e}")
except Exception as e:
print(f"Error: {e}")
"""
try:
# Resolve credentials from environment
public_key = os.environ.get("UNSANDBOX_PUBLIC_KEY")
secret_key = os.environ.get("UNSANDBOX_SECRET_KEY")
if not public_key or not secret_key:
print("Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY environment variables required")
print("Run with: export UNSANDBOX_PUBLIC_KEY=your-key UNSANDBOX_SECRET_KEY=your-key")
sys.exit(1)
# Execute the code
print("Executing JSON processing in sandbox...")
result = execute_code("python", code, public_key, secret_key)
# Check for errors
if result.get("status") == "completed":
print("\n=== STDOUT ===")
print(result.get("stdout", ""))
if result.get("stderr"):
print("\n=== STDERR ===")
print(result.get("stderr", ""))
else:
print(f"Execution failed with status: {result.get('status')}")
print(f"Error: {result.get('error', 'Unknown error')}")
sys.exit(1)
except CredentialsError as e:
print(f"Credentials error: {e}")
sys.exit(1)
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,11 @@
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --strict-markers
markers =
unit: unit tests (fast, no API calls)
integration: integration tests (may need credentials)
slow: slow tests

View file

@ -0,0 +1,41 @@
"""Setup configuration for unsandbox Python SDK (Synchronous)"""
from setuptools import setup, find_packages
with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
setup(
name="unsandbox",
version="1.0.0",
author="Unsandbox",
description="Synchronous Python SDK for unsandbox.com code execution",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/unsandbox/un-inception",
packages=find_packages(where="src"),
package_dir={"": "src"},
classifiers=[
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
python_requires=">=3.8",
install_requires=[
"requests>=2.25.0",
],
extras_require={
"dev": [
"pytest>=6.0",
"pytest-cov>=2.0",
"black>=21.0",
"flake8>=3.9",
"mypy>=0.900",
],
},
)

View file

@ -0,0 +1,46 @@
"""
Unsandbox Python SDK (Synchronous)
This module provides a synchronous client for executing code on unsandbox.com
Example usage:
from un import execute_code, get_languages
result = execute_code("python", "print('hello')")
print(result)
"""
from .un import (
execute_code,
execute_async,
get_job,
wait_for_job,
cancel_job,
list_jobs,
get_languages,
detect_language,
session_snapshot,
service_snapshot,
list_snapshots,
restore_snapshot,
delete_snapshot,
CredentialsError,
)
__version__ = "1.0.0"
__all__ = [
"execute_code",
"execute_async",
"get_job",
"wait_for_job",
"cancel_job",
"list_jobs",
"get_languages",
"detect_language",
"session_snapshot",
"service_snapshot",
"list_snapshots",
"restore_snapshot",
"delete_snapshot",
"CredentialsError",
]

View file

@ -0,0 +1 @@
"""Tests for unsandbox Python SDK (Synchronous)"""

View file

@ -0,0 +1,173 @@
"""Tests for languages cache functionality"""
import os
import json
import tempfile
from pathlib import Path
import time
from unittest.mock import patch, MagicMock
from un import _load_languages_cache, _save_languages_cache
class TestLanguagesCaching:
"""Test languages list caching"""
def test_save_and_load_cache(self):
"""Test saving and loading languages cache"""
with tempfile.TemporaryDirectory() as tmpdir:
cache_path = Path(tmpdir) / "languages.json"
languages = ["python", "javascript", "go"]
# Mock the cache path
with patch("un._get_languages_cache_path", return_value=cache_path):
_save_languages_cache(languages)
# Verify file was created
assert cache_path.exists()
# Load and verify
loaded = _load_languages_cache()
assert loaded == languages
def test_cache_ttl_expiration(self):
"""Test that cache expires after TTL"""
with tempfile.TemporaryDirectory() as tmpdir:
cache_path = Path(tmpdir) / "languages.json"
languages = ["python", "javascript"]
with patch("un._get_languages_cache_path", return_value=cache_path):
_save_languages_cache(languages)
# Make the file old
old_time = time.time() - 7200 # 2 hours ago
os.utime(cache_path, (old_time, old_time))
# Should return None (expired)
loaded = _load_languages_cache()
assert loaded is None
def test_cache_not_expired_within_ttl(self):
"""Test that cache is valid within TTL"""
with tempfile.TemporaryDirectory() as tmpdir:
cache_path = Path(tmpdir) / "languages.json"
languages = ["python", "javascript", "go", "rust"]
with patch("un._get_languages_cache_path", return_value=cache_path):
_save_languages_cache(languages)
# Should still be valid
loaded = _load_languages_cache()
assert loaded == languages
def test_cache_corrupted_json(self):
"""Test that corrupted JSON is handled gracefully"""
with tempfile.TemporaryDirectory() as tmpdir:
cache_path = Path(tmpdir) / "languages.json"
# Write invalid JSON
with open(cache_path, "w") as f:
f.write("invalid json {{{")
with patch("un._get_languages_cache_path", return_value=cache_path):
# Should return None on JSON error
loaded = _load_languages_cache()
assert loaded is None
def test_cache_missing_languages_key(self):
"""Test cache file with missing 'languages' key"""
with tempfile.TemporaryDirectory() as tmpdir:
cache_path = Path(tmpdir) / "languages.json"
# Write valid JSON but missing 'languages' key
with open(cache_path, "w") as f:
json.dump({"timestamp": int(time.time())}, f)
with patch("un._get_languages_cache_path", return_value=cache_path):
# Should return None when 'languages' key is missing
loaded = _load_languages_cache()
assert loaded is None
def test_cache_nonexistent_file(self):
"""Test loading cache when file doesn't exist"""
with tempfile.TemporaryDirectory() as tmpdir:
cache_path = Path(tmpdir) / "nonexistent.json"
with patch("un._get_languages_cache_path", return_value=cache_path):
loaded = _load_languages_cache()
assert loaded is None
def test_cache_permissions_error(self):
"""Test that permission errors are handled gracefully"""
with tempfile.TemporaryDirectory() as tmpdir:
cache_path = Path(tmpdir) / "languages.json"
# Create a directory instead of a file
os.makedirs(cache_path, exist_ok=True)
with patch("un._get_languages_cache_path", return_value=cache_path):
# Should return None on permission error
loaded = _load_languages_cache()
assert loaded is None
def test_cache_content_format(self):
"""Test that cache file has correct format"""
with tempfile.TemporaryDirectory() as tmpdir:
cache_path = Path(tmpdir) / "languages.json"
languages = ["python", "javascript"]
with patch("un._get_languages_cache_path", return_value=cache_path):
_save_languages_cache(languages)
# Read and verify format
with open(cache_path, "r") as f:
data = json.load(f)
assert "languages" in data
assert "timestamp" in data
assert data["languages"] == languages
assert isinstance(data["timestamp"], int)
def test_cache_save_permission_error(self):
"""Test that cache save errors are handled gracefully"""
with tempfile.TemporaryDirectory() as tmpdir:
# Create a read-only directory
cache_path = Path(tmpdir) / "readonly"
cache_path.mkdir(mode=0o444)
with patch("un._get_languages_cache_path", return_value=cache_path / "languages.json"):
# Should not raise, just silently fail
try:
_save_languages_cache(["python"])
except Exception:
# Permission errors might still occur, but should be caught
pass
def test_cache_empty_languages_list(self):
"""Test caching empty languages list"""
with tempfile.TemporaryDirectory() as tmpdir:
cache_path = Path(tmpdir) / "languages.json"
languages = []
with patch("un._get_languages_cache_path", return_value=cache_path):
_save_languages_cache(languages)
loaded = _load_languages_cache()
assert loaded == []
def test_cache_large_languages_list(self):
"""Test caching large languages list"""
with tempfile.TemporaryDirectory() as tmpdir:
cache_path = Path(tmpdir) / "languages.json"
# Create a list with 100 languages
languages = [f"lang_{i}" for i in range(100)]
with patch("un._get_languages_cache_path", return_value=cache_path):
_save_languages_cache(languages)
loaded = _load_languages_cache()
assert loaded == languages
assert len(loaded) == 100

View file

@ -0,0 +1,122 @@
"""Tests for credential resolution in unsandbox SDK"""
import os
import tempfile
from pathlib import Path
import pytest
from un import CredentialsError
def test_credentials_from_function_args():
"""Test that function arguments take priority"""
from un import _resolve_credentials
pk, sk = _resolve_credentials("func_pk", "func_sk")
assert pk == "func_pk"
assert sk == "func_sk"
def test_credentials_from_environment():
"""Test that environment variables are used when args are missing"""
from un import _resolve_credentials
# Save original values
orig_pk = os.environ.get("UNSANDBOX_PUBLIC_KEY")
orig_sk = os.environ.get("UNSANDBOX_SECRET_KEY")
try:
os.environ["UNSANDBOX_PUBLIC_KEY"] = "env_pk"
os.environ["UNSANDBOX_SECRET_KEY"] = "env_sk"
pk, sk = _resolve_credentials()
assert pk == "env_pk"
assert sk == "env_sk"
finally:
# Restore original values
if orig_pk is not None:
os.environ["UNSANDBOX_PUBLIC_KEY"] = orig_pk
else:
os.environ.pop("UNSANDBOX_PUBLIC_KEY", None)
if orig_sk is not None:
os.environ["UNSANDBOX_SECRET_KEY"] = orig_sk
else:
os.environ.pop("UNSANDBOX_SECRET_KEY", None)
def test_credentials_from_csv_file():
"""Test loading credentials from CSV file"""
from un import _load_credentials_from_csv
with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as f:
f.write("pk1,sk1\n")
f.write("pk2,sk2\n")
temp_path = f.name
try:
# Load first account
pk, sk = _load_credentials_from_csv(Path(temp_path), 0)
assert pk == "pk1"
assert sk == "sk1"
# Load second account
pk, sk = _load_credentials_from_csv(Path(temp_path), 1)
assert pk == "pk2"
assert sk == "sk2"
finally:
os.unlink(temp_path)
def test_credentials_csv_with_comments():
"""Test that CSV loader ignores comments"""
from un import _load_credentials_from_csv
with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as f:
f.write("# Comment line\n")
f.write("pk1,sk1\n")
f.write("\n") # Empty line
f.write("pk2,sk2\n")
temp_path = f.name
try:
pk, sk = _load_credentials_from_csv(Path(temp_path), 0)
assert pk == "pk1"
assert sk == "sk1"
finally:
os.unlink(temp_path)
def test_credentials_csv_nonexistent_file():
"""Test that nonexistent CSV returns None"""
from un import _load_credentials_from_csv
result = _load_credentials_from_csv(Path("/nonexistent/file.csv"))
assert result is None
def test_credentials_missing_all():
"""Test that CredentialsError is raised when no credentials available"""
from un import _resolve_credentials
# Save original values
orig_pk = os.environ.get("UNSANDBOX_PUBLIC_KEY")
orig_sk = os.environ.get("UNSANDBOX_SECRET_KEY")
try:
os.environ.pop("UNSANDBOX_PUBLIC_KEY", None)
os.environ.pop("UNSANDBOX_SECRET_KEY", None)
# This should raise because we're not providing args and env vars don't exist
# (and we don't have test CSV files)
with pytest.raises(CredentialsError):
_resolve_credentials()
finally:
if orig_pk is not None:
os.environ["UNSANDBOX_PUBLIC_KEY"] = orig_pk
else:
os.environ.pop("UNSANDBOX_PUBLIC_KEY", None)
if orig_sk is not None:
os.environ["UNSANDBOX_SECRET_KEY"] = orig_sk
else:
os.environ.pop("UNSANDBOX_SECRET_KEY", None)

View file

@ -0,0 +1,242 @@
"""Integration tests with mocked API responses"""
from unittest.mock import patch, MagicMock
import json
import pytest
from un import (
execute_code,
execute_async,
get_job,
wait_for_job,
cancel_job,
list_jobs,
get_languages,
)
class TestIntegrationMocked:
"""Integration tests using mocked HTTP responses"""
@patch("un.requests.post")
@patch("un._resolve_credentials")
def test_execute_code_success(self, mock_creds, mock_post):
"""Test successful synchronous code execution"""
mock_creds.return_value = ("pk_test", "sk_test")
# Mock the initial response
mock_response = MagicMock()
mock_response.json.return_value = {
"job_id": "job_123",
"status": "completed",
"stdout": "hello\n",
"stderr": "",
"exit_code": 0,
"runtime_ms": 150,
}
mock_post.return_value = mock_response
result = execute_code("python", "print('hello')")
assert result["status"] == "completed"
assert result["stdout"] == "hello\n"
assert result["exit_code"] == 0
@patch("un.requests.post")
@patch("un._resolve_credentials")
def test_execute_async_returns_job_id(self, mock_creds, mock_post):
"""Test async execution returns job ID"""
mock_creds.return_value = ("pk_test", "sk_test")
mock_response = MagicMock()
mock_response.json.return_value = {
"job_id": "job_456",
"status": "pending",
}
mock_post.return_value = mock_response
job_id = execute_async("javascript", "console.log('hello')")
assert job_id == "job_456"
@patch("un.requests.get")
@patch("un._resolve_credentials")
def test_get_job(self, mock_creds, mock_get):
"""Test getting job status"""
mock_creds.return_value = ("pk_test", "sk_test")
mock_response = MagicMock()
mock_response.json.return_value = {
"job_id": "job_789",
"status": "running",
"created_at": "2024-01-15T10:00:00Z",
}
mock_get.return_value = mock_response
result = get_job("job_789")
assert result["job_id"] == "job_789"
assert result["status"] == "running"
@patch("un.requests.get")
@patch("un.time.sleep")
@patch("un._resolve_credentials")
def test_wait_for_job_completes(self, mock_creds, mock_sleep, mock_get):
"""Test waiting for job to complete"""
mock_creds.return_value = ("pk_test", "sk_test")
# First call: running, second call: completed
mock_responses = [
MagicMock(
json=MagicMock(
return_value={
"job_id": "job_123",
"status": "running",
}
)
),
MagicMock(
json=MagicMock(
return_value={
"job_id": "job_123",
"status": "completed",
"stdout": "result\n",
"exit_code": 0,
}
)
),
]
mock_get.side_effect = mock_responses
result = wait_for_job("job_123")
assert result["status"] == "completed"
assert result["stdout"] == "result\n"
@patch("un.requests.delete")
@patch("un._resolve_credentials")
def test_cancel_job(self, mock_creds, mock_delete):
"""Test cancelling a job"""
mock_creds.return_value = ("pk_test", "sk_test")
mock_response = MagicMock()
mock_response.json.return_value = {
"job_id": "job_123",
"status": "cancelled",
}
mock_delete.return_value = mock_response
result = cancel_job("job_123")
assert result["status"] == "cancelled"
@patch("un.requests.get")
@patch("un._resolve_credentials")
def test_list_jobs(self, mock_creds, mock_get):
"""Test listing jobs"""
mock_creds.return_value = ("pk_test", "sk_test")
mock_response = MagicMock()
mock_response.json.return_value = {
"jobs": [
{"job_id": "job_1", "status": "completed"},
{"job_id": "job_2", "status": "running"},
{"job_id": "job_3", "status": "pending"},
]
}
mock_get.return_value = mock_response
jobs = list_jobs()
assert len(jobs) == 3
assert jobs[0]["job_id"] == "job_1"
assert jobs[1]["status"] == "running"
@patch("un._load_languages_cache")
@patch("un.requests.get")
@patch("un._resolve_credentials")
@patch("un._save_languages_cache")
def test_get_languages_uses_cache(
self, mock_save_cache, mock_creds, mock_get, mock_load_cache
):
"""Test that get_languages uses cache when available"""
mock_load_cache.return_value = ["python", "javascript", "go"]
languages = get_languages()
# Should use cache, not make API call
assert languages == ["python", "javascript", "go"]
mock_get.assert_not_called()
@patch("un._load_languages_cache")
@patch("un.requests.get")
@patch("un._resolve_credentials")
@patch("un._save_languages_cache")
def test_get_languages_fetches_from_api(
self, mock_save_cache, mock_creds, mock_get, mock_load_cache
):
"""Test that get_languages fetches from API when cache misses"""
mock_creds.return_value = ("pk_test", "sk_test")
mock_load_cache.return_value = None # Cache miss
mock_response = MagicMock()
mock_response.json.return_value = {
"languages": ["python", "javascript", "go", "rust"]
}
mock_get.return_value = mock_response
languages = get_languages()
assert len(languages) == 4
assert "python" in languages
mock_save_cache.assert_called_once_with(languages)
@patch("un.requests.post")
@patch("un._resolve_credentials")
def test_request_headers(self, mock_creds, mock_post):
"""Test that requests include proper authentication headers"""
mock_creds.return_value = ("pk_test", "sk_test")
mock_response = MagicMock()
mock_response.json.return_value = {
"job_id": "job_123",
"status": "completed",
}
mock_post.return_value = mock_response
execute_async("python", "print(1)")
# Verify headers were set
call_kwargs = mock_post.call_args[1]
headers = call_kwargs.get("headers", {})
assert "Authorization" in headers
assert headers["Authorization"].startswith("Bearer pk_test")
assert "X-Timestamp" in headers
assert "X-Signature" in headers
assert "Content-Type" in headers
assert headers["Content-Type"] == "application/json"
@patch("un.requests.post")
@patch("un._resolve_credentials")
def test_error_handling_http_error(self, mock_creds, mock_post):
"""Test error handling for HTTP errors"""
mock_creds.return_value = ("pk_test", "sk_test")
mock_response = MagicMock()
mock_response.raise_for_status.side_effect = Exception("404 Not Found")
mock_post.return_value = mock_response
with pytest.raises(Exception):
execute_async("python", "print(1)")
@patch("un.requests.get")
@patch("un._resolve_credentials")
def test_handle_network_timeout(self, mock_creds, mock_get):
"""Test handling of network timeouts"""
import requests
mock_creds.return_value = ("pk_test", "sk_test")
mock_get.side_effect = requests.Timeout("Connection timeout")
with pytest.raises(requests.Timeout):
get_job("job_123")

View file

@ -0,0 +1,112 @@
"""Tests for language detection in unsandbox SDK"""
from un import detect_language
class TestLanguageDetection:
"""Test language detection from filenames"""
def test_python_detection(self):
"""Test Python file detection"""
assert detect_language("script.py") == "python"
assert detect_language("main.py") == "python"
assert detect_language("/path/to/script.py") == "python"
def test_javascript_detection(self):
"""Test JavaScript file detection"""
assert detect_language("app.js") == "javascript"
assert detect_language("index.js") == "javascript"
def test_typescript_detection(self):
"""Test TypeScript file detection"""
assert detect_language("app.ts") == "typescript"
def test_go_detection(self):
"""Test Go file detection"""
assert detect_language("main.go") == "go"
def test_rust_detection(self):
"""Test Rust file detection"""
assert detect_language("main.rs") == "rust"
def test_c_detection(self):
"""Test C file detection"""
assert detect_language("main.c") == "c"
def test_cpp_detection(self):
"""Test C++ file detection"""
assert detect_language("main.cpp") == "cpp"
assert detect_language("main.cc") == "cpp"
assert detect_language("main.cxx") == "cpp"
def test_java_detection(self):
"""Test Java file detection"""
assert detect_language("Main.java") == "java"
def test_ruby_detection(self):
"""Test Ruby file detection"""
assert detect_language("script.rb") == "ruby"
def test_php_detection(self):
"""Test PHP file detection"""
assert detect_language("index.php") == "php"
def test_bash_detection(self):
"""Test Bash file detection"""
assert detect_language("script.sh") == "bash"
def test_r_detection(self):
"""Test R file detection (both lowercase and uppercase)"""
assert detect_language("script.r") == "r"
assert detect_language("script.R") == "r"
def test_perl_detection(self):
"""Test Perl file detection"""
assert detect_language("script.pl") == "perl"
def test_lua_detection(self):
"""Test Lua file detection"""
assert detect_language("script.lua") == "lua"
def test_unknown_extension(self):
"""Test that unknown extensions return None"""
assert detect_language("script.unknown") is None
assert detect_language("Makefile") is None
def test_no_extension(self):
"""Test that files without extension return None"""
assert detect_language("Makefile") is None
assert detect_language("README") is None
def test_empty_filename(self):
"""Test that empty filename returns None"""
assert detect_language("") is None
def test_dot_files(self):
"""Test dot files (like .gitignore) return None"""
assert detect_language(".gitignore") is None
def test_multiple_dots(self):
"""Test files with multiple dots (use last extension)"""
assert detect_language("app.test.py") == "python"
assert detect_language("archive.tar.gz") is None
def test_case_insensitivity(self):
"""Test that detection is case-insensitive for extensions"""
assert detect_language("script.PY") == "python"
assert detect_language("script.Py") == "python"
assert detect_language("script.JS") == "javascript"
def test_common_languages(self):
"""Test detection for common languages"""
test_cases = [
("helloworld.py", "python"),
("index.html", None), # HTML not supported for execution
("style.css", None), # CSS not supported
("app.ts", "typescript"),
("main.go", "go"),
("fibonacci.rs", "rust"),
]
for filename, expected in test_cases:
assert detect_language(filename) == expected, f"Failed for {filename}"

View file

@ -0,0 +1,360 @@
"""Real-world scenario tests demonstrating SDK usage patterns"""
from unittest.mock import patch, MagicMock
import pytest
from un import (
execute_code,
execute_async,
get_job,
wait_for_job,
cancel_job,
list_jobs,
get_languages,
detect_language,
)
class TestRealWorldScenarios:
"""Test real-world usage patterns"""
@patch("un.requests.post")
@patch("un._resolve_credentials")
def test_fibonacci_calculation(self, mock_creds, mock_post):
"""Test calculating Fibonacci number"""
mock_creds.return_value = ("pk_test", "sk_test")
code = """
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
print(fib(10))
"""
mock_response = MagicMock()
mock_response.json.return_value = {
"job_id": "fib_123",
"status": "completed",
"stdout": "55\n",
"stderr": "",
"exit_code": 0,
"runtime_ms": 250,
}
mock_post.return_value = mock_response
result = execute_code("python", code)
assert result["status"] == "completed"
assert "55" in result["stdout"]
assert result["exit_code"] == 0
@patch("un.requests.post")
@patch("un._resolve_credentials")
def test_json_processing(self, mock_creds, mock_post):
"""Test JSON processing use case"""
mock_creds.return_value = ("pk_test", "sk_test")
code = """
import json
data = {"name": "Alice", "age": 30}
json_str = json.dumps(data)
print(json_str)
"""
mock_response = MagicMock()
mock_response.json.return_value = {
"job_id": "json_123",
"status": "completed",
"stdout": '{"name": "Alice", "age": 30}\n',
"stderr": "",
"exit_code": 0,
"runtime_ms": 180,
}
mock_post.return_value = mock_response
result = execute_code("python", code)
assert result["status"] == "completed"
assert "Alice" in result["stdout"]
@patch("un.requests.post")
@patch("un._resolve_credentials")
def test_multiple_language_execution(self, mock_creds, mock_post):
"""Test executing code in different languages"""
mock_creds.return_value = ("pk_test", "sk_test")
test_cases = [
("python", "print('hello')", "hello"),
("javascript", "console.log('world')", "world"),
("go", 'fmt.Println("go")', "go"),
]
for language, code, expected_output in test_cases:
mock_response = MagicMock()
mock_response.json.return_value = {
"job_id": f"job_{language}",
"status": "completed",
"stdout": f"{expected_output}\n",
"stderr": "",
"exit_code": 0,
"runtime_ms": 200,
}
mock_post.return_value = mock_response
result = execute_code(language, code)
assert result["status"] == "completed"
assert expected_output in result["stdout"]
@patch("un.requests.post")
@patch("un.requests.get")
@patch("un.time.sleep")
@patch("un._resolve_credentials")
def test_long_running_job_with_polling(self, mock_creds, mock_sleep, mock_get, mock_post):
"""Test handling long-running jobs with polling"""
mock_creds.return_value = ("pk_test", "sk_test")
# Start job
start_response = MagicMock()
start_response.json.return_value = {
"job_id": "long_job_123",
"status": "pending",
}
mock_post.return_value = start_response
job_id = execute_async("python", "import time; time.sleep(10); print('done')")
assert job_id == "long_job_123"
# Poll for completion - simulate 3 polls before completion
poll_responses = [
MagicMock(json=MagicMock(return_value={"status": "pending"})),
MagicMock(json=MagicMock(return_value={"status": "running"})),
MagicMock(
json=MagicMock(
return_value={
"status": "completed",
"stdout": "done\n",
"exit_code": 0,
}
)
),
]
mock_get.side_effect = poll_responses
result = wait_for_job(job_id)
assert result["status"] == "completed"
assert result["stdout"] == "done\n"
@patch("un.requests.post")
@patch("un.requests.delete")
@patch("un._resolve_credentials")
def test_cancel_long_running_job(self, mock_creds, mock_delete, mock_post):
"""Test cancelling a job that takes too long"""
mock_creds.return_value = ("pk_test", "sk_test")
# Start job
start_response = MagicMock()
start_response.json.return_value = {
"job_id": "cancel_me_123",
"status": "pending",
}
mock_post.return_value = start_response
job_id = execute_async("python", "while True: pass")
# Cancel it
cancel_response = MagicMock()
cancel_response.json.return_value = {
"job_id": job_id,
"status": "cancelled",
}
mock_delete.return_value = cancel_response
result = cancel_job(job_id)
assert result["status"] == "cancelled"
@patch("un.requests.get")
@patch("un._resolve_credentials")
def test_error_handling_compilation_error(self, mock_creds, mock_get):
"""Test handling of compilation errors"""
mock_creds.return_value = ("pk_test", "sk_test")
mock_response = MagicMock()
mock_response.json.return_value = {
"job_id": "error_123",
"status": "failed",
"stdout": "",
"stderr": "SyntaxError: invalid syntax",
"exit_code": 1,
"runtime_ms": 100,
}
mock_get.return_value = mock_response
result = get_job("error_123")
assert result["status"] == "failed"
assert result["exit_code"] == 1
assert "SyntaxError" in result["stderr"]
@patch("un.requests.get")
@patch("un._resolve_credentials")
def test_error_handling_timeout(self, mock_creds, mock_get):
"""Test handling of execution timeouts"""
mock_creds.return_value = ("pk_test", "sk_test")
mock_response = MagicMock()
mock_response.json.return_value = {
"job_id": "timeout_123",
"status": "timeout",
"stdout": "",
"stderr": "Execution timeout: 30 seconds exceeded",
"exit_code": 124,
"runtime_ms": 30000,
}
mock_get.return_value = mock_response
result = get_job("timeout_123")
assert result["status"] == "timeout"
assert result["exit_code"] == 124
@patch("un.requests.post")
@patch("un.requests.get")
@patch("un._resolve_credentials")
def test_batch_job_execution(self, mock_creds, mock_get, mock_post):
"""Test executing multiple jobs in batch"""
mock_creds.return_value = ("pk_test", "sk_test")
# Start 3 jobs
job_ids = []
for i in range(3):
start_response = MagicMock()
start_response.json.return_value = {
"job_id": f"batch_job_{i}",
"status": "pending",
}
mock_post.return_value = start_response
job_id = execute_async("python", f"print({i})")
job_ids.append(job_id)
assert len(job_ids) == 3
# Check all jobs
for job_id in job_ids:
get_response = MagicMock()
get_response.json.return_value = {
"job_id": job_id,
"status": "completed",
"stdout": "output\n",
"exit_code": 0,
}
mock_get.return_value = get_response
result = get_job(job_id)
assert result["status"] == "completed"
def test_language_auto_detection_workflow(self):
"""Test workflow using language auto-detection"""
test_files = {
"script.py": "python",
"app.js": "javascript",
"main.go": "go",
"hello.rs": "rust",
"unknown.xyz": None,
}
for filename, expected_lang in test_files.items():
detected = detect_language(filename)
assert detected == expected_lang
@patch("un._load_languages_cache")
@patch("un.requests.get")
@patch("un._resolve_credentials")
def test_get_available_languages(self, mock_creds, mock_get, mock_load_cache):
"""Test getting list of available languages"""
mock_creds.return_value = ("pk_test", "sk_test")
mock_load_cache.return_value = None
mock_response = MagicMock()
mock_response.json.return_value = {
"languages": [
"python",
"javascript",
"go",
"rust",
"java",
"cpp",
]
}
mock_get.return_value = mock_response
languages = get_languages()
assert len(languages) >= 6
assert "python" in languages
assert "javascript" in languages
@patch("un.requests.get")
@patch("un._resolve_credentials")
def test_list_multiple_jobs(self, mock_creds, mock_get):
"""Test listing multiple jobs in different states"""
mock_creds.return_value = ("pk_test", "sk_test")
mock_response = MagicMock()
mock_response.json.return_value = {
"jobs": [
{"job_id": "job_1", "status": "completed", "runtime_ms": 150},
{"job_id": "job_2", "status": "running", "runtime_ms": 5000},
{"job_id": "job_3", "status": "pending", "runtime_ms": 0},
{"job_id": "job_4", "status": "failed", "runtime_ms": 200},
]
}
mock_get.return_value = mock_response
jobs = list_jobs()
assert len(jobs) == 4
assert jobs[0]["status"] == "completed"
assert jobs[1]["status"] == "running"
assert jobs[2]["status"] == "pending"
assert jobs[3]["status"] == "failed"
@patch("un.requests.post")
@patch("un._resolve_credentials")
def test_scientific_computation(self, mock_creds, mock_post):
"""Test scientific computation workflow"""
mock_creds.return_value = ("pk_test", "sk_test")
code = """
import math
# Calculate pi using Machin's formula
def compute_pi(iterations):
result = 0.0
for k in range(iterations):
result += ((-1)**k) / (2*k + 1)
return 4 * result
pi_approx = compute_pi(10000)
print(f"pi ≈ {pi_approx}")
"""
mock_response = MagicMock()
mock_response.json.return_value = {
"job_id": "pi_123",
"status": "completed",
"stdout": "pi ≈ 3.1415\n",
"stderr": "",
"exit_code": 0,
"runtime_ms": 500,
}
mock_post.return_value = mock_response
result = execute_code("python", code)
assert result["status"] == "completed"
assert "pi" in result["stdout"].lower()

View file

@ -0,0 +1,194 @@
"""Tests for HMAC-SHA256 request signing"""
from un import _sign_request
class TestRequestSigning:
"""Test HMAC-SHA256 request signing"""
def test_sign_request_basic(self):
"""Test basic request signing"""
signature = _sign_request(
secret_key="my_secret",
timestamp=1234567890,
method="POST",
path="/execute",
body='{"language":"python"}',
)
# Signature should be 64 hex characters
assert len(signature) == 64
assert all(c in "0123456789abcdef" for c in signature)
def test_sign_request_get(self):
"""Test signing GET request (no body)"""
signature = _sign_request(
secret_key="my_secret",
timestamp=1234567890,
method="GET",
path="/languages",
body=None,
)
assert len(signature) == 64
def test_sign_request_delete(self):
"""Test signing DELETE request"""
signature = _sign_request(
secret_key="my_secret",
timestamp=1234567890,
method="DELETE",
path="/jobs/job_123",
body=None,
)
assert len(signature) == 64
def test_sign_request_deterministic(self):
"""Test that same inputs produce same signature"""
signature1 = _sign_request(
secret_key="test_secret",
timestamp=9999,
method="POST",
path="/test",
body='{"test":"data"}',
)
signature2 = _sign_request(
secret_key="test_secret",
timestamp=9999,
method="POST",
path="/test",
body='{"test":"data"}',
)
assert signature1 == signature2
def test_sign_request_different_secrets(self):
"""Test that different secrets produce different signatures"""
signature1 = _sign_request(
secret_key="secret1",
timestamp=1234567890,
method="POST",
path="/execute",
body="code",
)
signature2 = _sign_request(
secret_key="secret2",
timestamp=1234567890,
method="POST",
path="/execute",
body="code",
)
assert signature1 != signature2
def test_sign_request_different_timestamps(self):
"""Test that different timestamps produce different signatures"""
signature1 = _sign_request(
secret_key="secret",
timestamp=1000,
method="POST",
path="/execute",
body="code",
)
signature2 = _sign_request(
secret_key="secret",
timestamp=2000,
method="POST",
path="/execute",
body="code",
)
assert signature1 != signature2
def test_sign_request_different_paths(self):
"""Test that different paths produce different signatures"""
signature1 = _sign_request(
secret_key="secret",
timestamp=1234567890,
method="GET",
path="/jobs",
body=None,
)
signature2 = _sign_request(
secret_key="secret",
timestamp=1234567890,
method="GET",
path="/languages",
body=None,
)
assert signature1 != signature2
def test_sign_request_different_methods(self):
"""Test that different HTTP methods produce different signatures"""
signature1 = _sign_request(
secret_key="secret",
timestamp=1234567890,
method="GET",
path="/jobs/123",
body=None,
)
signature2 = _sign_request(
secret_key="secret",
timestamp=1234567890,
method="DELETE",
path="/jobs/123",
body=None,
)
assert signature1 != signature2
def test_sign_request_message_format(self):
"""Test that message format is correct"""
# Known test case
secret = "test_secret_key"
timestamp = 1609459200 # 2021-01-01 00:00:00 UTC
method = "POST"
path = "/execute"
body = '{"language":"python","code":"print(42)"}'
signature = _sign_request(secret, timestamp, method, path, body)
# Should be a valid hex string
assert len(signature) == 64
assert isinstance(signature, str)
def test_sign_request_empty_body(self):
"""Test signing with empty body string"""
sig1 = _sign_request(
secret_key="secret",
timestamp=1234567890,
method="GET",
path="/test",
body="",
)
sig2 = _sign_request(
secret_key="secret",
timestamp=1234567890,
method="GET",
path="/test",
body=None,
)
# Both should be the same (None and "" are both empty)
assert sig1 == sig2
def test_sign_request_special_characters(self):
"""Test signing request with special characters in body"""
signature = _sign_request(
secret_key="my_secret",
timestamp=1234567890,
method="POST",
path="/execute",
body='{"code":"print(\\"hello\\")"}',
)
assert len(signature) == 64
assert isinstance(signature, str)

View file

@ -0,0 +1,288 @@
#!/usr/bin/env python3
"""
Verification script for unsandbox Python SDK (Synchronous)
This script verifies that the SDK is properly configured and working.
Run from the sync/ directory: python3 verify_sdk.py
"""
import sys
import os
from pathlib import Path
# Add src to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
from un import (
execute_code,
execute_async,
get_job,
wait_for_job,
cancel_job,
list_jobs,
get_languages,
detect_language,
CredentialsError,
_resolve_credentials,
_sign_request,
_load_languages_cache,
_save_languages_cache,
)
def test_language_detection():
"""Test language detection"""
print("Testing language detection...")
tests = [
("hello.py", "python"),
("app.js", "javascript"),
("main.go", "go"),
("script.rs", "rust"),
("main.cpp", "cpp"),
("unknown.xyz", None),
]
for filename, expected in tests:
result = detect_language(filename)
status = "" if result == expected else ""
print(f" {status} detect_language('{filename}') = {result}")
if result != expected:
return False
return True
def test_request_signing():
"""Test request signing"""
print("\nTesting request signing...")
sig1 = _sign_request("secret", 1234567890, "POST", "/execute", '{"code":"test"}')
sig2 = _sign_request("secret", 1234567890, "POST", "/execute", '{"code":"test"}')
# Test deterministic
status = "" if sig1 == sig2 else ""
print(f" {status} Signatures are deterministic: {sig1 == sig2}")
# Test format
status = "" if len(sig1) == 64 else ""
print(f" {status} Signature is 64 hex chars: {len(sig1)} chars")
# Test different secrets produce different sigs
sig_diff = _sign_request("different", 1234567890, "POST", "/execute", '{"code":"test"}')
status = "" if sig1 != sig_diff else ""
print(f" {status} Different secrets produce different signatures: {sig1 != sig_diff}")
return len(sig1) == 64 and sig1 == sig2 and sig1 != sig_diff
def test_credentials():
"""Test credential resolution"""
print("\nTesting credential resolution...")
# Test 1: Function arguments
try:
pk, sk = _resolve_credentials("func_pk", "func_sk")
status = "" if pk == "func_pk" and sk == "func_sk" else ""
print(f" {status} Function arguments: {pk == 'func_pk' and sk == 'func_sk'}")
except Exception as e:
print(f" ✗ Function arguments failed: {e}")
return False
# Test 2: Can resolve credentials (from env or config)
try:
pk, sk = _resolve_credentials()
print(f" ✓ Credentials resolved: {pk[:20]}... / {sk[:20]}...")
return True
except CredentialsError:
print(f" ✗ No credentials found (this is OK for testing)")
print(f" Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY environment variables")
return True # Not a test failure, just missing credentials
def test_caching():
"""Test languages cache"""
print("\nTesting caching...")
import tempfile
import json
import time
from unittest.mock import patch
try:
with tempfile.TemporaryDirectory() as tmpdir:
cache_path = Path(tmpdir) / "languages.json"
# Save cache
languages = ["python", "javascript", "go"]
with patch("un._get_languages_cache_path", return_value=cache_path):
_save_languages_cache(languages)
print(f" ✓ Cache saved")
# Load cache
loaded = _load_languages_cache()
if loaded == languages:
print(f" ✓ Cache loaded correctly: {loaded}")
else:
print(f" ✗ Cache mismatch: {loaded}")
return False
# Test expiration
old_time = time.time() - 7200 # 2 hours old
os.utime(cache_path, (old_time, old_time))
expired = _load_languages_cache()
if expired is None:
print(f" ✓ Cache correctly expired")
else:
print(f" ✗ Cache should be expired: {expired}")
return False
return True
except Exception as e:
print(f" ✗ Cache test failed: {e}")
import traceback
traceback.print_exc()
return False
def test_imports():
"""Test all public imports"""
print("\nTesting imports...")
functions = [
"execute_code",
"execute_async",
"get_job",
"wait_for_job",
"cancel_job",
"list_jobs",
"get_languages",
"detect_language",
"session_snapshot",
"service_snapshot",
"list_snapshots",
"restore_snapshot",
"delete_snapshot",
"CredentialsError",
]
print(f" ✓ Imported {len(functions)} public functions and classes:")
for func in functions:
print(f" - {func}")
return True
def test_package_structure():
"""Verify package structure"""
print("\nVerifying package structure...")
required_files = [
"src/__init__.py",
"src/un.py",
"setup.py",
"README.md",
"USAGE.md",
"LICENSE",
"MANIFEST.in",
"pytest.ini",
"tests/__init__.py",
"tests/test_credentials.py",
"tests/test_language_detection.py",
"tests/test_signatures.py",
"tests/test_caching.py",
"tests/test_integration_mock.py",
"tests/test_real_world_scenarios.py",
]
base_dir = Path(__file__).parent
all_exist = True
for filename in required_files:
filepath = base_dir / filename
status = "" if filepath.exists() else ""
print(f" {status} {filename}")
if not filepath.exists():
all_exist = False
return all_exist
def test_example_structure():
"""Verify examples exist"""
print("\nVerifying examples...")
example_files = [
"examples/hello_world.py",
"examples/fibonacci.py",
"examples/hello_world_client.py",
"examples/fibonacci_client.py",
"examples/json_processing.py",
"examples/http_request.py",
"examples/file_operations.py",
]
base_dir = Path(__file__).parent
all_exist = True
for filename in example_files:
filepath = base_dir / filename
status = "" if filepath.exists() else ""
print(f" {status} {filename}")
if not filepath.exists():
all_exist = False
return all_exist
def main():
"""Run all verification tests"""
print("=" * 60)
print("Unsandbox Python SDK (Sync) - Verification")
print("=" * 60)
tests = [
("Package Structure", test_package_structure),
("Imports", test_imports),
("Language Detection", test_language_detection),
("Request Signing", test_request_signing),
("Credentials", test_credentials),
("Caching", test_caching),
("Examples", test_example_structure),
]
results = []
for test_name, test_func in tests:
try:
result = test_func()
results.append((test_name, result))
except Exception as e:
print(f"\n{test_name} failed with exception: {e}")
import traceback
traceback.print_exc()
results.append((test_name, False))
print("\n" + "=" * 60)
print("Summary")
print("=" * 60)
passed = sum(1 for _, result in results if result)
total = len(results)
for test_name, result in results:
status = "" if result else ""
print(f"{status} {test_name}")
print(f"\nPassed: {passed}/{total}")
if passed == total:
print("\n✓ All verification tests passed!")
return 0
else:
print(f"\n{total - passed} test(s) failed")
return 1
if __name__ == "__main__":
sys.exit(main())

View file

@ -20,14 +20,14 @@ fi
CHANGED_FILES=$(git diff --name-only "$BASE...HEAD" 2>/dev/null || echo "")
# Extract unique languages from TWO sources:
# 1. Root-level files (un.py, un.go, etc.)
# 2. Client directory files (clients/python/*, clients/go/*, etc.)
# 1. Root-level files (un.py, un.go, un.c, etc.)
# 2. Client directory files (clients/python/*, clients/go/*, clients/c/*, etc.)
CHANGED_LANGS=$(
{
# Root-level implementations
# Root-level implementations (un.py, un.c, etc.)
echo "$CHANGED_FILES" | grep -E '^un\.' | sed 's/un\.\([^.]*\).*/\1/'
# Client directory implementations
# Client directory implementations (clients/python/, clients/c/, etc.)
echo "$CHANGED_FILES" | grep -E '^clients/([^/]+)/' | sed 's|^clients/\([^/]*\)/.*|\1|'
} | sort -u || echo ""
)