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:
parent
2701b29945
commit
1e01d09883
46 changed files with 7988 additions and 125 deletions
|
|
@ -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
507
clients/c/README.md
Normal 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/`
|
||||
566
clients/c/examples/README.md
Normal file
566
clients/c/examples/README.md
Normal 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
|
||||
392
clients/c/examples/credentials.c
Normal file
392
clients/c/examples/credentials.c
Normal 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;
|
||||
}
|
||||
418
clients/c/examples/error_handling.c
Normal file
418
clients/c/examples/error_handling.c
Normal 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;
|
||||
}
|
||||
217
clients/c/examples/fibonacci.c
Normal file
217
clients/c/examples/fibonacci.c
Normal 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;
|
||||
}
|
||||
121
clients/c/examples/hello_world.c
Normal file
121
clients/c/examples/hello_world.c
Normal 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
823
clients/c/src/unsandbox.c
Normal 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
311
clients/c/src/unsandbox.h
Normal 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
252
clients/go/Makefile
Normal 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
239
clients/python/Makefile
Normal 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"
|
||||
68
clients/python/async/Makefile
Normal file
68
clients/python/async/Makefile
Normal 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
|
||||
453
clients/python/async/README.md
Normal file
453
clients/python/async/README.md
Normal 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.
|
||||
58
clients/python/async/examples/async_job_polling.py
Normal file
58
clients/python/async/examples/async_job_polling.py
Normal 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)
|
||||
61
clients/python/async/examples/concurrent_execution.py
Normal file
61
clients/python/async/examples/concurrent_execution.py
Normal 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)
|
||||
98
clients/python/async/examples/concurrent_requests.py
Normal file
98
clients/python/async/examples/concurrent_requests.py
Normal 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)
|
||||
94
clients/python/async/examples/fibonacci_async.py
Normal file
94
clients/python/async/examples/fibonacci_async.py
Normal 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)
|
||||
73
clients/python/async/examples/hello_world_async.py
Normal file
73
clients/python/async/examples/hello_world_async.py
Normal 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)
|
||||
102
clients/python/async/examples/stream_processing.py
Normal file
102
clients/python/async/examples/stream_processing.py
Normal 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)
|
||||
78
clients/python/async/examples/sync_blocking_usage.py
Normal file
78
clients/python/async/examples/sync_blocking_usage.py
Normal 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)
|
||||
9
clients/python/async/requirements.txt
Normal file
9
clients/python/async/requirements.txt
Normal 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
|
||||
46
clients/python/async/setup.py
Normal file
46
clients/python/async/setup.py
Normal 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",
|
||||
],
|
||||
)
|
||||
3
clients/python/async/tests/__init__.py
Normal file
3
clients/python/async/tests/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
"""
|
||||
Tests for unsandbox async SDK
|
||||
"""
|
||||
48
clients/python/async/tests/conftest.py
Normal file
48
clients/python/async/tests/conftest.py
Normal 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,
|
||||
}
|
||||
210
clients/python/async/tests/test_async_operations.py
Normal file
210
clients/python/async/tests/test_async_operations.py
Normal 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)
|
||||
104
clients/python/async/tests/test_credentials.py
Normal file
104
clients/python/async/tests/test_credentials.py
Normal 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"
|
||||
206
clients/python/async/tests/test_hmac_signing.py
Normal file
206
clients/python/async/tests/test_hmac_signing.py
Normal 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
|
||||
126
clients/python/async/tests/test_language_detection.py
Normal file
126
clients/python/async/tests/test_language_detection.py
Normal 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}"
|
||||
219
clients/python/sync/README.md
Normal file
219
clients/python/sync/README.md
Normal 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
|
||||
77
clients/python/sync/examples/fibonacci_client.py
Normal file
77
clients/python/sync/examples/fibonacci_client.py
Normal 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()
|
||||
111
clients/python/sync/examples/file_operations.py
Normal file
111
clients/python/sync/examples/file_operations.py
Normal 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()
|
||||
70
clients/python/sync/examples/hello_world_client.py
Normal file
70
clients/python/sync/examples/hello_world_client.py
Normal 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()
|
||||
87
clients/python/sync/examples/http_request.py
Normal file
87
clients/python/sync/examples/http_request.py
Normal 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()
|
||||
100
clients/python/sync/examples/json_processing.py
Normal file
100
clients/python/sync/examples/json_processing.py
Normal 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()
|
||||
11
clients/python/sync/pytest.ini
Normal file
11
clients/python/sync/pytest.ini
Normal 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
|
||||
41
clients/python/sync/setup.py
Normal file
41
clients/python/sync/setup.py
Normal 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",
|
||||
],
|
||||
},
|
||||
)
|
||||
46
clients/python/sync/src/__init__.py
Normal file
46
clients/python/sync/src/__init__.py
Normal 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",
|
||||
]
|
||||
1
clients/python/sync/tests/__init__.py
Normal file
1
clients/python/sync/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Tests for unsandbox Python SDK (Synchronous)"""
|
||||
173
clients/python/sync/tests/test_caching.py
Normal file
173
clients/python/sync/tests/test_caching.py
Normal 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
|
||||
122
clients/python/sync/tests/test_credentials.py
Normal file
122
clients/python/sync/tests/test_credentials.py
Normal 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)
|
||||
242
clients/python/sync/tests/test_integration_mock.py
Normal file
242
clients/python/sync/tests/test_integration_mock.py
Normal 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")
|
||||
112
clients/python/sync/tests/test_language_detection.py
Normal file
112
clients/python/sync/tests/test_language_detection.py
Normal 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}"
|
||||
360
clients/python/sync/tests/test_real_world_scenarios.py
Normal file
360
clients/python/sync/tests/test_real_world_scenarios.py
Normal 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()
|
||||
194
clients/python/sync/tests/test_signatures.py
Normal file
194
clients/python/sync/tests/test_signatures.py
Normal 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)
|
||||
288
clients/python/sync/verify_sdk.py
Normal file
288
clients/python/sync/verify_sdk.py
Normal 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())
|
||||
|
|
@ -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 ""
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue