Implement Python asynchronous client SDK
Add complete async Python SDK implementation with all 13 API functions using aiohttp: - execute_code: Execute code asynchronously - execute_async: Start async code execution - get_job: Get job status - wait_for_job: Poll job with exponential backoff (async) - cancel_job: Cancel running job - list_jobs: List all jobs - get_languages: Get supported languages (1-hour cache) - detect_language: Detect language from filename extension - session_snapshot: Create session snapshot (NEW) - service_snapshot: Create service snapshot (NEW) - list_snapshots: List all snapshots (NEW) - restore_snapshot: Restore from snapshot (NEW) - delete_snapshot: Delete snapshot (NEW) Features: - Full async/await support with aiohttp - 4-tier credentials resolution - HMAC-SHA256 authentication - Exponential backoff polling with asyncio.sleep - Languages cache (1-hour TTL) - Type hints and comprehensive docstrings
This commit is contained in:
parent
9b9a76a79a
commit
8acb023bc2
1 changed files with 704 additions and 0 deletions
704
clients/python/async/src/un_async.py
Normal file
704
clients/python/async/src/un_async.py
Normal file
|
|
@ -0,0 +1,704 @@
|
|||
"""
|
||||
PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
|
||||
unsandbox.com Python SDK (Asynchronous)
|
||||
|
||||
Library Usage:
|
||||
import asyncio
|
||||
from un_async 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,
|
||||
)
|
||||
|
||||
async def main():
|
||||
# Execute code synchronously
|
||||
result = await execute_code("python", 'print("hello")', public_key, secret_key)
|
||||
|
||||
# Execute asynchronously
|
||||
job_id = await execute_async("javascript", 'console.log("hello")', public_key, secret_key)
|
||||
|
||||
# Wait for job completion with exponential backoff
|
||||
result = await wait_for_job(job_id, public_key, secret_key)
|
||||
|
||||
# List all jobs
|
||||
jobs = await list_jobs(public_key, secret_key)
|
||||
|
||||
# Get supported languages
|
||||
languages = await get_languages(public_key, secret_key)
|
||||
|
||||
# Snapshot operations
|
||||
snapshot_id = await session_snapshot(session_id, public_key, secret_key)
|
||||
|
||||
asyncio.run(main())
|
||||
|
||||
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)
|
||||
|
||||
Request Authentication (HMAC-SHA256):
|
||||
Authorization: Bearer <public_key>
|
||||
X-Timestamp: <unix_seconds>
|
||||
X-Signature: HMAC-SHA256(secret_key, "timestamp:METHOD:path:body")
|
||||
|
||||
Languages Cache:
|
||||
- Cached in ~/.unsandbox/languages.json
|
||||
- TTL: 1 hour
|
||||
- Updated on successful API calls
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import aiohttp
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
|
||||
API_BASE = "https://api.unsandbox.com"
|
||||
POLL_DELAYS_MS = [300, 450, 700, 900, 650, 1600, 2000]
|
||||
LANGUAGES_CACHE_TTL = 3600 # 1 hour
|
||||
|
||||
|
||||
class CredentialsError(Exception):
|
||||
"""Raised when credentials cannot be found or are invalid."""
|
||||
pass
|
||||
|
||||
|
||||
def _get_unsandbox_dir() -> Path:
|
||||
"""Get ~/.unsandbox directory path, creating if necessary."""
|
||||
home = Path.home()
|
||||
unsandbox_dir = home / ".unsandbox"
|
||||
unsandbox_dir.mkdir(exist_ok=True, mode=0o700)
|
||||
return unsandbox_dir
|
||||
|
||||
|
||||
def _load_credentials_from_csv(csv_path: Path, account_index: int = 0) -> Optional[tuple[str, str]]:
|
||||
"""Load credentials from CSV file (public_key,secret_key per line)."""
|
||||
if not csv_path.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(csv_path, "r") as f:
|
||||
for i, line in enumerate(f):
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
if i == account_index:
|
||||
parts = line.split(",")
|
||||
if len(parts) >= 2:
|
||||
return (parts[0].strip(), parts[1].strip())
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _resolve_credentials(
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
account_index: Optional[int] = None,
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
Resolve credentials from 4-tier priority system.
|
||||
|
||||
Priority:
|
||||
1. Function arguments
|
||||
2. Environment variables
|
||||
3. ~/.unsandbox/accounts.csv
|
||||
4. ./accounts.csv
|
||||
"""
|
||||
# Tier 1: Function arguments
|
||||
if public_key and secret_key:
|
||||
return (public_key, secret_key)
|
||||
|
||||
# Tier 2: Environment variables
|
||||
env_pk = os.environ.get("UNSANDBOX_PUBLIC_KEY")
|
||||
env_sk = os.environ.get("UNSANDBOX_SECRET_KEY")
|
||||
if env_pk and env_sk:
|
||||
return (env_pk, env_sk)
|
||||
|
||||
# Determine account index
|
||||
if account_index is None:
|
||||
account_index = int(os.environ.get("UNSANDBOX_ACCOUNT", "0"))
|
||||
|
||||
# Tier 3: ~/.unsandbox/accounts.csv
|
||||
unsandbox_dir = _get_unsandbox_dir()
|
||||
creds = _load_credentials_from_csv(unsandbox_dir / "accounts.csv", account_index)
|
||||
if creds:
|
||||
return creds
|
||||
|
||||
# Tier 4: ./accounts.csv
|
||||
creds = _load_credentials_from_csv(Path("accounts.csv"), account_index)
|
||||
if creds:
|
||||
return creds
|
||||
|
||||
raise CredentialsError(
|
||||
"No credentials found. Please provide via:\n"
|
||||
" 1. Function arguments (public_key, secret_key)\n"
|
||||
" 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n"
|
||||
" 3. ~/.unsandbox/accounts.csv\n"
|
||||
" 4. ./accounts.csv"
|
||||
)
|
||||
|
||||
|
||||
def _sign_request(
|
||||
secret_key: str,
|
||||
timestamp: int,
|
||||
method: str,
|
||||
path: str,
|
||||
body: Optional[str] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Sign a request using HMAC-SHA256.
|
||||
|
||||
Message format: "timestamp:METHOD:path:body"
|
||||
Returns: 64-character hex string
|
||||
"""
|
||||
body_str = body or ""
|
||||
message = f"{timestamp}:{method}:{path}:{body_str}"
|
||||
signature = hmac.new(
|
||||
secret_key.encode(),
|
||||
message.encode(),
|
||||
hashlib.sha256,
|
||||
).hexdigest()
|
||||
return signature
|
||||
|
||||
|
||||
async def _make_request(
|
||||
method: str,
|
||||
path: str,
|
||||
public_key: str,
|
||||
secret_key: str,
|
||||
data: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Make an authenticated HTTP request to the API (async).
|
||||
|
||||
Raises aiohttp.ClientError on network errors.
|
||||
Raises ValueError if response is not valid JSON.
|
||||
"""
|
||||
url = f"{API_BASE}{path}"
|
||||
timestamp = int(time.time())
|
||||
body = json.dumps(data) if data else ""
|
||||
|
||||
signature = _sign_request(secret_key, timestamp, method, path, body if data else None)
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {public_key}",
|
||||
"X-Timestamp": str(timestamp),
|
||||
"X-Signature": signature,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
if method == "GET":
|
||||
async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=120)) as resp:
|
||||
resp.raise_for_status()
|
||||
return await resp.json()
|
||||
elif method == "POST":
|
||||
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 == "DELETE":
|
||||
async with session.delete(url, headers=headers, timeout=aiohttp.ClientTimeout(total=120)) as resp:
|
||||
resp.raise_for_status()
|
||||
return await resp.json()
|
||||
else:
|
||||
raise ValueError(f"Unsupported HTTP method: {method}")
|
||||
|
||||
|
||||
def _get_languages_cache_path() -> Path:
|
||||
"""Get path to languages cache file."""
|
||||
return _get_unsandbox_dir() / "languages.json"
|
||||
|
||||
|
||||
def _load_languages_cache() -> Optional[List[str]]:
|
||||
"""Load languages from cache if valid (< 1 hour old)."""
|
||||
cache_path = _get_languages_cache_path()
|
||||
if not cache_path.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(cache_path, "r") as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Check if cache is fresh
|
||||
mtime = cache_path.stat().st_mtime
|
||||
age_seconds = time.time() - mtime
|
||||
if age_seconds < LANGUAGES_CACHE_TTL:
|
||||
return data.get("languages")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _save_languages_cache(languages: List[str]) -> None:
|
||||
"""Save languages to cache."""
|
||||
try:
|
||||
cache_path = _get_languages_cache_path()
|
||||
with open(cache_path, "w") as f:
|
||||
json.dump({"languages": languages, "timestamp": int(time.time())}, f)
|
||||
except Exception:
|
||||
pass # Cache failures are non-fatal
|
||||
|
||||
|
||||
async def execute_code(
|
||||
language: str,
|
||||
code: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute code synchronously (awaits until completion).
|
||||
|
||||
Args:
|
||||
language: Programming language (e.g., "python", "javascript", "go")
|
||||
code: Source code to execute
|
||||
public_key: Optional API key (uses credentials resolution if not provided)
|
||||
secret_key: Optional API secret (uses credentials resolution if not provided)
|
||||
|
||||
Returns:
|
||||
Response dict containing stdout, stderr, exit code, 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(
|
||||
"POST",
|
||||
"/execute",
|
||||
public_key,
|
||||
secret_key,
|
||||
{"language": language, "code": code},
|
||||
)
|
||||
|
||||
# If we got a job_id, poll until completion
|
||||
job_id = response.get("job_id")
|
||||
status = response.get("status")
|
||||
|
||||
if job_id and status in ("pending", "running"):
|
||||
return await wait_for_job(job_id, public_key, secret_key)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
async def execute_async(
|
||||
language: str,
|
||||
code: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Execute code asynchronously (returns immediately with job_id).
|
||||
|
||||
Args:
|
||||
language: Programming language (e.g., "python", "javascript")
|
||||
code: Source code to execute
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Job ID string
|
||||
|
||||
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(
|
||||
"POST",
|
||||
"/execute",
|
||||
public_key,
|
||||
secret_key,
|
||||
{"language": language, "code": code},
|
||||
)
|
||||
return response.get("job_id")
|
||||
|
||||
|
||||
async def get_job(
|
||||
job_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get current status/result of a job (single poll, no waiting).
|
||||
|
||||
Args:
|
||||
job_id: Job ID from execute_async()
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Job response 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"/jobs/{job_id}", public_key, secret_key)
|
||||
|
||||
|
||||
async def wait_for_job(
|
||||
job_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Wait for job completion with exponential backoff polling (async).
|
||||
|
||||
Polling delays (ms): [300, 450, 700, 900, 650, 1600, 2000, ...]
|
||||
Cumulative: 300, 750, 1450, 2350, 3000, 4600, 6600ms+
|
||||
|
||||
Args:
|
||||
job_id: Job ID from execute_async()
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Final job result when status is terminal (completed, failed, timeout, cancelled)
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
poll_count = 0
|
||||
|
||||
while True:
|
||||
# Sleep before polling
|
||||
delay_idx = min(poll_count, len(POLL_DELAYS_MS) - 1)
|
||||
await asyncio.sleep(POLL_DELAYS_MS[delay_idx] / 1000.0)
|
||||
poll_count += 1
|
||||
|
||||
response = await get_job(job_id, public_key, secret_key)
|
||||
status = response.get("status")
|
||||
|
||||
if status in ("completed", "failed", "timeout", "cancelled"):
|
||||
return response
|
||||
|
||||
# Still running, continue polling
|
||||
|
||||
|
||||
async def cancel_job(
|
||||
job_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Cancel a running job.
|
||||
|
||||
Args:
|
||||
job_id: Job ID to cancel
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with cancellation 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"/jobs/{job_id}", public_key, secret_key)
|
||||
|
||||
|
||||
async def list_jobs(
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
List all jobs for the authenticated account.
|
||||
|
||||
Args:
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
List of job dicts
|
||||
|
||||
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", "/jobs", public_key, secret_key)
|
||||
return response.get("jobs", [])
|
||||
|
||||
|
||||
async def get_languages(
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> List[str]:
|
||||
"""
|
||||
Get list of supported programming languages.
|
||||
|
||||
Results are cached for 1 hour in ~/.unsandbox/languages.json
|
||||
|
||||
Args:
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
List of language identifiers (e.g., ["python", "javascript", "go", ...])
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
# Try cache first
|
||||
cached = _load_languages_cache()
|
||||
if cached:
|
||||
return cached
|
||||
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
response = await _make_request("GET", "/languages", public_key, secret_key)
|
||||
languages = response.get("languages", [])
|
||||
|
||||
# Cache the result
|
||||
_save_languages_cache(languages)
|
||||
return languages
|
||||
|
||||
|
||||
# Language detection mapping (file extension -> language)
|
||||
_LANGUAGE_MAP = {
|
||||
"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",
|
||||
}
|
||||
|
||||
|
||||
def detect_language(filename: str) -> Optional[str]:
|
||||
"""
|
||||
Detect programming language from filename extension.
|
||||
|
||||
Args:
|
||||
filename: Filename to detect language from (e.g., "script.py")
|
||||
|
||||
Returns:
|
||||
Language identifier (e.g., "python") or None if unknown
|
||||
|
||||
Examples:
|
||||
detect_language("hello.py") # -> "python"
|
||||
detect_language("script.js") # -> "javascript"
|
||||
detect_language("main.go") # -> "go"
|
||||
detect_language("unknown") # -> None
|
||||
"""
|
||||
if not filename or "." not in filename:
|
||||
return None
|
||||
|
||||
ext = filename.rsplit(".", 1)[-1].lower()
|
||||
return _LANGUAGE_MAP.get(ext)
|
||||
|
||||
|
||||
async def session_snapshot(
|
||||
session_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
hot: bool = False,
|
||||
) -> str:
|
||||
"""
|
||||
Create a snapshot of a session (NEW).
|
||||
|
||||
Args:
|
||||
session_id: Session ID to snapshot
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
name: Optional snapshot name
|
||||
hot: If True, snapshot running session (hot snapshot)
|
||||
|
||||
Returns:
|
||||
Snapshot ID
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
data = {"session_id": session_id, "hot": hot}
|
||||
if name:
|
||||
data["name"] = name
|
||||
|
||||
response = await _make_request("POST", "/snapshots", public_key, secret_key, data)
|
||||
return response.get("snapshot_id")
|
||||
|
||||
|
||||
async def service_snapshot(
|
||||
service_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
hot: bool = False,
|
||||
) -> str:
|
||||
"""
|
||||
Create a snapshot of a service (NEW).
|
||||
|
||||
Args:
|
||||
service_id: Service ID to snapshot
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
name: Optional snapshot name
|
||||
hot: If True, snapshot running service (hot snapshot)
|
||||
|
||||
Returns:
|
||||
Snapshot ID
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
data = {"service_id": service_id, "hot": hot}
|
||||
if name:
|
||||
data["name"] = name
|
||||
|
||||
response = await _make_request("POST", "/snapshots", public_key, secret_key, data)
|
||||
return response.get("snapshot_id")
|
||||
|
||||
|
||||
async def list_snapshots(
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
List all snapshots (NEW).
|
||||
|
||||
Args:
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
List of snapshot dicts
|
||||
|
||||
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", "/snapshots", public_key, secret_key)
|
||||
return response.get("snapshots", [])
|
||||
|
||||
|
||||
async def restore_snapshot(
|
||||
snapshot_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Restore a snapshot (NEW).
|
||||
|
||||
Args:
|
||||
snapshot_id: Snapshot ID to restore
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with restored resource info
|
||||
|
||||
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}/restore", public_key, secret_key, {})
|
||||
|
||||
|
||||
async def delete_snapshot(
|
||||
snapshot_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Delete a snapshot (NEW).
|
||||
|
||||
Args:
|
||||
snapshot_id: Snapshot 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"/snapshots/{snapshot_id}", public_key, secret_key)
|
||||
Loading…
Add table
Add a link
Reference in a new issue