ci: Disable GitHub Actions workflow temporarily
This commit is contained in:
parent
331cba42aa
commit
9ab564da60
16 changed files with 15954 additions and 771 deletions
|
|
@ -6,6 +6,7 @@ unsandbox.com Python SDK (Asynchronous)
|
|||
Library Usage:
|
||||
import asyncio
|
||||
from un_async import (
|
||||
# Execution
|
||||
execute_code,
|
||||
execute_async,
|
||||
get_job,
|
||||
|
|
@ -14,11 +15,44 @@ Library Usage:
|
|||
list_jobs,
|
||||
get_languages,
|
||||
detect_language,
|
||||
# Sessions
|
||||
list_sessions,
|
||||
get_session,
|
||||
create_session,
|
||||
delete_session,
|
||||
freeze_session,
|
||||
unfreeze_session,
|
||||
boost_session,
|
||||
unboost_session,
|
||||
shell_session,
|
||||
# Services
|
||||
list_services,
|
||||
create_service,
|
||||
get_service,
|
||||
update_service,
|
||||
delete_service,
|
||||
freeze_service,
|
||||
unfreeze_service,
|
||||
lock_service,
|
||||
unlock_service,
|
||||
get_service_logs,
|
||||
get_service_env,
|
||||
set_service_env,
|
||||
delete_service_env,
|
||||
export_service_env,
|
||||
redeploy_service,
|
||||
execute_in_service,
|
||||
# Snapshots
|
||||
session_snapshot,
|
||||
service_snapshot,
|
||||
list_snapshots,
|
||||
restore_snapshot,
|
||||
delete_snapshot,
|
||||
lock_snapshot,
|
||||
unlock_snapshot,
|
||||
clone_snapshot,
|
||||
# Key validation
|
||||
validate_keys,
|
||||
)
|
||||
|
||||
async def main():
|
||||
|
|
@ -214,6 +248,10 @@ async def _make_request(
|
|||
async with session.post(url, headers=headers, json=data, timeout=aiohttp.ClientTimeout(total=120)) as resp:
|
||||
resp.raise_for_status()
|
||||
return await resp.json()
|
||||
elif method == "PATCH":
|
||||
async with session.patch(url, headers=headers, json=data, timeout=aiohttp.ClientTimeout(total=120)) as resp:
|
||||
resp.raise_for_status()
|
||||
return await resp.json()
|
||||
elif method == "DELETE":
|
||||
async with session.delete(url, headers=headers, timeout=aiohttp.ClientTimeout(total=120)) as resp:
|
||||
resp.raise_for_status()
|
||||
|
|
@ -702,3 +740,913 @@ async def delete_snapshot(
|
|||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return await _make_request("DELETE", f"/snapshots/{snapshot_id}", public_key, secret_key)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Session Management Functions
|
||||
# =============================================================================
|
||||
|
||||
|
||||
async def list_sessions(
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
List all sessions for the authenticated account.
|
||||
|
||||
Args:
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
List of session dicts containing id, container_name, status, etc.
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
response = await _make_request("GET", "/sessions", public_key, secret_key)
|
||||
return response.get("sessions", [])
|
||||
|
||||
|
||||
async def get_session(
|
||||
session_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get details of a specific session.
|
||||
|
||||
Args:
|
||||
session_id: Session ID to get details for
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Session details dict
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return await _make_request("GET", f"/sessions/{session_id}", public_key, secret_key)
|
||||
|
||||
|
||||
async def create_session(
|
||||
language: Optional[str] = None,
|
||||
network_mode: str = "zerotrust",
|
||||
ttl: int = 3600,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
shell: Optional[str] = None,
|
||||
multiplexer: Optional[str] = None,
|
||||
vcpu: int = 1,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a new interactive session.
|
||||
|
||||
Args:
|
||||
language: Optional programming language for the session
|
||||
network_mode: Network mode - "zerotrust" (default, no network) or "semitrusted" (with network)
|
||||
ttl: Time to live in seconds (default 3600)
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
shell: Optional shell to use (e.g., "bash", "python3")
|
||||
multiplexer: Optional terminal multiplexer ("tmux" or "screen")
|
||||
vcpu: Number of vCPUs (1-8, default 1)
|
||||
|
||||
Returns:
|
||||
Response dict containing session_id, container_name, etc.
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
data: Dict[str, Any] = {
|
||||
"network_mode": network_mode,
|
||||
"ttl": ttl,
|
||||
}
|
||||
if language:
|
||||
data["language"] = language
|
||||
if shell:
|
||||
data["shell"] = shell
|
||||
if multiplexer:
|
||||
data["multiplexer"] = multiplexer
|
||||
if vcpu > 1:
|
||||
data["vcpu"] = vcpu
|
||||
|
||||
return await _make_request("POST", "/sessions", public_key, secret_key, data)
|
||||
|
||||
|
||||
async def delete_session(
|
||||
session_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Delete/terminate a session.
|
||||
|
||||
Args:
|
||||
session_id: Session ID to delete
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with deletion confirmation and optional artifacts
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return await _make_request("DELETE", f"/sessions/{session_id}", public_key, secret_key)
|
||||
|
||||
|
||||
async def freeze_session(
|
||||
session_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Freeze a session (pause execution, preserve state).
|
||||
|
||||
Args:
|
||||
session_id: Session ID to freeze
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with freeze confirmation
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return await _make_request("POST", f"/sessions/{session_id}/freeze", public_key, secret_key, {})
|
||||
|
||||
|
||||
async def unfreeze_session(
|
||||
session_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Unfreeze a session (resume execution).
|
||||
|
||||
Args:
|
||||
session_id: Session ID to unfreeze
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with unfreeze confirmation
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return await _make_request("POST", f"/sessions/{session_id}/unfreeze", public_key, secret_key, {})
|
||||
|
||||
|
||||
async def boost_session(
|
||||
session_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Boost a session (increase resources).
|
||||
|
||||
Args:
|
||||
session_id: Session ID to boost
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with boost confirmation
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return await _make_request("POST", f"/sessions/{session_id}/boost", public_key, secret_key, {})
|
||||
|
||||
|
||||
async def unboost_session(
|
||||
session_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Unboost a session (return to normal resources).
|
||||
|
||||
Args:
|
||||
session_id: Session ID to unboost
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with unboost confirmation
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return await _make_request("POST", f"/sessions/{session_id}/unboost", public_key, secret_key, {})
|
||||
|
||||
|
||||
async def shell_session(
|
||||
session_id: str,
|
||||
command: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute a shell command in a session.
|
||||
|
||||
Note: This is for one-off commands. For interactive shell access,
|
||||
use WebSocket connection to /sessions/{id}/shell.
|
||||
|
||||
Args:
|
||||
session_id: Session ID to execute command in
|
||||
command: Shell command to execute
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with command output
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return await _make_request(
|
||||
"POST",
|
||||
f"/sessions/{session_id}/shell",
|
||||
public_key,
|
||||
secret_key,
|
||||
{"command": command},
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Service Management Functions
|
||||
# =============================================================================
|
||||
|
||||
|
||||
async def list_services(
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
List all services for the authenticated account.
|
||||
|
||||
Args:
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
List of service dicts containing id, name, status, ports, etc.
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
response = await _make_request("GET", "/services", public_key, secret_key)
|
||||
return response.get("services", [])
|
||||
|
||||
|
||||
async def create_service(
|
||||
name: str,
|
||||
ports: List[int],
|
||||
bootstrap: Optional[str] = None,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
network_mode: str = "semitrusted",
|
||||
custom_domains: Optional[List[str]] = None,
|
||||
vcpu: int = 1,
|
||||
service_type: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a new persistent service.
|
||||
|
||||
Args:
|
||||
name: Service name (used for subdomain: name.on.unsandbox.com)
|
||||
ports: List of ports to expose (e.g., [80, 443])
|
||||
bootstrap: Bootstrap script content, URL, or inline command
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
network_mode: Network mode (default "semitrusted" for services)
|
||||
custom_domains: Optional list of custom domain names
|
||||
vcpu: Number of vCPUs (1-8, default 1)
|
||||
service_type: Optional service type for SRV records (e.g., "minecraft")
|
||||
|
||||
Returns:
|
||||
Response dict containing service_id, etc.
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
data: Dict[str, Any] = {
|
||||
"name": name,
|
||||
"ports": ports,
|
||||
"network_mode": network_mode,
|
||||
}
|
||||
if bootstrap:
|
||||
# Check if it looks like a URL
|
||||
if bootstrap.startswith("http://") or bootstrap.startswith("https://"):
|
||||
data["bootstrap"] = bootstrap
|
||||
else:
|
||||
data["bootstrap_content"] = bootstrap
|
||||
if custom_domains:
|
||||
data["custom_domains"] = custom_domains
|
||||
if vcpu > 1:
|
||||
data["vcpu"] = vcpu
|
||||
if service_type:
|
||||
data["service_type"] = service_type
|
||||
|
||||
return await _make_request("POST", "/services", public_key, secret_key, data)
|
||||
|
||||
|
||||
async def get_service(
|
||||
service_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get details of a specific service.
|
||||
|
||||
Args:
|
||||
service_id: Service ID to get details for
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Service details dict
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return await _make_request("GET", f"/services/{service_id}", public_key, secret_key)
|
||||
|
||||
|
||||
async def update_service(
|
||||
service_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
vcpu: Optional[int] = None,
|
||||
**kwargs,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Update a service (e.g., resize vCPU/memory).
|
||||
|
||||
Args:
|
||||
service_id: Service ID to update
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
vcpu: Optional new vCPU count (1-8)
|
||||
**kwargs: Additional fields to update
|
||||
|
||||
Returns:
|
||||
Response dict with update confirmation
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
data: Dict[str, Any] = {}
|
||||
if vcpu is not None:
|
||||
data["vcpu"] = vcpu
|
||||
data.update(kwargs)
|
||||
|
||||
return await _make_request("PATCH", f"/services/{service_id}", public_key, secret_key, data)
|
||||
|
||||
|
||||
async def delete_service(
|
||||
service_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Delete/destroy a service.
|
||||
|
||||
Args:
|
||||
service_id: Service ID to delete
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with deletion confirmation
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return await _make_request("DELETE", f"/services/{service_id}", public_key, secret_key)
|
||||
|
||||
|
||||
async def freeze_service(
|
||||
service_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Freeze a service (pause execution, preserve state).
|
||||
|
||||
Args:
|
||||
service_id: Service ID to freeze
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with freeze confirmation
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return await _make_request("POST", f"/services/{service_id}/freeze", public_key, secret_key, {})
|
||||
|
||||
|
||||
async def unfreeze_service(
|
||||
service_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Unfreeze a service (resume execution).
|
||||
|
||||
Args:
|
||||
service_id: Service ID to unfreeze
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with unfreeze confirmation
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return await _make_request("POST", f"/services/{service_id}/unfreeze", public_key, secret_key, {})
|
||||
|
||||
|
||||
async def lock_service(
|
||||
service_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Lock a service to prevent accidental deletion.
|
||||
|
||||
Args:
|
||||
service_id: Service ID to lock
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with lock confirmation
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return await _make_request("POST", f"/services/{service_id}/lock", public_key, secret_key, {})
|
||||
|
||||
|
||||
async def unlock_service(
|
||||
service_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Unlock a service to allow deletion.
|
||||
|
||||
Args:
|
||||
service_id: Service ID to unlock
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with unlock confirmation
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return await _make_request("POST", f"/services/{service_id}/unlock", public_key, secret_key, {})
|
||||
|
||||
|
||||
async def get_service_logs(
|
||||
service_id: str,
|
||||
all_logs: bool = False,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get bootstrap/runtime logs for a service.
|
||||
|
||||
Args:
|
||||
service_id: Service ID to get logs for
|
||||
all_logs: If True, get all logs; if False, get last ~9000 lines (tail)
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict containing "log" field with log content
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
path = f"/services/{service_id}/logs"
|
||||
if all_logs:
|
||||
path += "?all=true"
|
||||
return await _make_request("GET", path, public_key, secret_key)
|
||||
|
||||
|
||||
async def get_service_env(
|
||||
service_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get environment vault status for a service.
|
||||
|
||||
Returns metadata about the vault (has_vault, count, updated_at)
|
||||
but NOT the actual secrets. Use export_service_env to retrieve secrets.
|
||||
|
||||
Args:
|
||||
service_id: Service ID to get env status for
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with has_vault, count, updated_at fields
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return await _make_request("GET", f"/services/{service_id}/env", public_key, secret_key)
|
||||
|
||||
|
||||
async def set_service_env(
|
||||
service_id: str,
|
||||
env_dict: Dict[str, str],
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Set environment variables for a service.
|
||||
|
||||
Replaces the entire environment vault with the provided variables.
|
||||
Variables are encrypted at rest and injected into the container.
|
||||
|
||||
Args:
|
||||
service_id: Service ID to set env for
|
||||
env_dict: Dictionary of environment variables (KEY: VALUE)
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with count of variables set
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
# Convert dict to .env format for the API
|
||||
env_content = "\n".join(f"{k}={v}" for k, v in env_dict.items())
|
||||
|
||||
# Note: This endpoint expects text/plain body, but we'll send as JSON
|
||||
# and let the API handle conversion
|
||||
return await _make_request(
|
||||
"POST",
|
||||
f"/services/{service_id}/env",
|
||||
public_key,
|
||||
secret_key,
|
||||
{"env": env_content},
|
||||
)
|
||||
|
||||
|
||||
async def delete_service_env(
|
||||
service_id: str,
|
||||
keys: Optional[List[str]] = None,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Delete environment vault or specific keys from a service.
|
||||
|
||||
Args:
|
||||
service_id: Service ID to delete env from
|
||||
keys: Optional list of specific keys to delete; if None, deletes entire vault
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with deletion confirmation
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
path = f"/services/{service_id}/env"
|
||||
# If specific keys provided, could add as query params (API dependent)
|
||||
return await _make_request("DELETE", path, public_key, secret_key)
|
||||
|
||||
|
||||
async def export_service_env(
|
||||
service_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Export environment vault secrets for a service.
|
||||
|
||||
Requires HMAC authentication to prove ownership.
|
||||
Returns the actual secret values in .env format.
|
||||
|
||||
Args:
|
||||
service_id: Service ID to export env from
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict containing "env" field with KEY=VALUE content
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return await _make_request("POST", f"/services/{service_id}/env/export", public_key, secret_key, {})
|
||||
|
||||
|
||||
async def redeploy_service(
|
||||
service_id: str,
|
||||
bootstrap: Optional[str] = None,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Redeploy a service (re-run bootstrap script).
|
||||
|
||||
Bootstrap scripts should be idempotent for proper upgrade behavior.
|
||||
|
||||
Args:
|
||||
service_id: Service ID to redeploy
|
||||
bootstrap: Optional new bootstrap script/URL (uses existing if not provided)
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with redeploy confirmation
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
data: Dict[str, Any] = {}
|
||||
if bootstrap:
|
||||
if bootstrap.startswith("http://") or bootstrap.startswith("https://"):
|
||||
data["bootstrap"] = bootstrap
|
||||
else:
|
||||
data["bootstrap_content"] = bootstrap
|
||||
|
||||
return await _make_request("POST", f"/services/{service_id}/redeploy", public_key, secret_key, data)
|
||||
|
||||
|
||||
async def execute_in_service(
|
||||
service_id: str,
|
||||
command: str,
|
||||
timeout: int = 30000,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute a command in a running service container.
|
||||
|
||||
Uses async job polling for long-running commands.
|
||||
|
||||
Args:
|
||||
service_id: Service ID to execute command in
|
||||
command: Shell command to execute
|
||||
timeout: Command timeout in milliseconds (default 30000)
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with job_id for async polling, or direct result
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return await _make_request(
|
||||
"POST",
|
||||
f"/services/{service_id}/execute",
|
||||
public_key,
|
||||
secret_key,
|
||||
{"command": command, "timeout": timeout},
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Additional Snapshot Functions
|
||||
# =============================================================================
|
||||
|
||||
|
||||
async def lock_snapshot(
|
||||
snapshot_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Lock a snapshot to prevent accidental deletion.
|
||||
|
||||
Args:
|
||||
snapshot_id: Snapshot ID to lock
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with lock confirmation
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return await _make_request("POST", f"/snapshots/{snapshot_id}/lock", public_key, secret_key, {})
|
||||
|
||||
|
||||
async def unlock_snapshot(
|
||||
snapshot_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Unlock a snapshot to allow deletion.
|
||||
|
||||
Args:
|
||||
snapshot_id: Snapshot ID to unlock
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with unlock confirmation
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return await _make_request("POST", f"/snapshots/{snapshot_id}/unlock", public_key, secret_key, {})
|
||||
|
||||
|
||||
async def clone_snapshot(
|
||||
snapshot_id: str,
|
||||
clone_type: str = "session",
|
||||
name: Optional[str] = None,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
shell: Optional[str] = None,
|
||||
ports: Optional[List[int]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Clone a snapshot to create a new session or service.
|
||||
|
||||
Args:
|
||||
snapshot_id: Snapshot ID to clone from
|
||||
clone_type: Type of resource to create ("session" or "service")
|
||||
name: Optional name for the new resource
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
shell: Optional shell for session clones
|
||||
ports: Optional ports list for service clones
|
||||
|
||||
Returns:
|
||||
Response dict containing session_id or service_id
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
data: Dict[str, Any] = {"type": clone_type}
|
||||
if name:
|
||||
data["name"] = name
|
||||
if shell:
|
||||
data["shell"] = shell
|
||||
if ports:
|
||||
data["ports"] = ports
|
||||
|
||||
return await _make_request("POST", f"/snapshots/{snapshot_id}/clone", public_key, secret_key, data)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Key Validation
|
||||
# =============================================================================
|
||||
|
||||
|
||||
PORTAL_BASE = "https://unsandbox.com"
|
||||
|
||||
|
||||
async def validate_keys(
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Validate API keys against the portal.
|
||||
|
||||
Checks if the keys are valid, not expired, and not suspended.
|
||||
|
||||
Args:
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with validation result:
|
||||
- valid: True if keys are valid
|
||||
- tier: Account tier level
|
||||
- expires_at: Expiration timestamp (if applicable)
|
||||
- reason: Reason for invalid status (if applicable)
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
|
||||
url = f"{PORTAL_BASE}/keys/validate"
|
||||
timestamp = int(time.time())
|
||||
body = ""
|
||||
|
||||
signature = _sign_request(secret_key, timestamp, "POST", "/keys/validate", body)
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {public_key}",
|
||||
"X-Timestamp": str(timestamp),
|
||||
"X-Signature": signature,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url, headers=headers, data=body, timeout=aiohttp.ClientTimeout(total=30)) as resp:
|
||||
resp.raise_for_status()
|
||||
return await resp.json()
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ unsandbox.com Python SDK (Synchronous)
|
|||
|
||||
Library Usage:
|
||||
from un import (
|
||||
# Execution
|
||||
execute_code,
|
||||
execute_async,
|
||||
get_job,
|
||||
|
|
@ -13,11 +14,44 @@ Library Usage:
|
|||
list_jobs,
|
||||
get_languages,
|
||||
detect_language,
|
||||
# Sessions
|
||||
list_sessions,
|
||||
get_session,
|
||||
create_session,
|
||||
delete_session,
|
||||
freeze_session,
|
||||
unfreeze_session,
|
||||
boost_session,
|
||||
unboost_session,
|
||||
shell_session,
|
||||
# Services
|
||||
list_services,
|
||||
create_service,
|
||||
get_service,
|
||||
update_service,
|
||||
delete_service,
|
||||
freeze_service,
|
||||
unfreeze_service,
|
||||
lock_service,
|
||||
unlock_service,
|
||||
get_service_logs,
|
||||
get_service_env,
|
||||
set_service_env,
|
||||
delete_service_env,
|
||||
export_service_env,
|
||||
redeploy_service,
|
||||
execute_in_service,
|
||||
# Snapshots
|
||||
session_snapshot,
|
||||
service_snapshot,
|
||||
list_snapshots,
|
||||
restore_snapshot,
|
||||
delete_snapshot,
|
||||
lock_snapshot,
|
||||
unlock_snapshot,
|
||||
clone_snapshot,
|
||||
# Key validation
|
||||
validate_keys,
|
||||
)
|
||||
|
||||
# Execute code synchronously
|
||||
|
|
@ -221,6 +255,8 @@ def _make_request(
|
|||
response = requests.get(url, headers=headers, timeout=120)
|
||||
elif method == "POST":
|
||||
response = requests.post(url, headers=headers, json=data, timeout=120)
|
||||
elif method == "PATCH":
|
||||
response = requests.patch(url, headers=headers, json=data, timeout=120)
|
||||
elif method == "DELETE":
|
||||
response = requests.delete(url, headers=headers, timeout=120)
|
||||
else:
|
||||
|
|
@ -718,3 +754,912 @@ def delete_snapshot(
|
|||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return _make_request("DELETE", f"/snapshots/{snapshot_id}", public_key, secret_key)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Session Management Functions
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def list_sessions(
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
List all sessions for the authenticated account.
|
||||
|
||||
Args:
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
List of session dicts containing id, container_name, status, etc.
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
response = _make_request("GET", "/sessions", public_key, secret_key)
|
||||
return response.get("sessions", [])
|
||||
|
||||
|
||||
def get_session(
|
||||
session_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get details of a specific session.
|
||||
|
||||
Args:
|
||||
session_id: Session ID to get details for
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Session details dict
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return _make_request("GET", f"/sessions/{session_id}", public_key, secret_key)
|
||||
|
||||
|
||||
def create_session(
|
||||
language: Optional[str] = None,
|
||||
network_mode: str = "zerotrust",
|
||||
ttl: int = 3600,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
shell: Optional[str] = None,
|
||||
multiplexer: Optional[str] = None,
|
||||
vcpu: int = 1,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a new interactive session.
|
||||
|
||||
Args:
|
||||
language: Optional programming language for the session
|
||||
network_mode: Network mode - "zerotrust" (default, no network) or "semitrusted" (with network)
|
||||
ttl: Time to live in seconds (default 3600)
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
shell: Optional shell to use (e.g., "bash", "python3")
|
||||
multiplexer: Optional terminal multiplexer ("tmux" or "screen")
|
||||
vcpu: Number of vCPUs (1-8, default 1)
|
||||
|
||||
Returns:
|
||||
Response dict containing session_id, container_name, etc.
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
data: Dict[str, Any] = {
|
||||
"network_mode": network_mode,
|
||||
"ttl": ttl,
|
||||
}
|
||||
if language:
|
||||
data["language"] = language
|
||||
if shell:
|
||||
data["shell"] = shell
|
||||
if multiplexer:
|
||||
data["multiplexer"] = multiplexer
|
||||
if vcpu > 1:
|
||||
data["vcpu"] = vcpu
|
||||
|
||||
return _make_request("POST", "/sessions", public_key, secret_key, data)
|
||||
|
||||
|
||||
def delete_session(
|
||||
session_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Delete/terminate a session.
|
||||
|
||||
Args:
|
||||
session_id: Session ID to delete
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with deletion confirmation and optional artifacts
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return _make_request("DELETE", f"/sessions/{session_id}", public_key, secret_key)
|
||||
|
||||
|
||||
def freeze_session(
|
||||
session_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Freeze a session (pause execution, preserve state).
|
||||
|
||||
Args:
|
||||
session_id: Session ID to freeze
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with freeze confirmation
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return _make_request("POST", f"/sessions/{session_id}/freeze", public_key, secret_key, {})
|
||||
|
||||
|
||||
def unfreeze_session(
|
||||
session_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Unfreeze a session (resume execution).
|
||||
|
||||
Args:
|
||||
session_id: Session ID to unfreeze
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with unfreeze confirmation
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return _make_request("POST", f"/sessions/{session_id}/unfreeze", public_key, secret_key, {})
|
||||
|
||||
|
||||
def boost_session(
|
||||
session_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Boost a session (increase resources).
|
||||
|
||||
Args:
|
||||
session_id: Session ID to boost
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with boost confirmation
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return _make_request("POST", f"/sessions/{session_id}/boost", public_key, secret_key, {})
|
||||
|
||||
|
||||
def unboost_session(
|
||||
session_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Unboost a session (return to normal resources).
|
||||
|
||||
Args:
|
||||
session_id: Session ID to unboost
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with unboost confirmation
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return _make_request("POST", f"/sessions/{session_id}/unboost", public_key, secret_key, {})
|
||||
|
||||
|
||||
def shell_session(
|
||||
session_id: str,
|
||||
command: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute a shell command in a session.
|
||||
|
||||
Note: This is for one-off commands. For interactive shell access,
|
||||
use WebSocket connection to /sessions/{id}/shell.
|
||||
|
||||
Args:
|
||||
session_id: Session ID to execute command in
|
||||
command: Shell command to execute
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with command output
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return _make_request(
|
||||
"POST",
|
||||
f"/sessions/{session_id}/shell",
|
||||
public_key,
|
||||
secret_key,
|
||||
{"command": command},
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Service Management Functions
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def list_services(
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
List all services for the authenticated account.
|
||||
|
||||
Args:
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
List of service dicts containing id, name, status, ports, etc.
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
response = _make_request("GET", "/services", public_key, secret_key)
|
||||
return response.get("services", [])
|
||||
|
||||
|
||||
def create_service(
|
||||
name: str,
|
||||
ports: List[int],
|
||||
bootstrap: Optional[str] = None,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
network_mode: str = "semitrusted",
|
||||
custom_domains: Optional[List[str]] = None,
|
||||
vcpu: int = 1,
|
||||
service_type: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a new persistent service.
|
||||
|
||||
Args:
|
||||
name: Service name (used for subdomain: name.on.unsandbox.com)
|
||||
ports: List of ports to expose (e.g., [80, 443])
|
||||
bootstrap: Bootstrap script content, URL, or inline command
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
network_mode: Network mode (default "semitrusted" for services)
|
||||
custom_domains: Optional list of custom domain names
|
||||
vcpu: Number of vCPUs (1-8, default 1)
|
||||
service_type: Optional service type for SRV records (e.g., "minecraft")
|
||||
|
||||
Returns:
|
||||
Response dict containing service_id, etc.
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
data: Dict[str, Any] = {
|
||||
"name": name,
|
||||
"ports": ports,
|
||||
"network_mode": network_mode,
|
||||
}
|
||||
if bootstrap:
|
||||
# Check if it looks like a URL
|
||||
if bootstrap.startswith("http://") or bootstrap.startswith("https://"):
|
||||
data["bootstrap"] = bootstrap
|
||||
else:
|
||||
data["bootstrap_content"] = bootstrap
|
||||
if custom_domains:
|
||||
data["custom_domains"] = custom_domains
|
||||
if vcpu > 1:
|
||||
data["vcpu"] = vcpu
|
||||
if service_type:
|
||||
data["service_type"] = service_type
|
||||
|
||||
return _make_request("POST", "/services", public_key, secret_key, data)
|
||||
|
||||
|
||||
def get_service(
|
||||
service_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get details of a specific service.
|
||||
|
||||
Args:
|
||||
service_id: Service ID to get details for
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Service details dict
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return _make_request("GET", f"/services/{service_id}", public_key, secret_key)
|
||||
|
||||
|
||||
def update_service(
|
||||
service_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
vcpu: Optional[int] = None,
|
||||
**kwargs,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Update a service (e.g., resize vCPU/memory).
|
||||
|
||||
Args:
|
||||
service_id: Service ID to update
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
vcpu: Optional new vCPU count (1-8)
|
||||
**kwargs: Additional fields to update
|
||||
|
||||
Returns:
|
||||
Response dict with update confirmation
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
data: Dict[str, Any] = {}
|
||||
if vcpu is not None:
|
||||
data["vcpu"] = vcpu
|
||||
data.update(kwargs)
|
||||
|
||||
return _make_request("PATCH", f"/services/{service_id}", public_key, secret_key, data)
|
||||
|
||||
|
||||
def delete_service(
|
||||
service_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Delete/destroy a service.
|
||||
|
||||
Args:
|
||||
service_id: Service ID to delete
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with deletion confirmation
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return _make_request("DELETE", f"/services/{service_id}", public_key, secret_key)
|
||||
|
||||
|
||||
def freeze_service(
|
||||
service_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Freeze a service (pause execution, preserve state).
|
||||
|
||||
Args:
|
||||
service_id: Service ID to freeze
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with freeze confirmation
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return _make_request("POST", f"/services/{service_id}/freeze", public_key, secret_key, {})
|
||||
|
||||
|
||||
def unfreeze_service(
|
||||
service_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Unfreeze a service (resume execution).
|
||||
|
||||
Args:
|
||||
service_id: Service ID to unfreeze
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with unfreeze confirmation
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return _make_request("POST", f"/services/{service_id}/unfreeze", public_key, secret_key, {})
|
||||
|
||||
|
||||
def lock_service(
|
||||
service_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Lock a service to prevent accidental deletion.
|
||||
|
||||
Args:
|
||||
service_id: Service ID to lock
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with lock confirmation
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return _make_request("POST", f"/services/{service_id}/lock", public_key, secret_key, {})
|
||||
|
||||
|
||||
def unlock_service(
|
||||
service_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Unlock a service to allow deletion.
|
||||
|
||||
Args:
|
||||
service_id: Service ID to unlock
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with unlock confirmation
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return _make_request("POST", f"/services/{service_id}/unlock", public_key, secret_key, {})
|
||||
|
||||
|
||||
def get_service_logs(
|
||||
service_id: str,
|
||||
all_logs: bool = False,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get bootstrap/runtime logs for a service.
|
||||
|
||||
Args:
|
||||
service_id: Service ID to get logs for
|
||||
all_logs: If True, get all logs; if False, get last ~9000 lines (tail)
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict containing "log" field with log content
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
path = f"/services/{service_id}/logs"
|
||||
if all_logs:
|
||||
path += "?all=true"
|
||||
return _make_request("GET", path, public_key, secret_key)
|
||||
|
||||
|
||||
def get_service_env(
|
||||
service_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get environment vault status for a service.
|
||||
|
||||
Returns metadata about the vault (has_vault, count, updated_at)
|
||||
but NOT the actual secrets. Use export_service_env to retrieve secrets.
|
||||
|
||||
Args:
|
||||
service_id: Service ID to get env status for
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with has_vault, count, updated_at fields
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return _make_request("GET", f"/services/{service_id}/env", public_key, secret_key)
|
||||
|
||||
|
||||
def set_service_env(
|
||||
service_id: str,
|
||||
env_dict: Dict[str, str],
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Set environment variables for a service.
|
||||
|
||||
Replaces the entire environment vault with the provided variables.
|
||||
Variables are encrypted at rest and injected into the container.
|
||||
|
||||
Args:
|
||||
service_id: Service ID to set env for
|
||||
env_dict: Dictionary of environment variables (KEY: VALUE)
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with count of variables set
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
# Convert dict to .env format for the API
|
||||
env_content = "\n".join(f"{k}={v}" for k, v in env_dict.items())
|
||||
|
||||
# Note: This endpoint expects text/plain body, but we'll send as JSON
|
||||
# and let the API handle conversion
|
||||
return _make_request(
|
||||
"POST",
|
||||
f"/services/{service_id}/env",
|
||||
public_key,
|
||||
secret_key,
|
||||
{"env": env_content},
|
||||
)
|
||||
|
||||
|
||||
def delete_service_env(
|
||||
service_id: str,
|
||||
keys: Optional[List[str]] = None,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Delete environment vault or specific keys from a service.
|
||||
|
||||
Args:
|
||||
service_id: Service ID to delete env from
|
||||
keys: Optional list of specific keys to delete; if None, deletes entire vault
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with deletion confirmation
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
path = f"/services/{service_id}/env"
|
||||
# If specific keys provided, could add as query params (API dependent)
|
||||
return _make_request("DELETE", path, public_key, secret_key)
|
||||
|
||||
|
||||
def export_service_env(
|
||||
service_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Export environment vault secrets for a service.
|
||||
|
||||
Requires HMAC authentication to prove ownership.
|
||||
Returns the actual secret values in .env format.
|
||||
|
||||
Args:
|
||||
service_id: Service ID to export env from
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict containing "env" field with KEY=VALUE content
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return _make_request("POST", f"/services/{service_id}/env/export", public_key, secret_key, {})
|
||||
|
||||
|
||||
def redeploy_service(
|
||||
service_id: str,
|
||||
bootstrap: Optional[str] = None,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Redeploy a service (re-run bootstrap script).
|
||||
|
||||
Bootstrap scripts should be idempotent for proper upgrade behavior.
|
||||
|
||||
Args:
|
||||
service_id: Service ID to redeploy
|
||||
bootstrap: Optional new bootstrap script/URL (uses existing if not provided)
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with redeploy confirmation
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
data: Dict[str, Any] = {}
|
||||
if bootstrap:
|
||||
if bootstrap.startswith("http://") or bootstrap.startswith("https://"):
|
||||
data["bootstrap"] = bootstrap
|
||||
else:
|
||||
data["bootstrap_content"] = bootstrap
|
||||
|
||||
return _make_request("POST", f"/services/{service_id}/redeploy", public_key, secret_key, data)
|
||||
|
||||
|
||||
def execute_in_service(
|
||||
service_id: str,
|
||||
command: str,
|
||||
timeout: int = 30000,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute a command in a running service container.
|
||||
|
||||
Uses async job polling for long-running commands.
|
||||
|
||||
Args:
|
||||
service_id: Service ID to execute command in
|
||||
command: Shell command to execute
|
||||
timeout: Command timeout in milliseconds (default 30000)
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with job_id for async polling, or direct result
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return _make_request(
|
||||
"POST",
|
||||
f"/services/{service_id}/execute",
|
||||
public_key,
|
||||
secret_key,
|
||||
{"command": command, "timeout": timeout},
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Additional Snapshot Functions
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def lock_snapshot(
|
||||
snapshot_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Lock a snapshot to prevent accidental deletion.
|
||||
|
||||
Args:
|
||||
snapshot_id: Snapshot ID to lock
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with lock confirmation
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return _make_request("POST", f"/snapshots/{snapshot_id}/lock", public_key, secret_key, {})
|
||||
|
||||
|
||||
def unlock_snapshot(
|
||||
snapshot_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Unlock a snapshot to allow deletion.
|
||||
|
||||
Args:
|
||||
snapshot_id: Snapshot ID to unlock
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with unlock confirmation
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return _make_request("POST", f"/snapshots/{snapshot_id}/unlock", public_key, secret_key, {})
|
||||
|
||||
|
||||
def clone_snapshot(
|
||||
snapshot_id: str,
|
||||
clone_type: str = "session",
|
||||
name: Optional[str] = None,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
shell: Optional[str] = None,
|
||||
ports: Optional[List[int]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Clone a snapshot to create a new session or service.
|
||||
|
||||
Args:
|
||||
snapshot_id: Snapshot ID to clone from
|
||||
clone_type: Type of resource to create ("session" or "service")
|
||||
name: Optional name for the new resource
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
shell: Optional shell for session clones
|
||||
ports: Optional ports list for service clones
|
||||
|
||||
Returns:
|
||||
Response dict containing session_id or service_id
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
data: Dict[str, Any] = {"type": clone_type}
|
||||
if name:
|
||||
data["name"] = name
|
||||
if shell:
|
||||
data["shell"] = shell
|
||||
if ports:
|
||||
data["ports"] = ports
|
||||
|
||||
return _make_request("POST", f"/snapshots/{snapshot_id}/clone", public_key, secret_key, data)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Key Validation
|
||||
# =============================================================================
|
||||
|
||||
|
||||
PORTAL_BASE = "https://unsandbox.com"
|
||||
|
||||
|
||||
def validate_keys(
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Validate API keys against the portal.
|
||||
|
||||
Checks if the keys are valid, not expired, and not suspended.
|
||||
|
||||
Args:
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with validation result:
|
||||
- valid: True if keys are valid
|
||||
- tier: Account tier level
|
||||
- expires_at: Expiration timestamp (if applicable)
|
||||
- reason: Reason for invalid status (if applicable)
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
|
||||
url = f"{PORTAL_BASE}/keys/validate"
|
||||
timestamp = int(time.time())
|
||||
body = ""
|
||||
|
||||
signature = _sign_request(secret_key, timestamp, "POST", "/keys/validate", body)
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {public_key}",
|
||||
"X-Timestamp": str(timestamp),
|
||||
"X-Signature": signature,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, data=body, timeout=30)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue