- Added `config.py` for centralized configuration management including Sync, API, Advoware, EspoCRM, Redis, Logging, Calendar Sync, and Feature Flags. - Created `exceptions.py` with a hierarchy of custom exceptions for integration errors, API errors, sync errors, and Redis errors. - Developed `logging_utils.py` for a unified logging wrapper supporting structured logging and performance tracking. - Defined Pydantic models in `models.py` for data validation of Advoware and EspoCRM entities, including sync operation models. - Introduced `redis_client.py` for a centralized Redis client factory with connection pooling, automatic reconnection, and health checks.
218 lines
5.2 KiB
Python
218 lines
5.2 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
|
|
|
|
|
|
# ========== 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
|