Initial commit
Dieser Commit ist enthalten in:
17
domain/value_objects/__init__.py
Normale Datei
17
domain/value_objects/__init__.py
Normale Datei
@ -0,0 +1,17 @@
|
||||
"""
|
||||
Domain Value Objects - Unveränderliche Wertobjekte ohne Identität
|
||||
"""
|
||||
|
||||
from .action_timing import ActionTiming, ActionType
|
||||
from .error_summary import ErrorSummary
|
||||
from .report import Report, ReportType
|
||||
from .login_credentials import LoginCredentials
|
||||
|
||||
__all__ = [
|
||||
'ActionTiming',
|
||||
'ActionType',
|
||||
'ErrorSummary',
|
||||
'Report',
|
||||
'ReportType',
|
||||
'LoginCredentials'
|
||||
]
|
||||
120
domain/value_objects/account_creation_params.py
Normale Datei
120
domain/value_objects/account_creation_params.py
Normale Datei
@ -0,0 +1,120 @@
|
||||
"""
|
||||
Typsichere Parameter für Account-Erstellung
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Dict, Any, List
|
||||
from domain.entities.browser_fingerprint import BrowserFingerprint
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationResult:
|
||||
"""Ergebnis einer Validierung"""
|
||||
is_valid: bool
|
||||
errors: List[str]
|
||||
|
||||
def get_error_message(self) -> str:
|
||||
"""Gibt eine formatierte Fehlermeldung zurück"""
|
||||
if self.is_valid:
|
||||
return ""
|
||||
return "\n".join(self.errors)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AccountCreationParams:
|
||||
"""Typsichere Parameter für Account-Erstellung"""
|
||||
full_name: str
|
||||
age: int
|
||||
registration_method: str = "email"
|
||||
show_browser: bool = False
|
||||
proxy_type: Optional[str] = None
|
||||
fingerprint: Optional[BrowserFingerprint] = None
|
||||
email_domain: str = "z5m7q9dk3ah2v1plx6ju.com"
|
||||
username: Optional[str] = None
|
||||
password: Optional[str] = None
|
||||
phone_number: Optional[str] = None
|
||||
imap_handler: Optional[Any] = None
|
||||
phone_service: Optional[Any] = None
|
||||
additional_params: Dict[str, Any] = None
|
||||
|
||||
# Platform-spezifische Konstanten
|
||||
MIN_AGE: int = 13
|
||||
MAX_AGE: int = 99
|
||||
|
||||
def __post_init__(self):
|
||||
if self.additional_params is None:
|
||||
self.additional_params = {}
|
||||
|
||||
def validate(self) -> ValidationResult:
|
||||
"""Validiert alle Parameter"""
|
||||
errors = []
|
||||
|
||||
# Name validieren
|
||||
if not self.full_name or len(self.full_name.strip()) < 2:
|
||||
errors.append("Der Name muss mindestens 2 Zeichen lang sein")
|
||||
|
||||
# Alter validieren
|
||||
if self.age < self.MIN_AGE:
|
||||
errors.append(f"Das Alter muss mindestens {self.MIN_AGE} sein")
|
||||
elif self.age > self.MAX_AGE:
|
||||
errors.append(f"Das Alter darf maximal {self.MAX_AGE} sein")
|
||||
|
||||
# Registrierungsmethode validieren
|
||||
if self.registration_method not in ["email", "phone"]:
|
||||
errors.append("Ungültige Registrierungsmethode")
|
||||
|
||||
# Telefonnummer bei Phone-Registrierung
|
||||
if self.registration_method == "phone" and not self.phone_number:
|
||||
errors.append("Telefonnummer erforderlich für Phone-Registrierung")
|
||||
|
||||
# E-Mail-Domain validieren
|
||||
if self.registration_method == "email" and not self.email_domain:
|
||||
errors.append("E-Mail-Domain erforderlich für Email-Registrierung")
|
||||
|
||||
return ValidationResult(is_valid=len(errors)==0, errors=errors)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Konvertiert zu Dictionary für Kompatibilität"""
|
||||
result = {
|
||||
"full_name": self.full_name,
|
||||
"age": self.age,
|
||||
"registration_method": self.registration_method,
|
||||
"show_browser": self.show_browser,
|
||||
"proxy_type": self.proxy_type,
|
||||
"fingerprint": self.fingerprint,
|
||||
"email_domain": self.email_domain,
|
||||
"username": self.username,
|
||||
"password": self.password,
|
||||
"phone_number": self.phone_number,
|
||||
"imap_handler": self.imap_handler,
|
||||
"phone_service": self.phone_service
|
||||
}
|
||||
|
||||
# Additional params hinzufügen
|
||||
result.update(self.additional_params)
|
||||
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'AccountCreationParams':
|
||||
"""Erstellt aus Dictionary"""
|
||||
# Bekannte Parameter extrahieren
|
||||
known_params = {
|
||||
"full_name": data.get("full_name", ""),
|
||||
"age": data.get("age", 18),
|
||||
"registration_method": data.get("registration_method", "email"),
|
||||
"show_browser": data.get("show_browser", False),
|
||||
"proxy_type": data.get("proxy_type"),
|
||||
"fingerprint": data.get("fingerprint"),
|
||||
"email_domain": data.get("email_domain", "z5m7q9dk3ah2v1plx6ju.com"),
|
||||
"username": data.get("username"),
|
||||
"password": data.get("password"),
|
||||
"phone_number": data.get("phone_number"),
|
||||
"imap_handler": data.get("imap_handler"),
|
||||
"phone_service": data.get("phone_service")
|
||||
}
|
||||
|
||||
# Alle anderen Parameter als additional_params
|
||||
additional = {k: v for k, v in data.items() if k not in known_params}
|
||||
known_params["additional_params"] = additional
|
||||
|
||||
return cls(**known_params)
|
||||
102
domain/value_objects/action_timing.py
Normale Datei
102
domain/value_objects/action_timing.py
Normale Datei
@ -0,0 +1,102 @@
|
||||
"""
|
||||
Action Timing Value Object - Repräsentiert Timing-Informationen einer Aktion
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
|
||||
class ActionType(Enum):
|
||||
"""Typen von Aktionen die getimed werden"""
|
||||
# Navigation
|
||||
PAGE_LOAD = "page_load"
|
||||
PAGE_NAVIGATION = "page_navigation"
|
||||
|
||||
# Form-Interaktionen
|
||||
FORM_FILL = "form_fill"
|
||||
BUTTON_CLICK = "button_click"
|
||||
INPUT_TYPE = "input_type"
|
||||
DROPDOWN_SELECT = "dropdown_select"
|
||||
CHECKBOX_TOGGLE = "checkbox_toggle"
|
||||
|
||||
# Verifizierung
|
||||
EMAIL_CHECK = "email_check"
|
||||
SMS_CHECK = "sms_check"
|
||||
CAPTCHA_SOLVE = "captcha_solve"
|
||||
|
||||
# Account-Aktionen
|
||||
REGISTRATION_START = "registration_start"
|
||||
REGISTRATION_COMPLETE = "registration_complete"
|
||||
LOGIN_ATTEMPT = "login_attempt"
|
||||
LOGOUT = "logout"
|
||||
|
||||
# Daten-Operationen
|
||||
SCREENSHOT = "screenshot"
|
||||
DATA_SAVE = "data_save"
|
||||
SESSION_SAVE = "session_save"
|
||||
|
||||
# Netzwerk
|
||||
API_REQUEST = "api_request"
|
||||
FILE_UPLOAD = "file_upload"
|
||||
FILE_DOWNLOAD = "file_download"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ActionTiming:
|
||||
"""
|
||||
Repräsentiert Timing-Informationen einer Aktion.
|
||||
Frozen dataclass macht es unveränderlich (Value Object).
|
||||
"""
|
||||
|
||||
action_type: ActionType
|
||||
timestamp: datetime
|
||||
duration: float # in Sekunden
|
||||
success: bool
|
||||
|
||||
# Optionale Metadaten
|
||||
url: Optional[str] = None
|
||||
element_selector: Optional[str] = None
|
||||
error_message: Optional[str] = None
|
||||
retry_count: int = 0
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
|
||||
def __post_init__(self):
|
||||
"""Validierung der Timing-Daten"""
|
||||
if self.duration < 0:
|
||||
raise ValueError("Duration kann nicht negativ sein")
|
||||
if self.retry_count < 0:
|
||||
raise ValueError("Retry count kann nicht negativ sein")
|
||||
|
||||
@property
|
||||
def duration_ms(self) -> float:
|
||||
"""Gibt die Dauer in Millisekunden zurück"""
|
||||
return self.duration * 1000
|
||||
|
||||
@property
|
||||
def is_slow(self) -> bool:
|
||||
"""Prüft ob die Aktion langsam war (> 3 Sekunden)"""
|
||||
return self.duration > 3.0
|
||||
|
||||
@property
|
||||
def is_very_slow(self) -> bool:
|
||||
"""Prüft ob die Aktion sehr langsam war (> 10 Sekunden)"""
|
||||
return self.duration > 10.0
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Konvertiert zu Dictionary für Serialisierung"""
|
||||
return {
|
||||
'action_type': self.action_type.value,
|
||||
'timestamp': self.timestamp.isoformat(),
|
||||
'duration': self.duration,
|
||||
'duration_ms': self.duration_ms,
|
||||
'success': self.success,
|
||||
'url': self.url,
|
||||
'element_selector': self.element_selector,
|
||||
'error_message': self.error_message,
|
||||
'retry_count': self.retry_count,
|
||||
'metadata': self.metadata or {},
|
||||
'is_slow': self.is_slow,
|
||||
'is_very_slow': self.is_very_slow
|
||||
}
|
||||
29
domain/value_objects/browser_protection_style.py
Normale Datei
29
domain/value_objects/browser_protection_style.py
Normale Datei
@ -0,0 +1,29 @@
|
||||
"""Browser protection style value object."""
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class ProtectionLevel(Enum):
|
||||
"""Defines the level of browser protection during automation."""
|
||||
NONE = "none" # No protection
|
||||
LIGHT = "light" # Visual indicator only
|
||||
MEDIUM = "medium" # Transparent overlay with interaction blocking
|
||||
STRONG = "strong" # Full blocking with opaque overlay
|
||||
|
||||
|
||||
@dataclass
|
||||
class BrowserProtectionStyle:
|
||||
"""Configuration for browser protection during automation."""
|
||||
level: ProtectionLevel = ProtectionLevel.MEDIUM
|
||||
show_border: bool = True # Show animated border
|
||||
show_badge: bool = True # Show info badge
|
||||
blur_effect: bool = False # Apply blur to page content
|
||||
opacity: float = 0.1 # Overlay opacity (0.0 - 1.0)
|
||||
badge_text: str = "🔒 Automatisierung läuft - Nicht eingreifen"
|
||||
badge_position: str = "top-right" # top-left, top-right, bottom-left, bottom-right
|
||||
border_color: str = "rgba(255, 0, 0, 0.5)"
|
||||
overlay_color: str = "rgba(0, 0, 0, {opacity})" # {opacity} will be replaced
|
||||
|
||||
def get_overlay_color(self) -> str:
|
||||
"""Get the overlay color with the configured opacity."""
|
||||
return self.overlay_color.format(opacity=self.opacity)
|
||||
98
domain/value_objects/error_summary.py
Normale Datei
98
domain/value_objects/error_summary.py
Normale Datei
@ -0,0 +1,98 @@
|
||||
"""
|
||||
Error Summary Value Object - Zusammenfassung von Fehlerinformationen
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ErrorSummary:
|
||||
"""
|
||||
Zusammenfassung von Fehlerinformationen für Berichte und Analysen.
|
||||
Frozen dataclass macht es unveränderlich (Value Object).
|
||||
"""
|
||||
|
||||
error_type: str
|
||||
error_count: int
|
||||
first_occurrence: datetime
|
||||
last_occurrence: datetime
|
||||
affected_sessions: List[str]
|
||||
affected_accounts: List[str]
|
||||
|
||||
# Statistiken
|
||||
avg_recovery_time: float # in Sekunden
|
||||
recovery_success_rate: float # 0.0 - 1.0
|
||||
|
||||
# Häufigste Kontexte
|
||||
most_common_urls: List[str]
|
||||
most_common_actions: List[str]
|
||||
most_common_steps: List[str]
|
||||
|
||||
# Impact
|
||||
total_user_impact: int
|
||||
total_system_impact: int
|
||||
data_loss_incidents: int
|
||||
|
||||
def __post_init__(self):
|
||||
"""Validierung der Summary-Daten"""
|
||||
if self.error_count < 0:
|
||||
raise ValueError("Error count kann nicht negativ sein")
|
||||
if not 0.0 <= self.recovery_success_rate <= 1.0:
|
||||
raise ValueError("Recovery success rate muss zwischen 0.0 und 1.0 liegen")
|
||||
if self.first_occurrence > self.last_occurrence:
|
||||
raise ValueError("First occurrence kann nicht nach last occurrence liegen")
|
||||
|
||||
@property
|
||||
def duration(self) -> float:
|
||||
"""Zeitspanne zwischen erstem und letztem Auftreten in Stunden"""
|
||||
delta = self.last_occurrence - self.first_occurrence
|
||||
return delta.total_seconds() / 3600
|
||||
|
||||
@property
|
||||
def frequency(self) -> float:
|
||||
"""Fehler pro Stunde"""
|
||||
if self.duration > 0:
|
||||
return self.error_count / self.duration
|
||||
return self.error_count
|
||||
|
||||
@property
|
||||
def severity_score(self) -> float:
|
||||
"""
|
||||
Berechnet einen Schweregrad-Score basierend auf:
|
||||
- Häufigkeit
|
||||
- Impact
|
||||
- Wiederherstellungsrate
|
||||
"""
|
||||
frequency_factor = min(self.frequency / 10, 1.0) # Normalisiert auf 0-1
|
||||
impact_factor = min((self.total_user_impact + self.total_system_impact) / 100, 1.0)
|
||||
recovery_factor = 1.0 - self.recovery_success_rate
|
||||
data_loss_factor = min(self.data_loss_incidents / 10, 1.0)
|
||||
|
||||
return (frequency_factor * 0.3 +
|
||||
impact_factor * 0.3 +
|
||||
recovery_factor * 0.2 +
|
||||
data_loss_factor * 0.2)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Konvertiert zu Dictionary für Serialisierung"""
|
||||
return {
|
||||
'error_type': self.error_type,
|
||||
'error_count': self.error_count,
|
||||
'first_occurrence': self.first_occurrence.isoformat(),
|
||||
'last_occurrence': self.last_occurrence.isoformat(),
|
||||
'duration_hours': self.duration,
|
||||
'frequency_per_hour': self.frequency,
|
||||
'affected_sessions': self.affected_sessions,
|
||||
'affected_accounts': self.affected_accounts,
|
||||
'avg_recovery_time': self.avg_recovery_time,
|
||||
'recovery_success_rate': self.recovery_success_rate,
|
||||
'most_common_urls': self.most_common_urls[:5],
|
||||
'most_common_actions': self.most_common_actions[:5],
|
||||
'most_common_steps': self.most_common_steps[:5],
|
||||
'total_user_impact': self.total_user_impact,
|
||||
'total_system_impact': self.total_system_impact,
|
||||
'data_loss_incidents': self.data_loss_incidents,
|
||||
'severity_score': self.severity_score
|
||||
}
|
||||
44
domain/value_objects/login_credentials.py
Normale Datei
44
domain/value_objects/login_credentials.py
Normale Datei
@ -0,0 +1,44 @@
|
||||
"""
|
||||
Login Credentials Value Object - Repräsentiert Login-Daten mit Session-Status
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LoginCredentials:
|
||||
"""Unveränderliche Login-Daten für einen Account"""
|
||||
|
||||
username: str
|
||||
password: str
|
||||
platform: str
|
||||
session_status: str # ACTIVE, EXPIRED, LOCKED, REQUIRES_2FA, UNKNOWN
|
||||
last_successful_login: Optional[datetime] = None
|
||||
session_id: Optional[str] = None
|
||||
fingerprint_id: Optional[str] = None
|
||||
|
||||
def is_session_active(self) -> bool:
|
||||
"""Prüft ob die Session aktiv ist"""
|
||||
return self.session_status == "ACTIVE"
|
||||
|
||||
def requires_manual_login(self) -> bool:
|
||||
"""Prüft ob manueller Login erforderlich ist"""
|
||||
return self.session_status in ["EXPIRED", "LOCKED", "REQUIRES_2FA", "UNKNOWN"]
|
||||
|
||||
def has_session_data(self) -> bool:
|
||||
"""Prüft ob Session-Daten vorhanden sind"""
|
||||
return self.session_id is not None and self.fingerprint_id is not None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Konvertiert zu Dictionary für Serialisierung"""
|
||||
return {
|
||||
'username': self.username,
|
||||
'password': self.password,
|
||||
'platform': self.platform,
|
||||
'session_status': self.session_status,
|
||||
'last_successful_login': self.last_successful_login.isoformat() if self.last_successful_login else None,
|
||||
'session_id': self.session_id,
|
||||
'fingerprint_id': self.fingerprint_id
|
||||
}
|
||||
229
domain/value_objects/operation_result.py
Normale Datei
229
domain/value_objects/operation_result.py
Normale Datei
@ -0,0 +1,229 @@
|
||||
"""
|
||||
Operation Result Value Object - Standardisierte Ergebnisstruktur
|
||||
Backward-compatible Wrapper für konsistente Fehlerbehandlung
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Any, Dict, Union
|
||||
from datetime import datetime
|
||||
import traceback
|
||||
|
||||
|
||||
@dataclass
|
||||
class OperationResult:
|
||||
"""
|
||||
Standardisierte Ergebnisstruktur für alle Operationen.
|
||||
Kompatibel mit bestehenden boolean und dict returns.
|
||||
"""
|
||||
success: bool
|
||||
data: Optional[Any] = None
|
||||
error_message: Optional[str] = None
|
||||
error_code: Optional[str] = None
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
timestamp: Optional[datetime] = None
|
||||
legacy_result: Optional[Any] = None # Für backward compatibility
|
||||
|
||||
def __post_init__(self):
|
||||
if self.timestamp is None:
|
||||
self.timestamp = datetime.now()
|
||||
|
||||
@classmethod
|
||||
def success_result(cls, data: Any = None, metadata: Dict[str, Any] = None, legacy_result: Any = None):
|
||||
"""Erstellt ein Erfolgsergebnis"""
|
||||
return cls(
|
||||
success=True,
|
||||
data=data,
|
||||
metadata=metadata or {},
|
||||
legacy_result=legacy_result if legacy_result is not None else data
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def error_result(cls, message: str, code: str = None,
|
||||
metadata: Dict[str, Any] = None, legacy_result: Any = None):
|
||||
"""Erstellt ein Fehlerergebnis"""
|
||||
return cls(
|
||||
success=False,
|
||||
error_message=message,
|
||||
error_code=code,
|
||||
metadata=metadata or {},
|
||||
legacy_result=legacy_result if legacy_result is not None else False
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_exception(cls, exception: Exception, code: str = None,
|
||||
metadata: Dict[str, Any] = None):
|
||||
"""Erstellt Fehlerergebnis aus Exception"""
|
||||
metadata = metadata or {}
|
||||
metadata.update({
|
||||
'exception_type': type(exception).__name__,
|
||||
'traceback': traceback.format_exc()
|
||||
})
|
||||
|
||||
return cls(
|
||||
success=False,
|
||||
error_message=str(exception),
|
||||
error_code=code or type(exception).__name__,
|
||||
metadata=metadata,
|
||||
legacy_result=False
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_legacy_boolean(cls, result: bool, success_data: Any = None, error_message: str = None):
|
||||
"""Konvertiert legacy boolean zu OperationResult"""
|
||||
if result:
|
||||
return cls.success_result(data=success_data, legacy_result=result)
|
||||
else:
|
||||
return cls.error_result(
|
||||
message=error_message or "Operation failed",
|
||||
legacy_result=result
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_legacy_dict(cls, result: Dict[str, Any]):
|
||||
"""Konvertiert legacy dict zu OperationResult"""
|
||||
success = result.get('success', False)
|
||||
|
||||
if success:
|
||||
return cls.success_result(
|
||||
data=result.get('data'),
|
||||
metadata=result.get('metadata', {}),
|
||||
legacy_result=result
|
||||
)
|
||||
else:
|
||||
return cls.error_result(
|
||||
message=result.get('error', 'Operation failed'),
|
||||
code=result.get('error_code'),
|
||||
metadata=result.get('metadata', {}),
|
||||
legacy_result=result
|
||||
)
|
||||
|
||||
def is_success(self) -> bool:
|
||||
"""Prüft ob Operation erfolgreich war"""
|
||||
return self.success
|
||||
|
||||
def is_error(self) -> bool:
|
||||
"""Prüft ob Operation fehlgeschlagen ist"""
|
||||
return not self.success
|
||||
|
||||
def get_legacy_result(self) -> Any:
|
||||
"""Gibt das ursprüngliche Result-Format zurück für backward compatibility"""
|
||||
return self.legacy_result
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Konvertiert zu Dictionary für API/JSON Serialisierung"""
|
||||
result = {
|
||||
'success': self.success,
|
||||
'timestamp': self.timestamp.isoformat() if self.timestamp else None
|
||||
}
|
||||
|
||||
if self.data is not None:
|
||||
result['data'] = self.data
|
||||
|
||||
if self.error_message:
|
||||
result['error'] = self.error_message
|
||||
|
||||
if self.error_code:
|
||||
result['error_code'] = self.error_code
|
||||
|
||||
if self.metadata:
|
||||
result['metadata'] = self.metadata
|
||||
|
||||
return result
|
||||
|
||||
def to_legacy_dict(self) -> Dict[str, Any]:
|
||||
"""Konvertiert zu legacy dict format"""
|
||||
return {
|
||||
'success': self.success,
|
||||
'data': self.data,
|
||||
'error': self.error_message,
|
||||
'error_code': self.error_code,
|
||||
'metadata': self.metadata
|
||||
}
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
"""Ermöglicht if result: syntax"""
|
||||
return self.success
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.success:
|
||||
return f"Success: {self.data}"
|
||||
else:
|
||||
return f"Error: {self.error_message} ({self.error_code})"
|
||||
|
||||
|
||||
class ResultWrapper:
|
||||
"""
|
||||
Utility-Klasse für backward-compatible Wrapping von bestehenden Methoden
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def wrap_boolean_method(method, *args, **kwargs) -> OperationResult:
|
||||
"""Wrapper für bestehende boolean-Methoden"""
|
||||
try:
|
||||
success = method(*args, **kwargs)
|
||||
return OperationResult.from_legacy_boolean(
|
||||
result=success,
|
||||
success_data=success,
|
||||
error_message="Operation failed" if not success else None
|
||||
)
|
||||
except Exception as e:
|
||||
return OperationResult.from_exception(e)
|
||||
|
||||
@staticmethod
|
||||
def wrap_dict_method(method, *args, **kwargs) -> OperationResult:
|
||||
"""Wrapper für bestehende dict-Methoden"""
|
||||
try:
|
||||
result = method(*args, **kwargs)
|
||||
if isinstance(result, dict):
|
||||
return OperationResult.from_legacy_dict(result)
|
||||
else:
|
||||
return OperationResult.success_result(data=result, legacy_result=result)
|
||||
except Exception as e:
|
||||
return OperationResult.from_exception(e)
|
||||
|
||||
@staticmethod
|
||||
def wrap_any_method(method, *args, **kwargs) -> OperationResult:
|
||||
"""Universal wrapper für beliebige Methoden"""
|
||||
try:
|
||||
result = method(*args, **kwargs)
|
||||
|
||||
if isinstance(result, bool):
|
||||
return OperationResult.from_legacy_boolean(result)
|
||||
elif isinstance(result, dict) and 'success' in result:
|
||||
return OperationResult.from_legacy_dict(result)
|
||||
elif result is None:
|
||||
return OperationResult.error_result("Method returned None", legacy_result=result)
|
||||
else:
|
||||
return OperationResult.success_result(data=result, legacy_result=result)
|
||||
|
||||
except Exception as e:
|
||||
return OperationResult.from_exception(e)
|
||||
|
||||
|
||||
# Error Codes für häufige Fehlertypen
|
||||
class CommonErrorCodes:
|
||||
"""Häufig verwendete Fehlercodes"""
|
||||
|
||||
# Instagram spezifisch
|
||||
CAPTCHA_REQUIRED = "CAPTCHA_REQUIRED"
|
||||
EMAIL_TIMEOUT = "EMAIL_TIMEOUT"
|
||||
SMS_NOT_IMPLEMENTED = "SMS_NOT_IMPLEMENTED"
|
||||
USERNAME_TAKEN = "USERNAME_TAKEN"
|
||||
SELECTOR_NOT_FOUND = "SELECTOR_NOT_FOUND"
|
||||
BIRTHDAY_SELECTOR_FAILED = "BIRTHDAY_SELECTOR_FAILED"
|
||||
|
||||
# Allgemein
|
||||
PROXY_ERROR = "PROXY_ERROR"
|
||||
RATE_LIMITED = "RATE_LIMITED"
|
||||
NETWORK_TIMEOUT = "NETWORK_TIMEOUT"
|
||||
BROWSER_ERROR = "BROWSER_ERROR"
|
||||
|
||||
# Fingerprint spezifisch
|
||||
FINGERPRINT_GENERATION_FAILED = "FINGERPRINT_GENERATION_FAILED"
|
||||
FINGERPRINT_RACE_CONDITION = "FINGERPRINT_RACE_CONDITION"
|
||||
FINGERPRINT_NOT_FOUND = "FINGERPRINT_NOT_FOUND"
|
||||
|
||||
# Session spezifisch
|
||||
SESSION_EXPIRED = "SESSION_EXPIRED"
|
||||
SESSION_INVALID = "SESSION_INVALID"
|
||||
SESSION_SAVE_FAILED = "SESSION_SAVE_FAILED"
|
||||
204
domain/value_objects/report.py
Normale Datei
204
domain/value_objects/report.py
Normale Datei
@ -0,0 +1,204 @@
|
||||
"""
|
||||
Report Value Object - Strukturierte Berichte für Analytics
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Any, Optional
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ReportType(Enum):
|
||||
"""Typen von verfügbaren Berichten"""
|
||||
DAILY = "daily"
|
||||
WEEKLY = "weekly"
|
||||
MONTHLY = "monthly"
|
||||
CUSTOM = "custom"
|
||||
REAL_TIME = "real_time"
|
||||
|
||||
|
||||
class MetricType(Enum):
|
||||
"""Typen von Metriken in Berichten"""
|
||||
SUCCESS_RATE = "success_rate"
|
||||
ERROR_RATE = "error_rate"
|
||||
AVG_DURATION = "avg_duration"
|
||||
TOTAL_ACCOUNTS = "total_accounts"
|
||||
ACCOUNTS_PER_HOUR = "accounts_per_hour"
|
||||
RETRY_RATE = "retry_rate"
|
||||
RECOVERY_RATE = "recovery_rate"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Metric:
|
||||
"""Einzelne Metrik im Bericht"""
|
||||
name: str
|
||||
value: float
|
||||
unit: str
|
||||
trend: float = 0.0 # Prozentuale Veränderung zum Vorperiode
|
||||
|
||||
@property
|
||||
def is_improving(self) -> bool:
|
||||
"""Prüft ob sich die Metrik verbessert"""
|
||||
positive_metrics = ["success_rate", "recovery_rate", "accounts_per_hour"]
|
||||
if self.name in positive_metrics:
|
||||
return self.trend > 0
|
||||
return self.trend < 0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PlatformStats:
|
||||
"""Statistiken für eine spezifische Plattform"""
|
||||
platform: str
|
||||
total_attempts: int
|
||||
successful_accounts: int
|
||||
failed_attempts: int
|
||||
avg_duration_seconds: float
|
||||
error_distribution: Dict[str, int]
|
||||
|
||||
@property
|
||||
def success_rate(self) -> float:
|
||||
"""Berechnet die Erfolgsrate"""
|
||||
if self.total_attempts > 0:
|
||||
return self.successful_accounts / self.total_attempts
|
||||
return 0.0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TimeSeriesData:
|
||||
"""Zeitreihen-Daten für Graphen"""
|
||||
timestamps: List[datetime]
|
||||
values: List[float]
|
||||
label: str
|
||||
|
||||
def get_average(self) -> float:
|
||||
"""Berechnet den Durchschnitt der Werte"""
|
||||
if self.values:
|
||||
return sum(self.values) / len(self.values)
|
||||
return 0.0
|
||||
|
||||
def get_trend(self) -> float:
|
||||
"""Berechnet den Trend (vereinfacht: Vergleich erste/letzte Hälfte)"""
|
||||
if len(self.values) < 2:
|
||||
return 0.0
|
||||
mid = len(self.values) // 2
|
||||
first_half_avg = sum(self.values[:mid]) / mid
|
||||
second_half_avg = sum(self.values[mid:]) / (len(self.values) - mid)
|
||||
if first_half_avg > 0:
|
||||
return ((second_half_avg - first_half_avg) / first_half_avg) * 100
|
||||
return 0.0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Report:
|
||||
"""
|
||||
Strukturierter Bericht für Analytics.
|
||||
Frozen dataclass macht es unveränderlich (Value Object).
|
||||
"""
|
||||
|
||||
report_id: str
|
||||
report_type: ReportType
|
||||
start_date: datetime
|
||||
end_date: datetime
|
||||
generated_at: datetime
|
||||
|
||||
# Zusammenfassende Metriken
|
||||
total_accounts_created: int
|
||||
total_attempts: int
|
||||
overall_success_rate: float
|
||||
avg_creation_time: float # in Sekunden
|
||||
|
||||
# Detaillierte Metriken
|
||||
metrics: List[Metric]
|
||||
platform_stats: List[PlatformStats]
|
||||
error_summaries: List[Dict[str, Any]] # ErrorSummary als Dict
|
||||
|
||||
# Zeitreihen-Daten
|
||||
success_rate_timeline: Optional[TimeSeriesData] = None
|
||||
creation_rate_timeline: Optional[TimeSeriesData] = None
|
||||
error_rate_timeline: Optional[TimeSeriesData] = None
|
||||
|
||||
# Top-Erkenntnisse
|
||||
insights: List[str] = field(default_factory=list)
|
||||
recommendations: List[str] = field(default_factory=list)
|
||||
|
||||
def __post_init__(self):
|
||||
"""Validierung der Report-Daten"""
|
||||
if self.start_date > self.end_date:
|
||||
raise ValueError("Start date kann nicht nach end date liegen")
|
||||
if not 0.0 <= self.overall_success_rate <= 1.0:
|
||||
raise ValueError("Success rate muss zwischen 0.0 und 1.0 liegen")
|
||||
if self.avg_creation_time < 0:
|
||||
raise ValueError("Average creation time kann nicht negativ sein")
|
||||
|
||||
@property
|
||||
def duration(self) -> timedelta:
|
||||
"""Dauer des Berichtszeitraums"""
|
||||
return self.end_date - self.start_date
|
||||
|
||||
@property
|
||||
def accounts_per_day(self) -> float:
|
||||
"""Durchschnittliche Accounts pro Tag"""
|
||||
days = self.duration.days or 1
|
||||
return self.total_accounts_created / days
|
||||
|
||||
def get_metric(self, metric_type: MetricType) -> Optional[Metric]:
|
||||
"""Holt eine spezifische Metrik"""
|
||||
for metric in self.metrics:
|
||||
if metric.name == metric_type.value:
|
||||
return metric
|
||||
return None
|
||||
|
||||
def get_platform_stats(self, platform: str) -> Optional[PlatformStats]:
|
||||
"""Holt Statistiken für eine spezifische Plattform"""
|
||||
for stats in self.platform_stats:
|
||||
if stats.platform.lower() == platform.lower():
|
||||
return stats
|
||||
return None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Konvertiert zu Dictionary für Serialisierung"""
|
||||
return {
|
||||
'report_id': self.report_id,
|
||||
'report_type': self.report_type.value,
|
||||
'start_date': self.start_date.isoformat(),
|
||||
'end_date': self.end_date.isoformat(),
|
||||
'generated_at': self.generated_at.isoformat(),
|
||||
'duration_days': self.duration.days,
|
||||
'total_accounts_created': self.total_accounts_created,
|
||||
'total_attempts': self.total_attempts,
|
||||
'overall_success_rate': self.overall_success_rate,
|
||||
'avg_creation_time': self.avg_creation_time,
|
||||
'accounts_per_day': self.accounts_per_day,
|
||||
'metrics': [
|
||||
{
|
||||
'name': m.name,
|
||||
'value': m.value,
|
||||
'unit': m.unit,
|
||||
'trend': m.trend,
|
||||
'is_improving': m.is_improving
|
||||
}
|
||||
for m in self.metrics
|
||||
],
|
||||
'platform_stats': [
|
||||
{
|
||||
'platform': ps.platform,
|
||||
'total_attempts': ps.total_attempts,
|
||||
'successful_accounts': ps.successful_accounts,
|
||||
'failed_attempts': ps.failed_attempts,
|
||||
'success_rate': ps.success_rate,
|
||||
'avg_duration_seconds': ps.avg_duration_seconds,
|
||||
'error_distribution': ps.error_distribution
|
||||
}
|
||||
for ps in self.platform_stats
|
||||
],
|
||||
'error_summaries': self.error_summaries,
|
||||
'success_rate_timeline': {
|
||||
'timestamps': [t.isoformat() for t in self.success_rate_timeline.timestamps],
|
||||
'values': self.success_rate_timeline.values,
|
||||
'label': self.success_rate_timeline.label,
|
||||
'average': self.success_rate_timeline.get_average(),
|
||||
'trend': self.success_rate_timeline.get_trend()
|
||||
} if self.success_rate_timeline else None,
|
||||
'insights': self.insights,
|
||||
'recommendations': self.recommendations
|
||||
}
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren