Skip to content

Bulk Operations API

The Bulk Operations API provides a REST-friendly foundation for performing bulk operations on Gira tickets. It's designed to enable TUI and web interface development with consistent patterns, structured error handling, and async operation support.

Overview

The API provides: - Standardized request/response formats across all bulk operations - Pre-flight validation with detailed error messages - Operation status tracking with unique operation IDs - Structured error responses with field-level details - JSON schema support for request/response validation - Async operation support for long-running operations - Transaction support for atomic operations

Core Components

BulkOperationManager

Base class for all bulk operation managers. Provides: - Request validation - Operation tracking - Error handling - Response formatting - Async/sync execution modes

ValidationEngine

Validates requests before execution: - Field validation (enums, required fields) - Ticket existence checks - Circular dependency detection - Custom validation rules

OperationTracker

Tracks operation status and progress: - Real-time progress updates - Operation history - Cancellation support - TTL-based cleanup

ResponseFormatter

Ensures consistent JSON responses: - Standard response structure - Error formatting - CLI-friendly output - Metadata inclusion

ErrorHandler

Provides structured error handling: - Error categorization - Field-level error details - Error collection and limiting - Custom error mappings

Request Format

All bulk operations follow this standard request format:

{
  "operation_id": "uuid",  // Optional, generated if not provided
  "operation_type": "bulk_update",
  "items": [
    {
      "id": "GCM-1",
      // Operation-specific fields
    }
  ],
  "options": {
    "dry_run": false,
    "validate_only": false,
    "all_or_nothing": false,
    "use_transaction": false,
    "skip_invalid": false,
    "validation_level": "basic"  // none, basic, strict
  }
}

Response Format

Standard response format for all operations:

{
  "operation_id": "uuid",
  "operation_type": "bulk_update",
  "status": "completed",  // pending, validating, in_progress, completed, failed, partially_completed
  "progress": {
    "completed": 5,
    "total": 10,
    "percentage": 50.0,
    "current_item": "GCM-3",
    "estimated_time_remaining": 30  // seconds
  },
  "results": {
    "successful": [
      {
        "item_id": "GCM-1",
        "status": "success",
        "changes": {
          "status": {
            "old": "todo",
            "new": "in_progress"
          }
        }
      }
    ],
    "failed": [
      {
        "item_id": "GCM-2",
        "status": "failed",
        "error": {
          "code": "TICKET_NOT_FOUND",
          "message": "Ticket GCM-2 not found",
          "details": {"item_id": "GCM-2"}
        }
      }
    ],
    "skipped": []
  },
  "errors": [],  // Global errors
  "metadata": {
    "created_at": "2025-01-29T10:00:00Z",
    "updated_at": "2025-01-29T10:00:30Z",
    "duration_ms": 30000
  }
}

Operation Types

Bulk Update

Update multiple tickets with the same or different changes.

from gira.api.bulk.update import create_bulk_update_api
from gira.api.bulk.schemas import BulkOperationRequest, BulkUpdateItem

# Create API manager
api = create_bulk_update_api()

# Create request
request = BulkOperationRequest(
    operation_type="bulk_update",
    items=[
        BulkUpdateItem(id="GCM-1", status="in_progress"),
        BulkUpdateItem(id="GCM-2", status="done", assignee="john")
    ],
    options={"use_transaction": True}
)

# Execute synchronously
response = api.execute(request)

# Or execute asynchronously
operation_id = api.execute(request, async_mode=True)

Bulk Add Dependencies

Add dependencies to multiple tickets.

from gira.api.bulk.dependencies import create_bulk_add_deps_api
from gira.api.bulk.schemas import BulkOperationRequest, BulkDependencyItem

api = create_bulk_add_deps_api()

request = BulkOperationRequest(
    operation_type="bulk_add_deps",
    items=[
        BulkDependencyItem(id="GCM-1", dependency_id="GCM-3"),
        BulkDependencyItem(id="GCM-2", dependencies=["GCM-3", "GCM-4"])
    ]
)

response = api.execute(request)

Bulk Remove Dependencies

Remove dependencies from multiple tickets.

from gira.api.bulk.dependencies import create_bulk_remove_deps_api

api = create_bulk_remove_deps_api()

request = BulkOperationRequest(
    operation_type="bulk_remove_deps",
    items=[
        {"id": "GCM-1", "dependency_id": "GCM-3"},
        {"id": "GCM-2", "remove_all": true}
    ]
)

response = api.execute(request)

Bulk Clear Dependencies

Clear all dependencies from tickets.

from gira.api.bulk.dependencies import create_bulk_clear_deps_api

api = create_bulk_clear_deps_api()

request = BulkOperationRequest(
    operation_type="bulk_clear_deps",
    items=[
        {"id": "GCM-1"},
        {"id": "GCM-2"}
    ]
)

response = api.execute(request)

Validation

Validation Levels

  • none: No validation, execute immediately
  • basic: Validate required fields and basic constraints
  • strict: Full validation including field values and relationships

Pre-flight Validation

# Validate without executing
request.options.validate_only = True
response = api.execute(request)

# Check validation result
if response.status == "failed":
    for error in response.errors:
        print(f"{error.code}: {error.message}")

Custom Validation Rules

from gira.api.bulk.validation import ValidationRule, ValidationEngine

