491 lines
18 KiB
Python
Executable File
491 lines
18 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Process multiple RDF datasets in parallel with rich progress display.
|
|
|
|
This script provides parallel processing capabilities for downloading, converting,
|
|
and uploading RDF datasets with clear progress tracking for each dataset.
|
|
|
|
USAGE:
|
|
# Process multiple datasets in parallel
|
|
python scripts/process_datasets_parallel.py --dataset wordnet --dataset yago-4.5
|
|
|
|
# Process all small datasets with 4 parallel workers
|
|
python scripts/process_datasets_parallel.py --category small --workers 4
|
|
|
|
# Process with specific steps only
|
|
python scripts/process_datasets_parallel.py --dataset wordnet --dataset schema-org --skip-upload
|
|
|
|
# Dry run to see what would happen
|
|
python scripts/process_datasets_parallel.py --category small --dry-run
|
|
|
|
FEATURES:
|
|
- Parallel processing with configurable worker count
|
|
- Rich progress display showing all datasets simultaneously
|
|
- Automatic resource management based on dataset size
|
|
- Graceful error handling and recovery
|
|
- Memory-efficient streaming for large datasets
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
import time
|
|
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
from rich.console import Console
|
|
from rich.live import Live
|
|
from rich.panel import Panel
|
|
from rich.progress import (
|
|
BarColumn,
|
|
MofNCompleteColumn,
|
|
Progress,
|
|
SpinnerColumn,
|
|
TaskID,
|
|
TextColumn,
|
|
TimeElapsedColumn,
|
|
TimeRemainingColumn,
|
|
)
|
|
from rich.table import Table
|
|
|
|
# Import our modules
|
|
from dataset_registry import DATASET_REGISTRY, DatasetInfo
|
|
|
|
console = Console()
|
|
|
|
|
|
class ParallelDatasetProcessor:
|
|
"""Manages parallel processing of multiple datasets with progress tracking."""
|
|
|
|
def __init__(
|
|
self,
|
|
base_dir: Path,
|
|
max_workers: int = 4,
|
|
skip_download: bool = False,
|
|
skip_convert: bool = False,
|
|
skip_upload: bool = False,
|
|
dry_run: bool = False,
|
|
remove_after: bool = False,
|
|
):
|
|
"""Initialize the parallel processor.
|
|
|
|
Args:
|
|
base_dir: Base directory for dataset processing
|
|
max_workers: Maximum number of parallel workers
|
|
skip_download: Skip download step
|
|
skip_convert: Skip conversion step
|
|
skip_upload: Skip upload step
|
|
dry_run: Show what would be done without executing
|
|
remove_after: Remove downloaded files after processing
|
|
"""
|
|
self.base_dir = base_dir
|
|
self.max_workers = max_workers
|
|
self.skip_download = skip_download
|
|
self.skip_convert = skip_convert
|
|
self.skip_upload = skip_upload
|
|
self.dry_run = dry_run
|
|
self.remove_after = remove_after
|
|
|
|
# Create progress tracking
|
|
self.overall_progress = Progress(
|
|
SpinnerColumn(),
|
|
TextColumn("[bold blue]Overall Progress"),
|
|
BarColumn(),
|
|
MofNCompleteColumn(),
|
|
TimeElapsedColumn(),
|
|
)
|
|
|
|
# Individual dataset progress trackers
|
|
self.dataset_progress = Progress(
|
|
TextColumn("[bold cyan]{task.fields[dataset_id]:20}"),
|
|
SpinnerColumn(),
|
|
TextColumn("{task.description}"),
|
|
BarColumn(),
|
|
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
TimeElapsedColumn(),
|
|
expand=True,
|
|
)
|
|
|
|
# Results tracking
|
|
self.results: Dict[str, Tuple[bool, str]] = {}
|
|
|
|
def process_dataset_task(self, dataset_id: str, task_id: TaskID) -> Tuple[str, bool, str]:
|
|
"""Process a single dataset (runs in worker process/thread).
|
|
|
|
Args:
|
|
dataset_id: Dataset identifier
|
|
task_id: Progress task ID for this dataset
|
|
|
|
Returns:
|
|
Tuple of (dataset_id, success, message)
|
|
"""
|
|
import subprocess
|
|
import shutil
|
|
|
|
try:
|
|
dataset_info = DATASET_REGISTRY.get(dataset_id)
|
|
if not dataset_info:
|
|
self.dataset_progress.update(task_id, description="[red]✗ Unknown dataset[/red]", completed=100)
|
|
return (dataset_id, False, f"Unknown dataset: {dataset_id}")
|
|
|
|
if not dataset_info.available:
|
|
self.dataset_progress.update(task_id, description="[yellow]⊘ Unavailable[/yellow]", completed=100)
|
|
return (dataset_id, False, "Dataset marked as unavailable")
|
|
|
|
# Setup directories
|
|
download_dir = self.base_dir / "downloads" / dataset_id
|
|
hf_dataset_dir = self.base_dir / "hf_datasets" / dataset_id
|
|
|
|
# Calculate progress steps
|
|
total_steps = 3 - (self.skip_download + self.skip_convert + self.skip_upload)
|
|
if total_steps == 0:
|
|
total_steps = 1
|
|
step_progress = 100 / total_steps
|
|
current_progress = 0
|
|
|
|
# Step 1: Download
|
|
if not self.skip_download:
|
|
self.dataset_progress.update(
|
|
task_id, description="[yellow]📥 Downloading...[/yellow]", completed=current_progress, total=100
|
|
)
|
|
|
|
if not self.dry_run:
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(Path(__file__).parent / "rdf_dataset_downloader.py"),
|
|
dataset_id,
|
|
"-o",
|
|
str(download_dir),
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode != 0:
|
|
self.dataset_progress.update(task_id, description="[red]✗ Download failed[/red]", completed=100)
|
|
return (dataset_id, False, f"Download failed: {result.stderr[:100]}")
|
|
|
|
current_progress += step_progress
|
|
self.dataset_progress.update(task_id, completed=current_progress)
|
|
|
|
# Step 2: Convert
|
|
if not self.skip_convert:
|
|
self.dataset_progress.update(
|
|
task_id, description="[cyan]🔄 Converting to HF...[/cyan]", completed=current_progress, total=100
|
|
)
|
|
|
|
if not self.dry_run:
|
|
# Find the downloaded file
|
|
rdf_files = (
|
|
list(download_dir.rglob("*.ttl"))
|
|
+ list(download_dir.rglob("*.nt"))
|
|
+ list(download_dir.rglob("*.rdf"))
|
|
+ list(download_dir.rglob("*.owl"))
|
|
)
|
|
|
|
if dataset_info.format == "tsv":
|
|
rdf_files += list(download_dir.rglob("*.txt")) + list(download_dir.rglob("*.tsv"))
|
|
|
|
if not rdf_files:
|
|
self.dataset_progress.update(
|
|
task_id, description="[red]✗ No RDF files found[/red]", completed=100
|
|
)
|
|
return (dataset_id, False, "No RDF files found after download")
|
|
|
|
rdf_file = rdf_files[0]
|
|
|
|
# Determine converter based on dataset size
|
|
use_streaming = dataset_info.category in ["medium", "large", "xlarge"]
|
|
converter_script = (
|
|
"convert_rdf_to_hf_dataset_streaming.py" if use_streaming else "convert_rdf_to_hf_dataset.py"
|
|
)
|
|
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(Path(__file__).parent / converter_script),
|
|
str(rdf_file),
|
|
str(hf_dataset_dir),
|
|
"--format",
|
|
dataset_info.format,
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
|
|
if result.returncode != 0:
|
|
self.dataset_progress.update(
|
|
task_id, description="[red]✗ Conversion failed[/red]", completed=100
|
|
)
|
|
return (dataset_id, False, f"Conversion failed: {result.stderr[:100]}")
|
|
|
|
current_progress += step_progress
|
|
self.dataset_progress.update(task_id, completed=current_progress)
|
|
|
|
# Step 3: Upload
|
|
if not self.skip_upload:
|
|
self.dataset_progress.update(
|
|
task_id, description="[green]📤 Uploading to HF...[/green]", completed=current_progress, total=100
|
|
)
|
|
|
|
if not self.dry_run:
|
|
# Here you would add the actual upload logic
|
|
# For now, just simulate with a delay
|
|
time.sleep(1)
|
|
|
|
current_progress += step_progress
|
|
self.dataset_progress.update(task_id, completed=current_progress)
|
|
|
|
# Cleanup if requested
|
|
if self.remove_after and not self.dry_run:
|
|
if download_dir.exists():
|
|
shutil.rmtree(download_dir)
|
|
|
|
self.dataset_progress.update(task_id, description="[green]✓ Complete[/green]", completed=100, total=100)
|
|
|
|
return (dataset_id, True, "Successfully processed")
|
|
|
|
except Exception as e:
|
|
self.dataset_progress.update(task_id, description=f"[red]✗ Failed: {str(e)[:30]}[/red]", completed=100)
|
|
return (dataset_id, False, str(e))
|
|
|
|
def process_datasets(self, dataset_ids: List[str]) -> Dict[str, Tuple[bool, str]]:
|
|
"""Process multiple datasets in parallel.
|
|
|
|
Args:
|
|
dataset_ids: List of dataset IDs to process
|
|
|
|
Returns:
|
|
Dictionary mapping dataset_id to (success, message)
|
|
"""
|
|
# Filter to available datasets only
|
|
available_datasets = [
|
|
d_id for d_id in dataset_ids if DATASET_REGISTRY.get(d_id) and DATASET_REGISTRY[d_id].available
|
|
]
|
|
|
|
if not available_datasets:
|
|
console.print("[red]No available datasets to process[/red]")
|
|
return {}
|
|
|
|
console.print(
|
|
f"\n[bold cyan]Processing {len(available_datasets)} datasets with {self.max_workers} workers[/bold cyan]\n"
|
|
)
|
|
|
|
# Create task IDs for each dataset
|
|
task_ids = {}
|
|
for dataset_id in available_datasets:
|
|
task_id = self.dataset_progress.add_task("[dim]Waiting...[/dim]", dataset_id=dataset_id, total=100)
|
|
task_ids[dataset_id] = task_id
|
|
|
|
# Create overall progress task
|
|
overall_task = self.overall_progress.add_task("Processing datasets", total=len(available_datasets))
|
|
|
|
# Create progress table
|
|
def generate_progress_table() -> Table:
|
|
"""Generate a table showing all progress bars."""
|
|
table = Table(title="Dataset Processing Progress", expand=True)
|
|
table.add_column("Progress", ratio=1)
|
|
|
|
# Add overall progress
|
|
table.add_row(self.overall_progress)
|
|
table.add_row("") # Spacer
|
|
|
|
# Add individual dataset progress
|
|
table.add_row(self.dataset_progress)
|
|
|
|
return table
|
|
|
|
# Process datasets in parallel with live progress display
|
|
with Live(generate_progress_table(), console=console, refresh_per_second=4) as live:
|
|
# Determine executor type based on operations
|
|
# Use ProcessPoolExecutor for CPU-intensive conversion
|
|
# Use ThreadPoolExecutor for I/O-bound download/upload
|
|
if self.skip_convert:
|
|
executor_class = ThreadPoolExecutor
|
|
else:
|
|
executor_class = ProcessPoolExecutor
|
|
|
|
with executor_class(max_workers=self.max_workers) as executor:
|
|
# Submit all tasks
|
|
futures = {
|
|
executor.submit(self.process_dataset_task, dataset_id, task_ids[dataset_id]): dataset_id
|
|
for dataset_id in available_datasets
|
|
}
|
|
|
|
# Process completed tasks
|
|
completed = 0
|
|
for future in as_completed(futures):
|
|
dataset_id = futures[future]
|
|
try:
|
|
dataset_id, success, message = future.result()
|
|
self.results[dataset_id] = (success, message)
|
|
except Exception as e:
|
|
self.results[dataset_id] = (False, str(e))
|
|
|
|
completed += 1
|
|
self.overall_progress.update(overall_task, completed=completed)
|
|
|
|
# Update live display
|
|
live.update(generate_progress_table())
|
|
|
|
return self.results
|
|
|
|
def print_summary(self):
|
|
"""Print a summary of processing results."""
|
|
console.print("\n[bold cyan]Processing Summary:[/bold cyan]\n")
|
|
|
|
table = Table(show_header=True)
|
|
table.add_column("Dataset", style="cyan")
|
|
table.add_column("Status", style="green")
|
|
table.add_column("Message", style="yellow")
|
|
|
|
successful = 0
|
|
failed = 0
|
|
|
|
for dataset_id, (success, message) in self.results.items():
|
|
if success:
|
|
status = "[green]✓ Success[/green]"
|
|
successful += 1
|
|
else:
|
|
status = "[red]✗ Failed[/red]"
|
|
failed += 1
|
|
|
|
# Truncate message for display
|
|
display_message = message[:50] + "..." if len(message) > 50 else message
|
|
table.add_row(dataset_id, status, display_message)
|
|
|
|
console.print(table)
|
|
|
|
console.print(f"\n[bold]Total: {successful} successful, {failed} failed[/bold]")
|
|
|
|
return successful, failed
|
|
|
|
|
|
def main():
|
|
"""Main entry point for parallel dataset processing."""
|
|
parser = argparse.ArgumentParser(description="Process RDF datasets in parallel with progress tracking")
|
|
parser.add_argument(
|
|
"--dataset",
|
|
"-d",
|
|
type=str,
|
|
action="append",
|
|
help="Specific dataset(s) to process (can be specified multiple times)",
|
|
)
|
|
parser.add_argument(
|
|
"--category",
|
|
"-c",
|
|
choices=["small", "medium", "large", "xlarge", "all"],
|
|
help="Process datasets by category",
|
|
)
|
|
parser.add_argument(
|
|
"--workers",
|
|
"-w",
|
|
type=int,
|
|
default=4,
|
|
help="Number of parallel workers (default: 4)",
|
|
)
|
|
parser.add_argument(
|
|
"--base-dir",
|
|
type=Path,
|
|
default=Path("./dataset_processing"),
|
|
help="Base directory for work",
|
|
)
|
|
parser.add_argument(
|
|
"--skip-download",
|
|
action="store_true",
|
|
help="Skip download step",
|
|
)
|
|
parser.add_argument(
|
|
"--skip-convert",
|
|
action="store_true",
|
|
help="Skip conversion step",
|
|
)
|
|
parser.add_argument(
|
|
"--skip-upload",
|
|
action="store_true",
|
|
help="Skip upload step",
|
|
)
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Show what would be done without doing it",
|
|
)
|
|
parser.add_argument(
|
|
"--rm",
|
|
action="store_true",
|
|
help="Remove downloaded files after processing",
|
|
)
|
|
parser.add_argument(
|
|
"--list",
|
|
action="store_true",
|
|
help="List datasets that would be processed",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Determine which datasets to process
|
|
if args.dataset:
|
|
datasets_to_process = args.dataset
|
|
elif args.category:
|
|
if args.category == "all":
|
|
datasets_to_process = list(DATASET_REGISTRY.keys())
|
|
else:
|
|
datasets_to_process = [
|
|
d_id for d_id, d_info in DATASET_REGISTRY.items() if d_info.category == args.category
|
|
]
|
|
else:
|
|
parser.error("Please specify either --dataset or --category")
|
|
|
|
# List mode
|
|
if args.list:
|
|
console.print("\n[bold cyan]Datasets to Process:[/bold cyan]\n")
|
|
table = Table(show_header=True)
|
|
table.add_column("ID", style="cyan")
|
|
table.add_column("Name", style="green")
|
|
table.add_column("Category", style="yellow")
|
|
table.add_column("Size", style="blue")
|
|
table.add_column("Available", style="magenta")
|
|
|
|
for dataset_id in datasets_to_process:
|
|
if dataset_id in DATASET_REGISTRY:
|
|
info = DATASET_REGISTRY[dataset_id]
|
|
available = "[green]✓[/green]" if info.available else "[red]✗[/red]"
|
|
table.add_row(dataset_id, info.name, info.category, f"{info.size_gb} GB", available)
|
|
|
|
console.print(table)
|
|
available_count = sum(
|
|
1 for d_id in datasets_to_process if d_id in DATASET_REGISTRY and DATASET_REGISTRY[d_id].available
|
|
)
|
|
console.print(f"\n[bold]Total: {len(datasets_to_process)} datasets ({available_count} available)[/bold]")
|
|
return
|
|
|
|
# Create processor and run
|
|
processor = ParallelDatasetProcessor(
|
|
base_dir=args.base_dir,
|
|
max_workers=args.workers,
|
|
skip_download=args.skip_download,
|
|
skip_convert=args.skip_convert,
|
|
skip_upload=args.skip_upload,
|
|
dry_run=args.dry_run,
|
|
remove_after=args.rm,
|
|
)
|
|
|
|
# Process datasets
|
|
start_time = time.time()
|
|
processor.process_datasets(datasets_to_process)
|
|
elapsed = time.time() - start_time
|
|
|
|
# Print summary
|
|
successful, failed = processor.print_summary()
|
|
|
|
console.print(f"\n[bold]Processing completed in {elapsed:.1f} seconds[/bold]")
|
|
|
|
# Exit with appropriate code
|
|
sys.exit(0 if failed == 0 else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|