Scaphoid Examples¶
This page provides practical examples of using Hands Scaphoid in real-world scenarios.
Introduction¶
Hands Scaphoid is a powerful tool for securely executing shell commands in Python. Below are several example scripts demonstrating its capabilities in various contexts, including deployment, development workflows, DevOps automation, and file processing.
Learn more about the items and objects in Operations Summary and get started with the basics.
Already a pro, go to Getting Started)
Example: Deployment scripts¶
More examples can be found in Examples.
Simple web app deployment¶
#!/usr/bin/env python3
"""
Deploy a web application using Hands Scaphoid.
"""
from hands_scaphoid import ShellContext
import sys
def deploy_web_app(branch="main", environment="production"):
"""Deploy web application."""
print(f"π Deploying from branch '{branch}' to '{environment}'...")
with ShellContext(cwd="/app") as shell:
# Allow required commands
required_commands = ["git", "docker", "docker-compose", "echo"]
for cmd in required_commands:
if not shell.allow(cmd):
print(f"β Required command '{cmd}' not found")
return False
try:
# Pull latest code
print("π₯ Pulling latest code...")
shell.run(f"git pull origin {branch}")
# Build new Docker image
print("π¨ Building Docker image...")
shell.run("docker build -t myapp:latest .")
# Stop old containers
print("π Stopping old containers...")
shell.run("docker-compose down")
# Start new containers
print("βΆοΈ Starting new containers...")
shell.run("docker-compose up -d")
# Verify deployment
print("β
Verifying deployment...")
result = shell.run("docker-compose ps")
if "Up" in result.stdout:
print("β
Deployment successful!")
return True
else:
print("β Deployment verification failed")
return False
except Exception as e:
print(f"β Deployment failed: {e}")
return False
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Deploy web application")
parser.add_argument("--branch", default="main", help="Git branch to deploy")
parser.add_argument("--env", default="production", help="Environment")
args = parser.parse_args()
if not deploy_web_app(args.branch, args.env):
sys.exit(1)
Database Migration Script¶
#!/usr/bin/env python3
"""
Database migration script with backup.
"""
from hands_scaphoid import ShellContext
from datetime import datetime
import sys
def migrate_database():
"""Run database migrations with backup."""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_file = f"backup_{timestamp}.sql"
with ShellContext() as shell:
# Allow database commands
for cmd in ["pg_dump", "psql", "echo"]:
if not shell.allow(cmd):
print(f"β Database command '{cmd}' not available")
return False
try:
# Create backup
print("πΎ Creating database backup...")
shell.run(f"pg_dump myapp_prod > {backup_file}")
# Run migrations
print("π Running migrations...")
shell.run("psql myapp_prod < migrations/latest.sql")
# Verify migration
print("β
Migration completed successfully!")
print(f"πΎ Backup saved as: {backup_file}")
return True
except Exception as e:
print(f"β Migration failed: {e}")
print(f"π Restoring from backup: {backup_file}")
try:
shell.run(f"psql myapp_prod < {backup_file}")
print("β
Database restored from backup")
except Exception as restore_error:
print(f"β Backup restoration failed: {restore_error}")
return False
if __name__ == "__main__":
if not migrate_database():
sys.exit(1)
Development Workflows¶
Project Setup Automation¶
#!/usr/bin/env python3
"""
Automated project setup script.
"""
from hands_scaphoid import ShellContext
import sys
import os
def setup_project(project_name: str, template: str = "python"):
"""Set up a new project from template."""
project_dir = f"/workspace/{project_name}"
with ShellContext() as shell:
# Allow development tools
dev_tools = ["git", "python", "pip", "npm", "mkdir", "cp", "echo"]
for tool in dev_tools:
shell.allow(tool)
try:
# Create project directory
print(f"π Creating project directory: {project_dir}")
shell.run(f"mkdir -p {project_dir}")
shell.cd(project_dir)
# Initialize git repository
print("π§ Initializing Git repository...")
shell.run("git init")
if template == "python":
setup_python_project(shell, project_name)
elif template == "node":
setup_node_project(shell, project_name)
else:
print(f"β Unknown template: {template}")
return False
# Create initial commit
print("πΎ Creating initial commit...")
shell.run("git add .")
shell.run(f"git commit -m 'Initial commit for {project_name}'")
print(f"β
Project '{project_name}' set up successfully!")
print(f"π Location: {project_dir}")
return True
except Exception as e:
print(f"β Project setup failed: {e}")
return False
def setup_python_project(shell, project_name):
"""Set up Python project structure."""
print("π Setting up Python project...")
# Create Python project structure
shell.run(f"mkdir -p {project_name}")
shell.run(f"mkdir -p tests")
shell.run(f"mkdir -p docs")
# Create basic files
pyproject_content = f'''[project]
name = "{project_name}"
version = "0.1.0"
description = ""
requires-python = ">=3.11"
dependencies = []
'''
with open("pyproject.toml", "w") as f:
f.write(pyproject_content)
# Create virtual environment
shell.run("python -m venv .venv")
print("β
Python project structure created")
def setup_node_project(shell, project_name):
"""Set up Node.js project structure."""
print("π¦ Setting up Node.js project...")
# Initialize npm project
shell.run("npm init -y")
# Create basic structure
shell.run("mkdir -p src")
shell.run("mkdir -p tests")
shell.run("mkdir -p docs")
print("β
Node.js project structure created")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: setup_project.py <project_name> [template]")
sys.exit(1)
project_name = sys.argv[1]
template = sys.argv[2] if len(sys.argv) > 2 else "python"
if not setup_project(project_name, template):
sys.exit(1)
Automated Testing Pipeline¶
#!/usr/bin/env python3
"""
Comprehensive testing pipeline.
"""
from hands_scaphoid import ShellContext
import sys
def run_test_pipeline():
"""Run complete testing pipeline."""
with ShellContext() as shell:
# Allow testing tools
test_tools = ["python", "pytest", "flake8", "mypy", "black", "echo"]
for tool in test_tools:
if not shell.allow(tool):
print(f"β οΈ Testing tool '{tool}' not available")
tests_passed = True
# Code formatting check
print("π¨ Checking code formatting...")
try:
shell.run("black --check src tests")
print("β
Code formatting OK")
except Exception:
print("β Code formatting issues found")
tests_passed = False
# Linting
print("π Running linter...")
try:
shell.run("flake8 src tests")
print("β
Linting passed")
except Exception:
print("β Linting issues found")
tests_passed = False
# Type checking
print("π¬ Type checking...")
try:
shell.run("mypy src")
print("β
Type checking passed")
except Exception:
print("β Type checking issues found")
tests_passed = False
# Unit tests
print("π§ͺ Running unit tests...")
try:
result = shell.run("pytest tests/ -v --cov")
print("β
Unit tests passed")
# Extract coverage percentage
coverage_line = [line for line in result.stdout.split('\n')
if 'TOTAL' in line and '%' in line]
if coverage_line:
print(f"π {coverage_line[0]}")
except Exception:
print("β Unit tests failed")
tests_passed = False
if tests_passed:
print("\nπ All tests passed! Ready for deployment.")
return True
else:
print("\nβ Some tests failed. Please fix issues before deployment.")
return False
if __name__ == "__main__":
if not run_test_pipeline():
sys.exit(1)
DevOps Automation¶
Container Management¶
#!/usr/bin/env python3
"""
Docker container management utilities.
"""
from hands_scaphoid import ShellContext
import json
import sys
def manage_containers(action: str, service: str = None):
"""Manage Docker containers."""
with ShellContext() as shell:
shell.allow("docker")
shell.allow("docker-compose")
try:
if action == "status":
show_container_status(shell)
elif action == "logs":
show_container_logs(shell, service)
elif action == "restart":
restart_service(shell, service)
elif action == "health":
check_health(shell)
else:
print(f"β Unknown action: {action}")
return False
return True
except Exception as e:
print(f"β Container management failed: {e}")
return False
def show_container_status(shell):
"""Show status of all containers."""
print("π Container Status:")
print("-" * 50)
result = shell.run("docker-compose ps")
print(result.stdout)
def show_container_logs(shell, service):
"""Show logs for a specific service."""
if not service:
print("β Service name required for logs")
return
print(f"π Logs for {service}:")
print("-" * 50)
result = shell.run(f"docker-compose logs --tail=50 {service}")
print(result.stdout)
def restart_service(shell, service):
"""Restart a specific service."""
if not service:
print("β Service name required for restart")
return
print(f"π Restarting {service}...")
shell.run(f"docker-compose restart {service}")
print(f"β
{service} restarted")
def check_health(shell):
"""Check health of all services."""
print("π₯ Health Check:")
print("-" * 50)
# Get container info
result = shell.run("docker ps --format '{{.Names}}\t{{.Status}}'")
for line in result.stdout.strip().split('\n'):
if line:
name, status = line.split('\t', 1)
if 'healthy' in status.lower():
print(f"β
{name}: {status}")
elif 'unhealthy' in status.lower():
print(f"β {name}: {status}")
else:
print(f"β οΈ {name}: {status}")
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Container management")
parser.add_argument("action", choices=["status", "logs", "restart", "health"])
parser.add_argument("--service", help="Service name")
args = parser.parse_args()
if not manage_containers(args.action, args.service):
sys.exit(1)
System Monitoring¶
#!/usr/bin/env python3
"""
System monitoring and alerts.
"""
from hands_scaphoid import ShellContext
import time
import sys
def monitor_system(duration_minutes: int = 5):
"""Monitor system resources and services."""
end_time = time.time() + (duration_minutes * 60)
with ShellContext() as shell:
# Allow monitoring commands
monitor_commands = ["ps", "df", "free", "top", "docker", "curl"]
for cmd in monitor_commands:
shell.allow(cmd)
print(f"π Starting system monitoring for {duration_minutes} minutes...")
while time.time() < end_time:
print(f"\nπ System Check - {time.strftime('%H:%M:%S')}")
print("-" * 50)
# Check disk usage
check_disk_usage(shell)
# Check memory usage
check_memory_usage(shell)
# Check running services
check_services(shell)
# Check application health
check_application_health(shell)
# Wait before next check
time.sleep(30)
print("\nβ
Monitoring completed")
def check_disk_usage(shell):
"""Check disk usage."""
result = shell.run("df -h /")
lines = result.stdout.strip().split('\n')
if len(lines) > 1:
usage_line = lines[1].split()
usage_percent = usage_line[4].rstrip('%')
if int(usage_percent) > 90:
print(f"π΄ Disk usage: {usage_percent}% (CRITICAL)")
elif int(usage_percent) > 80:
print(f"π‘ Disk usage: {usage_percent}% (WARNING)")
else:
print(f"π’ Disk usage: {usage_percent}% (OK)")
def check_memory_usage(shell):
"""Check memory usage."""
result = shell.run("free -m")
lines = result.stdout.strip().split('\n')
if len(lines) > 1:
mem_line = lines[1].split()
total = int(mem_line[1])
used = int(mem_line[2])
usage_percent = (used / total) * 100
if usage_percent > 90:
print(f"π΄ Memory usage: {usage_percent:.1f}% (CRITICAL)")
elif usage_percent > 80:
print(f"π‘ Memory usage: {usage_percent:.1f}% (WARNING)")
else:
print(f"π’ Memory usage: {usage_percent:.1f}% (OK)")
def check_services(shell):
"""Check running services."""
try:
result = shell.run("docker-compose ps", check=False)
if result.returncode == 0:
# Count running containers
running = result.stdout.count("Up")
total = len([line for line in result.stdout.split('\n')
if line and not line.startswith('Name')])
if running == total:
print(f"π’ Services: {running}/{total} running (OK)")
else:
print(f"π΄ Services: {running}/{total} running (ISSUES)")
else:
print("β οΈ Could not check services")
except Exception:
print("β οΈ Service check failed")
def check_application_health(shell):
"""Check application health endpoints."""
try:
result = shell.run("curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/health",
check=False)
status_code = result.stdout.strip()
if status_code == "200":
print("π’ Application health: OK")
else:
print(f"π΄ Application health: HTTP {status_code}")
except Exception:
print("π΄ Application health: UNREACHABLE")
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="System monitoring")
parser.add_argument("--duration", type=int, default=5,
help="Monitoring duration in minutes")
args = parser.parse_args()
try:
monitor_system(args.duration)
except KeyboardInterrupt:
print("\nβΉοΈ Monitoring stopped by user")
File Processing¶
Batch Image Processing¶
#!/usr/bin/env python3
"""
Batch image processing with ImageMagick.
"""
from hands_scaphoid import ShellContext
import os
import sys
def process_images(input_dir: str, output_dir: str,
width: int = 800, height: int = 600, quality: int = 85):
"""Process images in batch."""
with ShellContext(cwd=input_dir) as shell:
# Allow image processing commands
shell.allow("find")
shell.allow("convert") # ImageMagick
shell.allow("mkdir")
shell.allow("identify")
try:
# Create output directory
shell.run(f"mkdir -p {output_dir}")
# Find image files
result = shell.run("find . -type f \\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.gif' \\)")
image_files = [f.strip() for f in result.stdout.split('\n') if f.strip()]
if not image_files:
print("β No image files found")
return False
print(f"π· Found {len(image_files)} images to process")
processed = 0
for image_file in image_files:
try:
# Get original image info
info_result = shell.run(f"identify -format '%wx%h' '{image_file}'")
original_size = info_result.stdout.strip()
# Generate output file name
output_file = os.path.join(output_dir,
os.path.basename(image_file))
# Process image
convert_cmd = (f"convert '{image_file}' "
f"-resize {width}x{height}> "
f"-quality {quality} "
f"'{output_file}'")
shell.run(convert_cmd)
# Get new image info
new_info_result = shell.run(f"identify -format '%wx%h' '{output_file}'")
new_size = new_info_result.stdout.strip()
print(f"β
{image_file}: {original_size} β {new_size}")
processed += 1
except Exception as e:
print(f"β Failed to process {image_file}: {e}")
print(f"\nπ Processed {processed}/{len(image_files)} images")
return processed > 0
except Exception as e:
print(f"β Image processing failed: {e}")
return False
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Batch image processing")
parser.add_argument("input_dir", help="Input directory")
parser.add_argument("output_dir", help="Output directory")
parser.add_argument("--width", type=int, default=800, help="Max width")
parser.add_argument("--height", type=int, default=600, help="Max height")
parser.add_argument("--quality", type=int, default=85, help="JPEG quality")
args = parser.parse_args()
if not os.path.exists(args.input_dir):
print(f"β Input directory does not exist: {args.input_dir}")
sys.exit(1)
if not process_images(args.input_dir, args.output_dir,
args.width, args.height, args.quality):
sys.exit(1)
These examples demonstrate the flexibility and power of Hands Scaphoid for various automation tasks while maintaining security through command allowlisting.
Getting Started¶
Welcome to Hands Scaphoid! This guide will help you get up and running quickly with secure shell command execution in Python.
What is Hands Scaphoid?¶
Hands Scaphoid is a Python library that provides a secure and convenient way to execute shell commands with:
- Security: Command allowlisting prevents execution of unauthorized commands
- Environment Management: Load environment variables from files
- Docker Integration: Execute commands in Docker containers
- Clean API: Both object-oriented and script-like interfaces
Basic Concepts¶
Shell Class¶
The Shell class is the core of Hands Scaphoid. It provides secure command execution with environment management.
from hands_scaphoid import Shell
# Create a shell instance
shell = Shell(cwd="/path/to/working/dir")
# Allow commands before executing them
shell.allow("echo")
shell.allow("ls")
# Execute commands
result = shell.run("echo 'Hello, World!'")
print(result.stdout) # Output: Hello, World!
ShellContext Manager¶
The ShellContext provides a more convenient way to use Shell functionality with automatic cleanup.
from hands_scaphoid import ShellContext
with ShellContext(cwd="/tmp") as shell:
shell.allow("pwd")
result = shell.run("pwd")
print(result.stdout) # Output: /tmp
Global Functions¶
For script-like usage, ShellContext can inject functions globally:
from hands_scaphoid import ShellContext
with ShellContext():
# These functions are now available globally
allow("echo")
allow("git")
cd("/path/to/project")
run("git status")
run("echo 'Done!'")
Your First Script¶
Let's create a simple script that demonstrates the key features:
#!/usr/bin/env python3
"""
Simple deployment script using Hands Scaphoid
"""
from hands_scaphoid import ShellContext
def deploy_app():
with ShellContext(cwd="/app") as shell:
# Allow the commands we need
shell.allow("git")
shell.allow("docker")
shell.allow("echo")
try:
# Pull latest code
print("Pulling latest code...")
shell.run("git pull origin main")
# Build Docker image
print("Building Docker image...")
shell.run("docker build -t myapp:latest .")
# Check if container is running
print("Checking dependencies...")
shell.depends_on(["database", "redis"])
# Deploy new version
print("Deploying new version...")
shell.run("docker run -d --name myapp myapp:latest")
print("Deployment complete!")
except Exception as e:
print(f"Deployment failed: {e}")
return False
return True
if __name__ == "__main__":
deploy_app()
Security Model¶
Hands Scaphoid uses an allowlist-based security model:
- No commands are allowed by default
- Commands must be explicitly allowed using
allow() - Only the command name is checked, not arguments
- Commands are validated to exist on the system
with ShellContext() as shell:
# This will fail - command not allowed
try:
shell.run("rm -rf /")
except PermissionError:
print("Security working!")
# Allow the command first
shell.allow("echo")
shell.run("echo 'This works!'") # This succeeds
Environment Management¶
Load environment variables from files:
# Create an .env file
with open(".env", "w") as f:
f.write("DATABASE_URL=postgresql://localhost/mydb\n")
f.write("API_KEY=secret123\n")
# Use the environment file
with ShellContext(env_file=".env") as shell:
db_url = shell.get_env_var("DATABASE_URL")
print(f"Database URL: {db_url}")
# Set additional variables
shell.set_env_var("DEPLOYMENT", "production")
Error Handling¶
Hands Scaphoid provides detailed error handling:
from hands_scaphoid import ShellContext
import subprocess
with ShellContext() as shell:
shell.allow("ls")
try:
# This will fail if directory doesn't exist
result = shell.run("ls /nonexistent", check=True)
except subprocess.CalledProcessError as e:
print(f"Command failed with exit code {e.returncode}")
print(f"Error output: {e.stderr}")
except PermissionError as e:
print(f"Security error: {e}")
Next Steps¶
Now that you understand the basics, explore these advanced topics:
- Basic Usage - Detailed usage examples
- Windows Shells - Windows PowerShell and WSL support
- API Reference - Complete API documentation
- Shell Object - Complete API documentation
Hands Scaphoid¶
A secure shell execution context manager for Python that provides controlled command execution with environment management, command allowlisting, and Docker integration.
Features¶
- Secure Command Execution: Allowlist-based command execution for enhanced security
- Environment Management: Load and manage environment variables from files
- Docker Integration: Execute commands inside Docker containers
- Context Management: Clean global function injection for script-like usage
- Rich Output: Beautiful console output using Rich library
- Type Safety: Full type hints for better development experience
Quick Start¶
from hands_scaphoid import ShellContext
# Basic usage with context manager
with ShellContext() as shell:
# Allow specific commands
shell.allow("echo")
shell.allow("ls")
# Execute commands securely
result = shell.run("echo 'Hello, World!'")
print(result.stdout)
# Change directory
shell.cd("/tmp")
# List files
result = shell.run("ls -la")
print(result.stdout)
Global Function Style¶
from hands_scaphoid import ShellContext
# Use global functions for script-like experience
with ShellContext():
allow("git")
allow("echo")
# Functions are available globally within the context
cd("/path/to/project")
run("git status")
run("echo 'Build complete'")
Docker Integration¶
from hands_scaphoid import ShellContext
with ShellContext() as shell:
shell.allow("docker")
# Execute commands in containers
result = shell.run_in("mycontainer", "ls /app")
# Check if containers are running
shell.depends_on(["web", "database"])
Installation¶
Requirements¶
- Python 3.11+
- Rich library for console output
- Click for CLI interface
Documentation¶
For comprehensive documentation, visit: https://42sol-eu.github.io/hands_scaphoid
Contributing¶
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
Author¶
Andreas HΓ€berle - 42sol-eu