Implement central configuration, custom exceptions, logging utilities, Pydantic models, and Redis client for BitByLaw integration
- 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.
This commit is contained in:
217
services/exceptions.py
Normal file
217
services/exceptions.py
Normal file
@@ -0,0 +1,217 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user