Skip to main content

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:
  1. Authenticate with the Voyager API
  2. List available projects (optionally filtered)
  3. Allow you to select which projects to export (configurable)
  4. Request exports for selected projects
  5. Monitor export status
  6. Download completed exports
  7. Handle errors gracefully

Complete Script

"""
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)

# 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

# Export specific projects by UUID
python export_selected_projects.py ~/exports active "uuid1,uuid2,uuid3"

Export All Projects (Non-Interactive)

# Export all active projects without prompting
python export_selected_projects.py ~/exports active "" "" "all"

Export from Specific Workspace

# 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 the EXPORT_CONFIG dictionary:
EXPORT_CONFIG = {
    "exportMeshes": True,
    "exportVolumes": False,  # Skip volumes
    "exportRadiographs": True
}

Adjust Timing

Modify timing constants:
MAX_WAIT_TIME = 3600  # Wait up to 1 hour
CHECK_INTERVAL = 60   # Check every minute

Programmatic Selection

Use the function programmatically:
# 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_EXPORT capability
  • 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