Files
AI/참고/ontocast-main/ontocast/cli/batch_process.py

305 lines
9.0 KiB
Python
Raw Normal View History

2026-05-12 19:40:31 +09:00
"""Batch processing client for OntoCast.
This module provides a command-line client for batch processing multiple files
through the OntoCast API server. It supports async processing with configurable
concurrency limits.
The client supports:
- Recursive directory scanning
- File pattern matching (e.g., by extension)
- Async processing with concurrency control
- Progress tracking and error reporting
- JSON and PDF file types
Example:
# Process all JSON files in a directory (max 3 concurrent)
python batch_process.py --url http://localhost:8999 --path ./data --pattern "*.json" --max-concurrent 3
# Process all PDF files recursively
python batch_process.py --url http://localhost:8999 --path ./documents --pattern "*.pdf" --recursive
"""
import asyncio
import json
import pathlib
from typing import Optional
import click
import httpx
async def process_file(
client: httpx.AsyncClient,
url: str,
file_path: pathlib.Path,
semaphore: asyncio.Semaphore,
results: dict,
dataset: Optional[str] = None,
) -> None:
"""Process a single file by sending it to the OntoCast API.
Args:
client: httpx async client
url: API endpoint URL
file_path: Path to the file to process
semaphore: Semaphore to limit concurrent requests
results: Dictionary to store results (success/error counts)
dataset: Optional dataset name for triple store storage
"""
async with semaphore:
try:
file_ext = file_path.suffix.lower()
mime_type = "application/pdf" if file_ext == ".pdf" else "application/json"
with open(file_path, "rb") as f:
file_content = f.read()
files = {"file": (file_path.name, file_content, mime_type)}
# Add dataset as query parameter if provided
params = {}
if dataset:
params["dataset"] = dataset
response = await client.post(url, files=files, params=params)
status = response.status_code
if status == 200:
results["success"] += 1
click.echo(f"{file_path.name} - Success")
else:
error_text = (
response.text[:200] if response.text else "No error message"
)
results["errors"] += 1
results["error_details"][file_path.name] = {
"status": status,
"error": error_text,
}
click.echo(f"{file_path.name} - Error {status}")
except Exception as e:
results["errors"] += 1
results["error_details"][file_path.name] = {
"status": None,
"error": str(e)[:200],
}
click.echo(f"{file_path.name} - Exception: {str(e)[:100]}")
async def process_files_async(
url: str,
file_paths: list[pathlib.Path],
max_concurrent: int,
dataset: Optional[str] = None,
) -> dict:
"""Process multiple files asynchronously with concurrency control.
Args:
url: API endpoint URL
file_paths: List of file paths to process
max_concurrent: Maximum number of concurrent requests
dataset: Optional dataset name for triple store storage
Returns:
Dictionary with processing results (success count, error count, details)
"""
results = {
"success": 0,
"errors": 0,
"error_details": {},
"total": len(file_paths),
}
if not file_paths:
click.echo("No files found to process.")
return results
semaphore = asyncio.Semaphore(max_concurrent)
click.echo(
f"Processing {len(file_paths)} file(s) with max {max_concurrent} concurrent requests..."
)
if dataset:
click.echo(f"Using dataset: {dataset}")
async with httpx.AsyncClient(timeout=300.0) as client:
tasks = [
process_file(client, url, file_path, semaphore, results, dataset)
for file_path in file_paths
]
await asyncio.gather(*tasks)
return results
def find_files(
path: pathlib.Path, pattern: Optional[str], recursive: bool
) -> list[pathlib.Path]:
"""Find files matching the given pattern.
Args:
path: Base path to search
pattern: Glob pattern (e.g., "*.json", "*.pdf") or None for all files
recursive: Whether to search recursively
Returns:
List of matching file paths
"""
if not path.exists():
raise click.BadParameter(f"Path does not exist: {path}", param_hint="--path")
if path.is_file():
return [path]
if pattern:
if recursive:
files = list(path.rglob(pattern))
else:
files = list(path.glob(pattern))
else:
if recursive:
files = [f for f in path.rglob("*") if f.is_file()]
else:
files = [f for f in path.glob("*") if f.is_file()]
# Filter to only JSON and PDF files
supported_extensions = {".json", ".pdf"}
files = [f for f in files if f.suffix.lower() in supported_extensions]
return sorted(files)
@click.command()
@click.option(
"--url",
required=True,
help="Base URL for the server (e.g. http://localhost:8999)",
)
@click.option(
"--path",
type=click.Path(path_type=pathlib.Path, exists=True),
required=True,
help="Path to file or directory to process",
)
@click.option(
"--pattern",
type=str,
default=None,
help="Glob pattern to match files (e.g., '*.json', '*.pdf'). If not provided, processes all supported files.",
)
@click.option(
"--recursive",
is_flag=True,
default=True,
help="Search for files recursively in subdirectories",
)
@click.option(
"--max-concurrent",
type=int,
default=3,
help="Maximum number of concurrent requests (default: 3)",
)
@click.option(
"--output",
type=click.Path(path_type=pathlib.Path),
default=None,
help="Optional path to save results summary as JSON",
)
@click.option(
"--dataset",
type=str,
default=None,
help="Dataset name for triple store storage (Fuseki only). If provided, all files will be processed into this dataset.",
)
def main(
url: str,
path: pathlib.Path,
pattern: Optional[str],
recursive: bool,
max_concurrent: int,
output: Optional[pathlib.Path],
dataset: Optional[str],
):
"""Batch process files through the OntoCast API server.
This command finds files matching the given pattern (or all supported files)
and sends them to the OntoCast API server for processing. Files are processed
asynchronously with a configurable concurrency limit.
Supported file types: .json, .pdf
Examples:
# Process all JSON files in a directory
batch_process.py --url http://localhost:8999 --path ./data --pattern "*.json"
# Process all PDFs recursively with 5 concurrent requests
batch_process.py --url http://localhost:8999 --path ./documents --pattern "*.pdf" --recursive --max-concurrent 5
# Process files into a specific dataset
batch_process.py --url http://localhost:8999 --path ./data --pattern "*.json" --dataset my_dataset
# Process a single file
batch_process.py --url http://localhost:8999 --path ./document.pdf
"""
if not url.endswith("/process"):
url = f"{url.rstrip('/')}/process"
if max_concurrent < 1:
raise click.BadParameter(
"max-concurrent must be at least 1", param_hint="--max-concurrent"
)
# Expand user path
path = path.expanduser()
# Find files
try:
file_paths = find_files(path, pattern, recursive)
except Exception as e:
raise click.ClickException(f"Error finding files: {e}")
if not file_paths:
click.echo(f"No files found matching pattern '{pattern or '*.*'}' in {path}")
return
click.echo(f"Found {len(file_paths)} file(s) to process")
if pattern:
click.echo(f"Pattern: {pattern}")
if recursive:
click.echo("Recursive search: enabled")
if dataset:
click.echo(f"Dataset: {dataset}")
click.echo(f"Max concurrent requests: {max_concurrent}")
click.echo("")
# Process files
results = asyncio.run(process_files_async(url, file_paths, max_concurrent, dataset))
# Print summary
click.echo("")
click.echo("=" * 60)
click.echo("Processing Summary")
click.echo("=" * 60)
click.echo(f"Total files: {results['total']}")
click.echo(f"Successful: {results['success']}")
click.echo(f"Errors: {results['errors']}")
if results["error_details"]:
click.echo("")
click.echo("Error Details:")
for filename, details in results["error_details"].items():
click.echo(f" {filename}: {details['error']}")
# Save results if output path provided
if output:
output = output.expanduser()
output.parent.mkdir(parents=True, exist_ok=True)
with open(output, "w") as f:
json.dump(results, f, indent=2)
click.echo("")
click.echo(f"Results saved to: {output}")
if __name__ == "__main__":
main()