claw-code/.guardrails/scripts/batch_operations.py

492 lines
14 KiB
Python

#!/usr/bin/env python3
"""
Batch Operations Module for Team Manager (FUNC-005)
Provides CSV and JSON import/export functionality for bulk team operations.
"""
import csv
import json
import io
from pathlib import Path
from typing import List, Dict, Any, Optional, Tuple
from dataclasses import asdict
from datetime import datetime
def import_csv(manager, csv_path: Path, dry_run: bool = False) -> Dict[str, Any]:
"""
Import role assignments from a CSV file.
Args:
manager: TeamManager instance
csv_path: Path to CSV file
dry_run: If True, validate without making changes
Returns:
Dict with import results:
{
"success": bool,
"imported": int,
"skipped": int,
"errors": List[Dict],
"dry_run": bool
}
"""
result = {
"success": True,
"imported": 0,
"skipped": 0,
"errors": [],
"dry_run": dry_run
}
if not csv_path.exists():
result["success"] = False
result["errors"].append({
"row": 0,
"error": f"File not found: {csv_path}"
})
return result
# Validate file is UTF-8
try:
with open(csv_path, 'r', encoding='utf-8') as f:
content = f.read()
except UnicodeDecodeError as e:
result["success"] = False
result["errors"].append({
"row": 0,
"error": f"File is not valid UTF-8: {e}"
})
return result
# Parse CSV
reader = csv.DictReader(io.StringIO(content))
expected_columns = {'team_id', 'role_name', 'assignee'}
if not reader.fieldname:
result["success"] = False
result["errors"].append({
"row": 0,
"error": "CSV file is empty or has no header row"
})
return result
# Check for required columns
missing = expected_columns - set(reader.fieldname)
if missing:
result["success"] = False
result["errors"].append({
"row": 0,
"error": f"Missing required columns: {', '.join(missing)}"
})
return result
# Process rows
rows = list(reader)
# First pass: validate all rows
validated_rows = []
for i, row in enumerate(rows, start=2): # Start at 2 (1 for header, 1 for 0-index)
validation = _validate_csv_row(manager, row, i)
if validation["valid"]:
validated_rows.append((i, row, validation))
else:
result["errors"].append({
"row": i,
"data": row,
"error": validation["error"]
})
if result["errors"]:
result["success"] = False
if not dry_run:
return result
# Second pass: apply changes (unless dry run)
if not dry_run:
for row_num, row, validation in validated_rows:
try:
team_id = int(row['team_id'])
role_name = row['role_name'].strip()
assignee = row['assignee'].strip()
success = manager.assign_role(team_id, role_name, assignee)
if success:
result["imported"] += 1
else:
result["skipped"] += 1
result["errors"].append({
"row": row_num,
"data": row,
"error": "assign_role returned False"
})
except Exception as e:
result["errors"].append({
"row": row_num,
"data": row,
"error": str(e)
})
return result
def _validate_csv_row(manager, row: Dict[str, str], row_num: int) -> Dict[str, Any]:
"""Validate a single CSV row."""
result = {"valid": True, "error": None}
# Check required fields
if not row.get('team_id', '').strip():
result["valid"] = False
result["error"] = "team_id is required"
return result
if not row.get('role_name', '').strip():
result["valid"] = False
result["error"] = "role_name is required"
return result
if not row.get('assignee', '').strip():
result["valid"] = False
result["error"] = "assignee is required"
return result
# Validate team_id is integer
try:
team_id = int(row['team_id'].strip())
except ValueError:
result["valid"] = False
result["error"] = f"team_id must be an integer, got: {row['team_id']}"
return result
# Check team exists
if team_id not in manager.teams:
result["valid"] = False
result["error"] = f"Team {team_id} does not exist"
return result
# Check role exists in team
team = manager.teams[team_id]
role_name = row['role_name'].strip()
valid_roles = [r.name for r in team.roles]
if role_name not in valid_roles:
result["valid"] = False
result["error"] = f"Role '{role_name}' not found in team {team_id}. Valid roles: {', '.join(valid_roles)}"
return result
return result
def export_csv(manager, csv_path: Path) -> Dict[str, Any]:
"""
Export all role assignments to a CSV file.
Args:
manager: TeamManager instance
csv_path: Path for output CSV file
Returns:
Dict with export results
"""
result = {
"success": True,
"exported": 0,
"file_path": str(csv_path),
"errors": []
}
try:
with open(csv_path, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
# Header
writer.writerow(['team_id', 'team_name', 'role_name', 'assigned_to', 'status'])
# Data rows
for team in sorted(manager.teams.values(), key=lambda t: t.id):
for role in team.roles:
writer.writerow([
team.id,
team.name,
role.name,
role.assigned_to or '',
team.status
])
result["exported"] += 1
except Exception as e:
result["success"] = False
result["errors"].append(str(e))
return result
def import_json(manager, json_path: Path, dry_run: bool = False) -> Dict[str, Any]:
"""
Import role assignments from a JSON file.
Args:
manager: TeamManager instance
json_path: Path to JSON file
dry_run: If True, validate without making changes
Returns:
Dict with import results
"""
result = {
"success": True,
"imported": 0,
"skipped": 0,
"errors": [],
"dry_run": dry_run
}
if not json_path.exists():
result["success"] = False
result["errors"].append(f"File not found: {json_path}")
return result
# Load JSON
try:
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
except json.JSONDecodeError as e:
result["success"] = False
result["errors"].append(f"Invalid JSON: {e}")
return result
except Exception as e:
result["success"] = False
result["errors"].append(f"Error reading file: {e}")
return result
# Handle different JSON formats
if isinstance(data, dict):
if "assignments" in data:
assignments = data["assignments"]
else:
result["success"] = False
result["errors"].append("JSON must contain 'assignments' array or be an array")
return result
elif isinstance(data, list):
assignments = data
else:
result["success"] = False
result["errors"].append("JSON must be an array or object with 'assignments' key")
return result
# Validate all entries first (atomic operation)
validated = []
for i, entry in enumerate(assignments):
validation = _validate_json_entry(manager, entry, i)
if validation["valid"]:
validated.append((i, entry))
else:
result["errors"].append({
"index": i,
"entry": entry,
"error": validation["error"]
})
if result["errors"]:
result["success"] = False
if not dry_run:
return result
# Apply changes (unless dry run)
if not dry_run:
for index, entry in validated:
try:
team_id = int(entry['team_id'])
role_name = entry['role_name']
assignee = entry['assignee']
success = manager.assign_role(team_id, role_name, assignee)
if success:
result["imported"] += 1
else:
result["skipped"] += 1
except Exception as e:
result["errors"].append({
"index": index,
"entry": entry,
"error": str(e)
})
return result
def _validate_json_entry(manager, entry: Dict, index: int) -> Dict[str, Any]:
"""Validate a single JSON entry."""
result = {"valid": True, "error": None}
if not isinstance(entry, dict):
result["valid"] = False
result["error"] = f"Entry must be an object, got {type(entry).__name__}"
return result
# Check required fields
required = ['team_id', 'role_name', 'assignee']
missing = [f for f in required if f not in entry]
if missing:
result["valid"] = False
result["error"] = f"Missing required fields: {', '.join(missing)}"
return result
# Validate team_id
try:
team_id = int(entry['team_id'])
except (ValueError, TypeError):
result["valid"] = False
result["error"] = f"team_id must be an integer, got: {entry['team_id']}"
return result
# Check team exists
if team_id not in manager.teams:
result["valid"] = False
result["error"] = f"Team {team_id} does not exist"
return result
# Check role exists
team = manager.teams[team_id]
role_name = entry['role_name']
valid_roles = [r.name for r in team.roles]
if role_name not in valid_roles:
result["valid"] = False
result["error"] = f"Role '{role_name}' not found in team {team_id}"
return result
return result
def export_json(manager, json_path: Path, pretty: bool = True) -> Dict[str, Any]:
"""
Export full project state to a JSON file.
Args:
manager: TeamManager instance
json_path: Path for output JSON file
pretty: Whether to pretty-print JSON
Returns:
Dict with export results
"""
result = {
"success": True,
"file_path": str(json_path),
"team_count": len(manager.teams),
"errors": []
}
data = {
"project_name": manager.project_name,
"version": getattr(manager, '_data_version', '1.0.0'), # OPS-005: Include version
"exported_at": datetime.now().isoformat(),
"team_count": len(manager.teams),
"teams": []
}
for team in sorted(manager.teams.values(), key=lambda t: t.id):
team_data = {
"id": team.id,
"name": team.name,
"phase": team.phase,
"description": team.description,
"status": team.status,
"started_at": team.started_at,
"completed_at": team.completed_at,
"exit_criteria": team.exit_criteria,
"roles": [
{
"name": role.name,
"responsibility": role.responsibility,
"deliverables": role.deliverables,
"assigned_to": role.assigned_to
}
for role in team.roles
]
}
data["teams"].append(team_data)
try:
with open(json_path, 'w', encoding='utf-8') as f:
indent = 2 if pretty else None
json.dump(data, f, indent=indent, ensure_ascii=False)
except Exception as e:
result["success"] = False
result["errors"].append(str(e))
return result
# For CLI integration
def create_csv_template(csv_path: Path) -> Dict[str, Any]:
"""
Create a CSV template file for bulk assignments.
Args:
csv_path: Path for template file
Returns:
Dict with result
"""
result = {
"success": True,
"file_path": str(csv_path),
"errors": []
}
try:
with open(csv_path, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['team_id', 'role_name', 'assignee'])
# Add example row
writer.writerow([1, 'Business Relationship Manager', 'John Doe'])
writer.writerow([7, 'Technical Lead', 'Jane Smith'])
except Exception as e:
result["success"] = False
result["errors"].append(str(e))
return result
def create_json_template(json_path: Path) -> Dict[str, Any]:
"""
Create a JSON template file for bulk assignments.
Args:
json_path: Path for template file
Returns:
Dict with result
"""
result = {
"success": True,
"file_path": str(json_path),
"errors": []
}
template = {
"description": "Bulk role assignments",
"assignments": [
{
"team_id": 1,
"role_name": "Business Relationship Manager",
"assignee": "John Doe"
},
{
"team_id": 7,
"role_name": "Technical Lead",
"assignee": "Jane Smith"
}
]
}
try:
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(template, f, indent=2)
except Exception as e:
result["success"] = False
result["errors"].append(str(e))
return result