Files
motia-iii/services/exceptions.py
bsiggel 1ffc37b0b7 feat: Add Advoware History and Watcher services for document synchronization
- Implement AdvowareHistoryService for fetching and creating history entries.
- Implement AdvowareWatcherService for file operations including listing, downloading, and uploading with Blake3 hash verification.
- Introduce Blake3 utility functions for hash computation and verification.
- Create document sync cron step to poll Redis for pending Aktennummern and emit sync events.
- Develop document sync event handler to manage 3-way merge synchronization for Akten, including metadata updates and error handling.
2026-03-25 21:24:31 +00:00

223 lines
5.3 KiB
Python

"""
Custom Exception Classes für BitByLaw Integration
Hierarchie:
- IntegrationError (Base)
- APIError
- AdvowareAPIError
- AdvowareAuthError
- AdvowareTimeoutError
- EspoCRMAPIError
- EspoCRMAuthError
- EspoCRMTimeoutError
- SyncError
- LockAcquisitionError
- ValidationError
- ConflictError
- RetryableError
- NonRetryableError
"""
from typing import Optional, Dict, Any
class IntegrationError(Exception):
"""Base exception for all integration errors"""
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
super().__init__(message)
self.message = message
self.details = details or {}
# ========== API Errors ==========
class APIError(IntegrationError):
"""Base class for all API-related errors"""
def __init__(
self,
message: str,
status_code: Optional[int] = None,
response_body: Optional[str] = None,
details: Optional[Dict[str, Any]] = None
):
super().__init__(message, details)
self.status_code = status_code
self.response_body = response_body
class AdvowareAPIError(APIError):
"""Advoware API error"""
pass
class AdvowareAuthError(AdvowareAPIError):
"""Advoware authentication error"""
pass
class AdvowareTimeoutError(AdvowareAPIError):
"""Advoware API timeout"""
pass
class EspoCRMAPIError(APIError):
"""EspoCRM API error"""
pass
class EspoCRMAuthError(EspoCRMAPIError):
"""EspoCRM authentication error"""
pass
class EspoCRMTimeoutError(EspoCRMAPIError):
"""EspoCRM API timeout"""
pass
class ExternalAPIError(APIError):
"""Generic external API error (Watcher, etc.)"""
pass
# ========== Sync Errors ==========
class SyncError(IntegrationError):
"""Base class for synchronization errors"""
pass
class LockAcquisitionError(SyncError):
"""Failed to acquire distributed lock"""
def __init__(self, entity_id: str, lock_key: str, message: Optional[str] = None):
super().__init__(
message or f"Could not acquire lock for entity {entity_id}",
details={"entity_id": entity_id, "lock_key": lock_key}
)
self.entity_id = entity_id
self.lock_key = lock_key
class ValidationError(SyncError):
"""Data validation error"""
def __init__(self, message: str, field: Optional[str] = None, value: Any = None):
super().__init__(
message,
details={"field": field, "value": value}
)
self.field = field
self.value = value
class ConflictError(SyncError):
"""Data conflict during synchronization"""
def __init__(
self,
message: str,
entity_id: str,
source_system: Optional[str] = None,
target_system: Optional[str] = None
):
super().__init__(
message,
details={
"entity_id": entity_id,
"source_system": source_system,
"target_system": target_system
}
)
self.entity_id = entity_id
# ========== Retry Classification ==========
class RetryableError(IntegrationError):
"""Error that should trigger retry logic"""
def __init__(
self,
message: str,
retry_after_seconds: Optional[int] = None,
details: Optional[Dict[str, Any]] = None
):
super().__init__(message, details)
self.retry_after_seconds = retry_after_seconds
class NonRetryableError(IntegrationError):
"""Error that should NOT trigger retry (e.g., validation errors)"""
pass
# ========== Redis Errors ==========
class RedisError(IntegrationError):
"""Redis connection or operation error"""
def __init__(self, message: str, operation: Optional[str] = None):
super().__init__(message, details={"operation": operation})
self.operation = operation
class RedisConnectionError(RedisError):
"""Redis connection failed"""
pass
# ========== Helper Functions ==========
def is_retryable(error: Exception) -> bool:
"""
Determine if an error should trigger retry logic.
Args:
error: Exception to check
Returns:
True if error is retryable
"""
if isinstance(error, NonRetryableError):
return False
if isinstance(error, RetryableError):
return True
if isinstance(error, (AdvowareTimeoutError, EspoCRMTimeoutError)):
return True
if isinstance(error, ValidationError):
return False
# Default: assume retryable for API errors
if isinstance(error, APIError):
return True
return False
def get_retry_delay(error: Exception, attempt: int) -> int:
"""
Calculate retry delay based on error type and attempt number.
Args:
error: The error that occurred
attempt: Current retry attempt (0-indexed)
Returns:
Delay in seconds
"""
if isinstance(error, RetryableError) and error.retry_after_seconds:
return error.retry_after_seconds
# Exponential backoff: [1, 5, 15, 60, 240] minutes
backoff_minutes = [1, 5, 15, 60, 240]
if attempt < len(backoff_minutes):
return backoff_minutes[attempt] * 60
return backoff_minutes[-1] * 60