class CustomRule(ValidationRule):
    def validate(self, value, context=None):
        if not self.is_valid(value):
            return OperationError(
                code="CUSTOM_ERROR",
                message="Custom validation failed"
            )
        return None

# Add to validation engine
validator = ValidationEngine()
validator.custom_rules.append(CustomRule())

Error Handling

Error Codes

  • VALIDATION_ERROR: Request validation failed
  • TICKET_NOT_FOUND: Referenced ticket doesn't exist
  • CIRCULAR_DEPENDENCY: Would create circular dependency
  • OPERATION_NOT_ALLOWED: Operation not permitted
  • TRANSACTION_ERROR: Transaction failed
  • INTERNAL_ERROR: Unexpected error

Error Response

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid status value",
    "field": "status",
    "details": {
      "allowed_values": ["todo", "in_progress", "done"],
      "provided_value": "invalid"
    },
    "error_id": "uuid"
  }
}

Async Operations

Starting Async Operation

# Start async operation
operation_id = api.execute(request, async_mode=True)
print(f"Operation started: {operation_id}")

# Check status
status_response = api.get_status(operation_id)
print(f"Progress: {status_response.progress.percentage}%")

# Cancel if needed
cancelled = api.cancel_operation(operation_id)

Progress Callbacks

# Configure webhook for progress updates
request.options.progress_callback = "https://example.com/webhook"

# The API will POST progress updates to the webhook

Transaction Support

Atomic Operations

# Enable transactions for all-or-nothing execution
request.options.use_transaction = True
request.options.all_or_nothing = True

response = api.execute(request)
# If any item fails, all changes are rolled back

Transaction Metadata

{
  "metadata": {
    "transaction_id": "uuid",
    "rolled_back": false
  }
}

Integration Examples

Web API Integration

from flask import Flask, request, jsonify
from gira.api.bulk import BulkOperationManager
from gira.api.bulk.update import create_bulk_update_api

app = Flask(__name__)
bulk_update_api = create_bulk_update_api()

@app.route('/api/bulk/update', methods=['POST'])
def bulk_update():
    try:
        # Parse request
        bulk_request = BulkOperationRequest(**request.json)

        # Execute operation
        response = bulk_update_api.execute(bulk_request)

        # Return response
        return jsonify(response.model_dump()), 200

    except ValidationError as e:
        return jsonify(e.to_dict()), 400
    except Exception as e:
        return jsonify({"error": str(e)}), 500

TUI Integration

from textual.app import App
from gira.api.bulk import BulkOperationManager

class BulkOperationTUI(App):
    def __init__(self):
        super().__init__()
        self.bulk_api = create_bulk_update_api()
        self.current_operation = None

    async def perform_bulk_update(self, items):
        # Start async operation
        request = BulkOperationRequest(
            operation_type="bulk_update",
            items=items
        )

        self.current_operation = self.bulk_api.execute(
            request, 
            async_mode=True
        )

        # Poll for updates
        while True:
            status = self.bulk_api.get_status(self.current_operation)
            self.update_progress_bar(status.progress)

            if status.is_complete:
                self.show_results(status)
                break

            await asyncio.sleep(0.5)

CLI Integration

import typer
from gira.api.bulk.schemas import BulkOperationRequest
from gira.api.bulk.update import create_bulk_update_api

app = typer.Typer()

@app.command()
def bulk_update(
    items_file: Path,
    dry_run: bool = False,
    transaction: bool = False
):
    """Execute bulk update from JSON file."""
    # Load items
    with open(items_file) as f:
        items = json.load(f)

    # Create API and request
    api = create_bulk_update_api()
    request = BulkOperationRequest(
        operation_type="bulk_update",
        items=items,
        options={
            "dry_run": dry_run,
            "use_transaction": transaction
        }
    )

    # Execute and display results
    response = api.execute(request)

    # Use CLI formatter
    output = api.response_formatter.format_cli_output(response)
    typer.echo(output)

Best Practices

  1. Use transactions for critical operations

    request.options.use_transaction = True
    request.options.all_or_nothing = True
    

  2. Implement progress tracking for large operations

    # For operations with many items
    if len(request.items) > 100:
        operation_id = api.execute(request, async_mode=True)
    

  3. Handle partial failures gracefully

    if response.status == "partially_completed":
        # Process successful items
        for item in response.results["successful"]:
            log_success(item)
    
        # Retry failed items
        retry_items = [
            {"id": item.item_id} 
            for item in response.results["failed"]
        ]
    

  4. Validate before executing

    # Always validate first for user-initiated operations
    request.options.validate_only = True
    validation_response = api.execute(request)
    
    if validation_response.status == "completed":
        request.options.validate_only = False
        response = api.execute(request)
    

  5. Use appropriate validation levels

    # Strict for user input
    request.options.validation_level = "strict"
    
    # Basic for system operations
    request.options.validation_level = "basic"
    
    # None for trusted batch operations
    request.options.validation_level = "none"
    

Performance Considerations

  • Batch Size: Optimal batch size is 100-500 items
  • Async Threshold: Use async mode for >50 items
  • Transaction Overhead: ~10% performance impact
  • Validation Cost: Strict validation adds ~20% overhead

Future Enhancements

  • WebSocket support for real-time progress
  • Batch scheduling and queuing
  • Distributed execution across workers
  • Result caching and deduplication
  • Audit logging and history