Export Selected Projects
This example demonstrates how to export one or more selected projects from your Voyager account. The script allows you to configure which projects to export, handles authentication, export requests, status polling, and downloads.Overview
This script will:- Authenticate with the Voyager API
- List available projects (optionally filtered)
- Allow you to select which projects to export (configurable)
- Request exports for selected projects
- Monitor export status
- Download completed exports
- Handle errors gracefully
Complete Script
Copy
Ask AI
"""
Export All Active Projects
Automatically exports all active projects from Voyager
"""
import os
import sys
import time
import requests
from typing import Dict, Any, Optional
from pathlib import Path
# Configuration
API_URL = os.getenv("VOYAGER_API_URL", "https://voyager.lumafield.com")
EMAIL = os.getenv("VOYAGER_EMAIL")
PASSWORD = os.getenv("VOYAGER_PASSWORD")
OUTPUT_DIR = os.getenv("VOYAGER_EXPORT_DIR", "~/Downloads/voyager_exports")
# Export configuration
EXPORT_CONFIG = {
"exportMeshes": True,
"exportVolumes": True,
"exportRadiographs": True
}
# Timing configuration
MAX_WAIT_TIME = 1800 # 30 minutes
CHECK_INTERVAL = 30 # Check every 30 seconds
class VoyagerExporter:
"""Voyager API client for exporting projects"""
def __init__(self, api_url: str, email: str, password: str):
self.api_url = api_url.rstrip("/")
self.email = email
self.password = password
self.token = None
self.session = requests.Session()
self._authenticate()
def list_projects_with_filter(
self,
state: Optional[str] = None,
workspace: Optional[str] = None,
tag: Optional[str] = None,
search: Optional[str] = None
) -> Dict[str, Any]:
"""List projects with optional filters"""
params = {}
if state:
params["state"] = state
if workspace:
params["workspace"] = workspace
if tag:
params["tag"] = tag
if search:
params["search"] = search
all_projects = []
page = 1
while True:
params["page"] = page
params["page_size"] = 100
response = self.session.get(
f"{self.api_url}/api/v2/projects",
params=params
)
response.raise_for_status()
data = response.json()
all_projects.extend(data["results"])
if not data.get("next"):
break
page += 1
return {"count": len(all_projects), "results": all_projects}
def _authenticate(self):
"""Authenticate and get token"""
response = requests.post(
f"{self.api_url}/api/login",
json={"email": self.email, "password": self.password}
)
response.raise_for_status()
self.token = response.json()["token"]
self.session.headers.update({
"Authorization": f"Token {self.token}",
"Content-Type": "application/json"
})
print(f"✓ Authenticated as {self.email}")
def list_projects(self, state: Optional[str] = None) -> Dict[str, Any]:
"""List all projects"""
params = {}
if state:
params["state"] = state
all_projects = []
page = 1
while True:
params["page"] = page
params["page_size"] = 100
response = self.session.get(
f"{self.api_url}/api/v2/projects",
params=params
)
response.raise_for_status()
data = response.json()
all_projects.extend(data["results"])
if not data.get("next"):
break
page += 1
return {"count": len(all_projects), "results": all_projects}
def request_export(self, project_id: str) -> Dict[str, Any]:
"""Request project export"""
response = self.session.post(
f"{self.api_url}/api/v2/projects/{project_id}/export",
json={
"config": EXPORT_CONFIG,
"sendToRequester": False
}
)
response.raise_for_status()
return response.json()
def check_export_status(self, project_id: str) -> Dict[str, Any]:
"""Check export status"""
response = self.session.get(
f"{self.api_url}/api/v2/projects/{project_id}/export"
)
response.raise_for_status()
return response.json()
def wait_for_export(
self,
project_id: str,
max_wait: int = MAX_WAIT_TIME,
check_interval: int = CHECK_INTERVAL
) -> Optional[str]:
"""Wait for export to complete and return download URL"""
start_time = time.time()
while time.time() - start_time < max_wait:
status = self.check_export_status(project_id)
if status.get("url"):
return status["url"]
elapsed = int(time.time() - start_time)
if elapsed % 60 == 0: # Print every minute
print(f" Still processing... ({elapsed}s elapsed)")
time.sleep(check_interval)
return None
def download_export(self, download_url: str, output_path: str):
"""Download export archive"""
response = requests.get(download_url, stream=True)
response.raise_for_status()
total_size = int(response.headers.get("content-length", 0))
downloaded = 0
with open(output_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded += len(chunk)
if total_size > 0:
progress = (downloaded / total_size) * 100
mb_downloaded = downloaded / (1024 * 1024)
mb_total = total_size / (1024 * 1024)
print(f"\r Downloading: {progress:.1f}% ({mb_downloaded:.1f}/{mb_total:.1f} MB)",
end="", flush=True)
print()
def sanitize_filename(name: str) -> str:
"""Sanitize filename for filesystem"""
return "".join(c if c.isalnum() or c in (' ', '-', '_') else '_' for c in name)
def select_projects(projects: list, selection_mode: str = "interactive") -> list:
"""Select which projects to export"""
if selection_mode == "all":
return projects
elif selection_mode == "interactive":
print("\nAvailable projects:")
for i, project in enumerate(projects, 1):
print(f" {i}. {project.get('name', 'Unnamed')} (ID: {project['id'][:8]}...)")
print("\nSelect projects to export:")
print(" - Enter project numbers separated by commas (e.g., 1,3,5)")
print(" - Enter 'all' to export all projects")
print(" - Enter 'none' to skip")
choice = input("\nSelection: ").strip().lower()
if choice == "all":
return projects
elif choice == "none":
return []
else:
try:
indices = [int(x.strip()) - 1 for x in choice.split(",")]
selected = [projects[i] for i in indices if 0 <= i < len(projects)]
return selected
except (ValueError, IndexError):
print("Invalid selection, exporting all projects")
return projects
else:
# selection_mode could be a list of project IDs
if isinstance(selection_mode, list):
return [p for p in projects if p["id"] in selection_mode]
return projects
def export_selected_projects(
output_dir: Optional[str] = None,
project_state: str = "active",
project_ids: Optional[list] = None,
workspace: Optional[str] = None,
selection_mode: str = "interactive",
skip_existing: bool = True
):
"""Export selected projects"""
# Validate credentials
if not EMAIL or not PASSWORD:
print("ERROR: VOYAGER_EMAIL and VOYAGER_PASSWORD must be set")
print("\nSet environment variables:")
print(" export VOYAGER_EMAIL='your-email@company.com'")
print(" export VOYAGER_PASSWORD='your-password'")
sys.exit(1)
# Setup output directory
if not output_dir:
output_dir = os.path.expanduser(OUTPUT_DIR)
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
print("=" * 70)
print(" VOYAGER PROJECT EXPORTER")
print("=" * 70)
print(f"\nAPI URL: {API_URL}")
print(f"Output Directory: {output_dir}")
print(f"Project State: {project_state}")
if workspace:
print(f"Workspace Filter: {workspace}")
if project_ids:
print(f"Specific Project IDs: {len(project_ids)} project(s)")
print(f"Selection Mode: {selection_mode}")
print(f"Export Config: {EXPORT_CONFIG}")
print()
try:
# Initialize client
exporter = VoyagerExporter(API_URL, EMAIL, PASSWORD)
# List projects with filters
print("Fetching projects...")
projects_data = exporter.list_projects_with_filter(
state=project_state,
workspace=workspace
)
all_projects = projects_data["results"]
if not all_projects:
print(f"No {project_state} projects found.")
return
# Filter by specific project IDs if provided
if project_ids:
all_projects = [p for p in all_projects if p["id"] in project_ids]
if not all_projects:
print("None of the specified project IDs were found.")
return
# Select which projects to export
print(f"Found {len(all_projects)} project(s)")
projects_to_export = select_projects(all_projects, selection_mode)
if not projects_to_export:
print("No projects selected for export.")
return
print(f"\nSelected {len(projects_to_export)} project(s) for export:")
for project in projects_to_export:
print(f" • {project.get('name', 'Unnamed')}")
print()
print(f"✓ Found {len(projects)} {project_state} project(s)\n")
# Export each selected project
successful = 0
failed = 0
skipped = 0
for i, project in enumerate(projects_to_export, 1):
project_id = project["id"]
project_name = project.get("name", "Unnamed")
print(f"[{i}/{len(projects)}] {project_name}")
print(f" Project ID: {project_id}")
# Check if already exported
safe_name = sanitize_filename(project_name)
archive_filename = f"{safe_name}_{project_id[:8]}_export.zip"
archive_path = output_path / archive_filename
if skip_existing and archive_path.exists():
print(f" ⏭ Skipped (already exists)")
skipped += 1
print()
continue
try:
# Request export
print(" Requesting export...")
export_info = exporter.request_export(project_id)
export_id = export_info.get("id")
print(f" ✓ Export requested (ID: {export_id})")
# Wait for completion
print(" Waiting for export to complete...")
download_url = exporter.wait_for_export(project_id)
if not download_url:
print(" ⚠ Timeout: Export not ready after maximum wait time")
print(" You can check status later or run this script again")
failed += 1
print()
continue
# Download export
print(" Downloading export...")
exporter.download_export(download_url, str(archive_path))
# Verify download
if archive_path.exists():
file_size_mb = archive_path.stat().st_size / (1024 * 1024)
print(f" ✓ Export complete ({file_size_mb:.1f} MB)")
successful += 1
else:
print(" ✗ Download failed")
failed += 1
except requests.exceptions.HTTPError as e:
if e.response.status_code == 403:
print(f" ✗ Failed: Insufficient permissions (PROJECT_DATA_EXPORT required)")
elif e.response.status_code == 404:
print(f" ✗ Failed: Project not found")
else:
print(f" ✗ Failed: HTTP {e.response.status_code} - {e.response.text}")
failed += 1
except Exception as e:
print(f" ✗ Failed: {e}")
failed += 1
print()
# Summary
print("=" * 70)
print(" EXPORT SUMMARY")
print("=" * 70)
print(f" Successful: {successful}")
print(f" Failed: {failed}")
print(f" Skipped: {skipped}")
print(f" Total Selected: {len(projects_to_export)}")
print(f"\n Exports saved to: {output_dir}")
print()
except Exception as e:
print(f"\nERROR: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
# Parse command line arguments
# Usage: python export_selected_projects.py [output_dir] [state] [project_ids] [workspace] [selection_mode]
# Examples:
# python export_selected_projects.py
# python export_selected_projects.py ~/exports active
# python export_selected_projects.py ~/exports active "uuid1,uuid2" "" "all"
output_dir = sys.argv[1] if len(sys.argv) > 1 else None
project_state = sys.argv[2] if len(sys.argv) > 2 else "active"
project_ids_arg = sys.argv[3] if len(sys.argv) > 3 else None
workspace = sys.argv[4] if len(sys.argv) > 4 else None
selection_mode = sys.argv[5] if len(sys.argv) > 5 else "interactive"
# Parse project IDs if provided
project_ids = None
if project_ids_arg:
project_ids = [pid.strip() for pid in project_ids_arg.split(",")]
export_selected_projects(
output_dir=output_dir,
project_state=project_state,
project_ids=project_ids,
workspace=workspace,
selection_mode=selection_mode
)
Usage
Basic Usage (Interactive Selection)
Copy
Ask AI
# Set environment variables
export VOYAGER_API_URL="https://voyager.lumafield.com"
export VOYAGER_EMAIL="your-email@company.com"
export VOYAGER_PASSWORD="your-password"
# Run the script - will prompt you to select projects
python export_selected_projects.py
Export Specific Projects by ID
Copy
Ask AI
# Export specific projects by UUID
python export_selected_projects.py ~/exports active "uuid1,uuid2,uuid3"
Export All Projects (Non-Interactive)
Copy
Ask AI
# Export all active projects without prompting
python export_selected_projects.py ~/exports active "" "" "all"
Export from Specific Workspace
Copy
Ask AI
# Export projects from a specific workspace
python export_selected_projects.py ~/exports active "" "workspace-uuid"
Features
- Automatic pagination - Handles projects across multiple pages
- Status polling - Monitors export progress automatically
- Progress tracking - Shows download progress with file sizes
- Error handling - Gracefully handles failures and continues
- Skip existing - Skips projects that are already exported
- Resumable - Can be run multiple times safely
Customization
Change Export Configuration
Modify theEXPORT_CONFIG dictionary:
Copy
Ask AI
EXPORT_CONFIG = {
"exportMeshes": True,
"exportVolumes": False, # Skip volumes
"exportRadiographs": True
}
Adjust Timing
Modify timing constants:Copy
Ask AI
MAX_WAIT_TIME = 3600 # Wait up to 1 hour
CHECK_INTERVAL = 60 # Check every minute
Programmatic Selection
Use the function programmatically:Copy
Ask AI
# Export specific projects by ID
export_selected_projects(
output_dir="~/exports",
project_state="active",
project_ids=["uuid1", "uuid2", "uuid3"],
selection_mode="all" # Skip interactive prompt
)
# Export from specific workspace
export_selected_projects(
output_dir="~/exports",
workspace="workspace-uuid",
selection_mode="all"
)
Error Handling
The script handles common errors:- 403 Forbidden: Missing
PROJECT_DATA_EXPORTcapability - 404 Not Found: Project doesn’t exist or was deleted
- 429 Rate Limit: Too many requests (add delays)
- Timeout: Export took longer than
MAX_WAIT_TIME
Next Steps
- Data Export Guide - Learn more about export options
- API Reference - Complete endpoint reference
- Error Handling - Learn about error responses