Initial commit
Dieser Commit ist enthalten in:
3
domain/__init__.py
Normale Datei
3
domain/__init__.py
Normale Datei
@ -0,0 +1,3 @@
|
||||
"""
|
||||
Domain Layer - Enthält die Geschäftslogik und Kernkonzepte der Anwendung
|
||||
"""
|
||||
15
domain/entities/__init__.py
Normale Datei
15
domain/entities/__init__.py
Normale Datei
@ -0,0 +1,15 @@
|
||||
"""
|
||||
Domain Entities - Geschäftsobjekte mit Identität
|
||||
"""
|
||||
|
||||
from .rate_limit_policy import RateLimitPolicy
|
||||
from .browser_fingerprint import BrowserFingerprint
|
||||
from .account_creation_event import AccountCreationEvent
|
||||
from .error_event import ErrorEvent
|
||||
|
||||
__all__ = [
|
||||
'RateLimitPolicy',
|
||||
'BrowserFingerprint',
|
||||
'AccountCreationEvent',
|
||||
'ErrorEvent'
|
||||
]
|
||||
174
domain/entities/account_creation_event.py
Normale Datei
174
domain/entities/account_creation_event.py
Normale Datei
@ -0,0 +1,174 @@
|
||||
"""
|
||||
Account Creation Event Entity - Event für jede Account-Erstellung
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Any, Optional
|
||||
from enum import Enum
|
||||
import uuid
|
||||
|
||||
|
||||
class WorkflowStepStatus(Enum):
|
||||
"""Status eines Workflow-Schritts"""
|
||||
PENDING = "pending"
|
||||
IN_PROGRESS = "in_progress"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
SKIPPED = "skipped"
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkflowStep:
|
||||
"""Einzelner Schritt im Account-Erstellungsprozess"""
|
||||
step_name: str
|
||||
start_time: datetime
|
||||
end_time: Optional[datetime] = None
|
||||
status: WorkflowStepStatus = WorkflowStepStatus.PENDING
|
||||
retry_count: int = 0
|
||||
error_message: Optional[str] = None
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def duration(self) -> Optional[timedelta]:
|
||||
"""Berechnet die Dauer des Schritts"""
|
||||
if self.start_time and self.end_time:
|
||||
return self.end_time - self.start_time
|
||||
return None
|
||||
|
||||
@property
|
||||
def success(self) -> bool:
|
||||
"""Prüft ob der Schritt erfolgreich war"""
|
||||
return self.status == WorkflowStepStatus.COMPLETED
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Konvertiert zu Dictionary"""
|
||||
return {
|
||||
'step_name': self.step_name,
|
||||
'start_time': self.start_time.isoformat(),
|
||||
'end_time': self.end_time.isoformat() if self.end_time else None,
|
||||
'status': self.status.value,
|
||||
'retry_count': self.retry_count,
|
||||
'error_message': self.error_message,
|
||||
'metadata': self.metadata,
|
||||
'duration_seconds': self.duration.total_seconds() if self.duration else None
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class AccountData:
|
||||
"""Daten des erstellten Accounts"""
|
||||
platform: str
|
||||
username: str
|
||||
password: str
|
||||
email: str
|
||||
phone: Optional[str] = None
|
||||
full_name: Optional[str] = None
|
||||
birthday: Optional[str] = None
|
||||
profile_image: Optional[str] = None
|
||||
bio: Optional[str] = None
|
||||
verification_status: str = "unverified"
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ErrorDetails:
|
||||
"""Details zu aufgetretenen Fehlern"""
|
||||
error_type: str
|
||||
error_message: str
|
||||
stack_trace: Optional[str] = None
|
||||
screenshot_path: Optional[str] = None
|
||||
recovery_attempted: bool = False
|
||||
recovery_successful: bool = False
|
||||
context: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AccountCreationEvent:
|
||||
"""Event für jede Account-Erstellung"""
|
||||
|
||||
event_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
||||
timestamp: datetime = field(default_factory=datetime.now)
|
||||
account_data: Optional[AccountData] = None
|
||||
session_id: str = ""
|
||||
fingerprint_id: str = ""
|
||||
duration: Optional[timedelta] = None
|
||||
success: bool = False
|
||||
error_details: Optional[ErrorDetails] = None
|
||||
steps_completed: List[WorkflowStep] = field(default_factory=list)
|
||||
|
||||
# Performance-Metriken
|
||||
total_retry_count: int = 0
|
||||
network_requests: int = 0
|
||||
screenshots_taken: int = 0
|
||||
|
||||
# Kontext-Informationen
|
||||
proxy_used: bool = False
|
||||
proxy_type: Optional[str] = None
|
||||
browser_type: str = "chromium"
|
||||
headless: bool = False
|
||||
|
||||
def add_step(self, step: WorkflowStep):
|
||||
"""Fügt einen Workflow-Schritt hinzu"""
|
||||
self.steps_completed.append(step)
|
||||
if step.retry_count > 0:
|
||||
self.total_retry_count += step.retry_count
|
||||
|
||||
def get_step(self, step_name: str) -> Optional[WorkflowStep]:
|
||||
"""Holt einen Schritt nach Name"""
|
||||
for step in self.steps_completed:
|
||||
if step.step_name == step_name:
|
||||
return step
|
||||
return None
|
||||
|
||||
def calculate_duration(self):
|
||||
"""Berechnet die Gesamtdauer der Account-Erstellung"""
|
||||
if self.steps_completed:
|
||||
first_step = min(self.steps_completed, key=lambda s: s.start_time)
|
||||
last_step = max(self.steps_completed, key=lambda s: s.end_time or s.start_time)
|
||||
if last_step.end_time:
|
||||
self.duration = last_step.end_time - first_step.start_time
|
||||
|
||||
def get_success_rate(self) -> float:
|
||||
"""Berechnet die Erfolgsrate der Schritte"""
|
||||
if not self.steps_completed:
|
||||
return 0.0
|
||||
successful_steps = sum(1 for step in self.steps_completed if step.success)
|
||||
return successful_steps / len(self.steps_completed)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Konvertiert Event zu Dictionary für Serialisierung"""
|
||||
return {
|
||||
'event_id': self.event_id,
|
||||
'timestamp': self.timestamp.isoformat(),
|
||||
'account_data': {
|
||||
'platform': self.account_data.platform,
|
||||
'username': self.account_data.username,
|
||||
'email': self.account_data.email,
|
||||
'phone': self.account_data.phone,
|
||||
'full_name': self.account_data.full_name,
|
||||
'birthday': self.account_data.birthday,
|
||||
'verification_status': self.account_data.verification_status,
|
||||
'metadata': self.account_data.metadata
|
||||
} if self.account_data else None,
|
||||
'session_id': self.session_id,
|
||||
'fingerprint_id': self.fingerprint_id,
|
||||
'duration_seconds': self.duration.total_seconds() if self.duration else None,
|
||||
'success': self.success,
|
||||
'error_details': {
|
||||
'error_type': self.error_details.error_type,
|
||||
'error_message': self.error_details.error_message,
|
||||
'recovery_attempted': self.error_details.recovery_attempted,
|
||||
'recovery_successful': self.error_details.recovery_successful,
|
||||
'context': self.error_details.context
|
||||
} if self.error_details else None,
|
||||
'steps_completed': [step.to_dict() for step in self.steps_completed],
|
||||
'total_retry_count': self.total_retry_count,
|
||||
'network_requests': self.network_requests,
|
||||
'screenshots_taken': self.screenshots_taken,
|
||||
'proxy_used': self.proxy_used,
|
||||
'proxy_type': self.proxy_type,
|
||||
'browser_type': self.browser_type,
|
||||
'headless': self.headless,
|
||||
'success_rate': self.get_success_rate()
|
||||
}
|
||||
276
domain/entities/browser_fingerprint.py
Normale Datei
276
domain/entities/browser_fingerprint.py
Normale Datei
@ -0,0 +1,276 @@
|
||||
"""
|
||||
Browser Fingerprint Entity - Repräsentiert einen kompletten Browser-Fingerprint
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any, Optional
|
||||
from enum import Enum
|
||||
import uuid
|
||||
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class StaticComponents:
|
||||
"""Static fingerprint components that don't change"""
|
||||
device_type: str = "desktop" # desktop/mobile/tablet
|
||||
os_family: str = "windows" # windows/macos/linux/android/ios
|
||||
browser_family: str = "chromium" # chromium/firefox/safari
|
||||
gpu_vendor: str = "Intel Inc."
|
||||
gpu_model: str = "Intel Iris OpenGL Engine"
|
||||
cpu_architecture: str = "x86_64"
|
||||
base_fonts: List[str] = field(default_factory=list)
|
||||
base_resolution: tuple = (1920, 1080)
|
||||
base_timezone: str = "Europe/Berlin"
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'device_type': self.device_type,
|
||||
'os_family': self.os_family,
|
||||
'browser_family': self.browser_family,
|
||||
'gpu_vendor': self.gpu_vendor,
|
||||
'gpu_model': self.gpu_model,
|
||||
'cpu_architecture': self.cpu_architecture,
|
||||
'base_fonts': self.base_fonts,
|
||||
'base_resolution': self.base_resolution,
|
||||
'base_timezone': self.base_timezone
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class CanvasNoise:
|
||||
"""Canvas Fingerprinting Schutz-Konfiguration"""
|
||||
noise_level: float = 0.02
|
||||
seed: int = 42
|
||||
algorithm: str = "gaussian"
|
||||
|
||||
|
||||
@dataclass
|
||||
class WebRTCConfig:
|
||||
"""WebRTC Konfiguration für IP-Leak Prevention"""
|
||||
enabled: bool = True
|
||||
ice_servers: List[str] = field(default_factory=list)
|
||||
local_ip_mask: str = "10.0.0.x"
|
||||
disable_webrtc: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class HardwareConfig:
|
||||
"""Hardware-Konfiguration für Fingerprinting"""
|
||||
hardware_concurrency: int = 4
|
||||
device_memory: int = 8
|
||||
max_touch_points: int = 0
|
||||
screen_resolution: tuple = (1920, 1080)
|
||||
color_depth: int = 24
|
||||
pixel_ratio: float = 1.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class NavigatorProperties:
|
||||
"""Navigator-Eigenschaften für Browser-Fingerprint"""
|
||||
platform: str = "Win32"
|
||||
vendor: str = "Google Inc."
|
||||
vendor_sub: str = ""
|
||||
product: str = "Gecko"
|
||||
product_sub: str = "20030107"
|
||||
app_name: str = "Netscape"
|
||||
app_version: str = "5.0"
|
||||
user_agent: str = ""
|
||||
language: str = "de-DE"
|
||||
languages: List[str] = field(default_factory=lambda: ["de-DE", "de", "en-US", "en"])
|
||||
online: bool = True
|
||||
do_not_track: str = "1"
|
||||
|
||||
|
||||
@dataclass
|
||||
class BrowserFingerprint:
|
||||
"""Repräsentiert einen kompletten Browser-Fingerprint"""
|
||||
|
||||
fingerprint_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
||||
canvas_noise: CanvasNoise = field(default_factory=CanvasNoise)
|
||||
webrtc_config: WebRTCConfig = field(default_factory=WebRTCConfig)
|
||||
font_list: List[str] = field(default_factory=list)
|
||||
hardware_config: HardwareConfig = field(default_factory=HardwareConfig)
|
||||
navigator_props: NavigatorProperties = field(default_factory=NavigatorProperties)
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
last_rotated: datetime = None
|
||||
|
||||
# WebGL Parameter
|
||||
webgl_vendor: str = "Intel Inc."
|
||||
webgl_renderer: str = "Intel Iris OpenGL Engine"
|
||||
|
||||
# Audio Context
|
||||
audio_context_base_latency: float = 0.00
|
||||
audio_context_output_latency: float = 0.00
|
||||
audio_context_sample_rate: int = 48000
|
||||
|
||||
# Timezone
|
||||
timezone: str = "Europe/Berlin"
|
||||
timezone_offset: int = -60 # UTC+1
|
||||
|
||||
# Plugins
|
||||
plugins: List[Dict[str, str]] = field(default_factory=list)
|
||||
|
||||
# New fields for account-bound persistence
|
||||
static_components: Optional[StaticComponents] = None
|
||||
rotation_seed: Optional[str] = None
|
||||
account_bound: bool = False
|
||||
platform_specific_config: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Konvertiert Fingerprint zu Dictionary für Serialisierung"""
|
||||
return {
|
||||
'fingerprint_id': self.fingerprint_id,
|
||||
'canvas_noise': {
|
||||
'noise_level': self.canvas_noise.noise_level,
|
||||
'seed': self.canvas_noise.seed,
|
||||
'algorithm': self.canvas_noise.algorithm
|
||||
},
|
||||
'webrtc_config': {
|
||||
'enabled': self.webrtc_config.enabled,
|
||||
'ice_servers': self.webrtc_config.ice_servers,
|
||||
'local_ip_mask': self.webrtc_config.local_ip_mask,
|
||||
'disable_webrtc': self.webrtc_config.disable_webrtc
|
||||
},
|
||||
'font_list': self.font_list,
|
||||
'hardware_config': {
|
||||
'hardware_concurrency': self.hardware_config.hardware_concurrency,
|
||||
'device_memory': self.hardware_config.device_memory,
|
||||
'max_touch_points': self.hardware_config.max_touch_points,
|
||||
'screen_resolution': self.hardware_config.screen_resolution,
|
||||
'color_depth': self.hardware_config.color_depth,
|
||||
'pixel_ratio': self.hardware_config.pixel_ratio
|
||||
},
|
||||
'navigator_props': {
|
||||
'platform': self.navigator_props.platform,
|
||||
'vendor': self.navigator_props.vendor,
|
||||
'vendor_sub': self.navigator_props.vendor_sub,
|
||||
'product': self.navigator_props.product,
|
||||
'product_sub': self.navigator_props.product_sub,
|
||||
'app_name': self.navigator_props.app_name,
|
||||
'app_version': self.navigator_props.app_version,
|
||||
'user_agent': self.navigator_props.user_agent,
|
||||
'language': self.navigator_props.language,
|
||||
'languages': self.navigator_props.languages,
|
||||
'online': self.navigator_props.online,
|
||||
'do_not_track': self.navigator_props.do_not_track
|
||||
},
|
||||
'webgl_vendor': self.webgl_vendor,
|
||||
'webgl_renderer': self.webgl_renderer,
|
||||
'audio_context': {
|
||||
'base_latency': self.audio_context_base_latency,
|
||||
'output_latency': self.audio_context_output_latency,
|
||||
'sample_rate': self.audio_context_sample_rate
|
||||
},
|
||||
'timezone': self.timezone,
|
||||
'timezone_offset': self.timezone_offset,
|
||||
'plugins': self.plugins,
|
||||
'created_at': self.created_at.isoformat(),
|
||||
'last_rotated': self.last_rotated.isoformat() if self.last_rotated else None,
|
||||
'static_components': self.static_components.to_dict() if self.static_components else None,
|
||||
'rotation_seed': self.rotation_seed,
|
||||
'account_bound': self.account_bound,
|
||||
'platform_specific_config': self.platform_specific_config
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'BrowserFingerprint':
|
||||
"""Creates BrowserFingerprint from dictionary"""
|
||||
fingerprint = cls()
|
||||
|
||||
# Basic fields
|
||||
fingerprint.fingerprint_id = data.get('fingerprint_id', str(uuid.uuid4()))
|
||||
fingerprint.webgl_vendor = data.get('webgl_vendor', "Intel Inc.")
|
||||
fingerprint.webgl_renderer = data.get('webgl_renderer', "Intel Iris OpenGL Engine")
|
||||
fingerprint.timezone = data.get('timezone', "Europe/Berlin")
|
||||
fingerprint.timezone_offset = data.get('timezone_offset', -60)
|
||||
fingerprint.plugins = data.get('plugins', [])
|
||||
|
||||
# Canvas noise
|
||||
if 'canvas_noise' in data:
|
||||
cn = data['canvas_noise']
|
||||
fingerprint.canvas_noise = CanvasNoise(
|
||||
noise_level=cn.get('noise_level', 0.02),
|
||||
seed=cn.get('seed', 42),
|
||||
algorithm=cn.get('algorithm', 'gaussian')
|
||||
)
|
||||
|
||||
# WebRTC config
|
||||
if 'webrtc_config' in data:
|
||||
wc = data['webrtc_config']
|
||||
fingerprint.webrtc_config = WebRTCConfig(
|
||||
enabled=wc.get('enabled', True),
|
||||
ice_servers=wc.get('ice_servers', []),
|
||||
local_ip_mask=wc.get('local_ip_mask', "10.0.0.x"),
|
||||
disable_webrtc=wc.get('disable_webrtc', False)
|
||||
)
|
||||
|
||||
# Hardware config
|
||||
if 'hardware_config' in data:
|
||||
hc = data['hardware_config']
|
||||
fingerprint.hardware_config = HardwareConfig(
|
||||
hardware_concurrency=hc.get('hardware_concurrency', 4),
|
||||
device_memory=hc.get('device_memory', 8),
|
||||
max_touch_points=hc.get('max_touch_points', 0),
|
||||
screen_resolution=tuple(hc.get('screen_resolution', [1920, 1080])),
|
||||
color_depth=hc.get('color_depth', 24),
|
||||
pixel_ratio=hc.get('pixel_ratio', 1.0)
|
||||
)
|
||||
|
||||
# Navigator properties
|
||||
if 'navigator_props' in data:
|
||||
np = data['navigator_props']
|
||||
fingerprint.navigator_props = NavigatorProperties(
|
||||
platform=np.get('platform', "Win32"),
|
||||
vendor=np.get('vendor', "Google Inc."),
|
||||
vendor_sub=np.get('vendor_sub', ""),
|
||||
product=np.get('product', "Gecko"),
|
||||
product_sub=np.get('product_sub', "20030107"),
|
||||
app_name=np.get('app_name', "Netscape"),
|
||||
app_version=np.get('app_version', "5.0"),
|
||||
user_agent=np.get('user_agent', ""),
|
||||
language=np.get('language', "de-DE"),
|
||||
languages=np.get('languages', ["de-DE", "de", "en-US", "en"]),
|
||||
online=np.get('online', True),
|
||||
do_not_track=np.get('do_not_track', "1")
|
||||
)
|
||||
|
||||
# Audio context
|
||||
if 'audio_context' in data:
|
||||
ac = data['audio_context']
|
||||
fingerprint.audio_context_base_latency = ac.get('base_latency', 0.00)
|
||||
fingerprint.audio_context_output_latency = ac.get('output_latency', 0.00)
|
||||
fingerprint.audio_context_sample_rate = ac.get('sample_rate', 48000)
|
||||
|
||||
# Font list
|
||||
fingerprint.font_list = data.get('font_list', [])
|
||||
|
||||
# Dates
|
||||
if 'created_at' in data:
|
||||
fingerprint.created_at = datetime.fromisoformat(data['created_at'])
|
||||
if 'last_rotated' in data and data['last_rotated']:
|
||||
fingerprint.last_rotated = datetime.fromisoformat(data['last_rotated'])
|
||||
|
||||
# New persistence fields
|
||||
if 'static_components' in data and data['static_components']:
|
||||
sc = data['static_components']
|
||||
fingerprint.static_components = StaticComponents(
|
||||
device_type=sc.get('device_type', 'desktop'),
|
||||
os_family=sc.get('os_family', 'windows'),
|
||||
browser_family=sc.get('browser_family', 'chromium'),
|
||||
gpu_vendor=sc.get('gpu_vendor', 'Intel Inc.'),
|
||||
gpu_model=sc.get('gpu_model', 'Intel Iris OpenGL Engine'),
|
||||
cpu_architecture=sc.get('cpu_architecture', 'x86_64'),
|
||||
base_fonts=sc.get('base_fonts', []),
|
||||
base_resolution=tuple(sc.get('base_resolution', [1920, 1080])),
|
||||
base_timezone=sc.get('base_timezone', 'Europe/Berlin')
|
||||
)
|
||||
|
||||
fingerprint.rotation_seed = data.get('rotation_seed')
|
||||
fingerprint.account_bound = data.get('account_bound', False)
|
||||
fingerprint.platform_specific_config = data.get('platform_specific_config', {})
|
||||
|
||||
return fingerprint
|
||||
150
domain/entities/error_event.py
Normale Datei
150
domain/entities/error_event.py
Normale Datei
@ -0,0 +1,150 @@
|
||||
"""
|
||||
Error Event Entity - Detailliertes Fehler-Event
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional, List
|
||||
from enum import Enum
|
||||
import uuid
|
||||
|
||||
|
||||
class ErrorType(Enum):
|
||||
"""Typen von Fehlern die auftreten können"""
|
||||
RATE_LIMIT = "rate_limit"
|
||||
CAPTCHA = "captcha"
|
||||
NETWORK = "network"
|
||||
VALIDATION = "validation"
|
||||
BROWSER = "browser"
|
||||
PROXY = "proxy"
|
||||
EMAIL = "email"
|
||||
TIMEOUT = "timeout"
|
||||
AUTHENTICATION = "authentication"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
class ErrorSeverity(Enum):
|
||||
"""Schweregrad des Fehlers"""
|
||||
LOW = "low"
|
||||
MEDIUM = "medium"
|
||||
HIGH = "high"
|
||||
CRITICAL = "critical"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ErrorContext:
|
||||
"""Kontext-Informationen zum Fehler"""
|
||||
url: Optional[str] = None
|
||||
action: Optional[str] = None
|
||||
step_name: Optional[str] = None
|
||||
user_input: Optional[Dict[str, Any]] = None
|
||||
browser_state: Optional[Dict[str, Any]] = None
|
||||
network_state: Optional[Dict[str, Any]] = None
|
||||
screenshot_path: Optional[str] = None
|
||||
html_snapshot: Optional[str] = None
|
||||
additional_data: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RecoveryAttempt:
|
||||
"""Informationen über Wiederherstellungsversuche"""
|
||||
strategy: str
|
||||
timestamp: datetime
|
||||
successful: bool
|
||||
error_message: Optional[str] = None
|
||||
duration_seconds: float = 0.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class ErrorEvent:
|
||||
"""Detailliertes Fehler-Event"""
|
||||
|
||||
error_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
||||
timestamp: datetime = field(default_factory=datetime.now)
|
||||
error_type: ErrorType = ErrorType.UNKNOWN
|
||||
error_message: str = ""
|
||||
stack_trace: Optional[str] = None
|
||||
context: ErrorContext = field(default_factory=ErrorContext)
|
||||
recovery_attempted: bool = False
|
||||
recovery_successful: bool = False
|
||||
recovery_attempts: List[RecoveryAttempt] = field(default_factory=list)
|
||||
|
||||
# Fehler-Metadaten
|
||||
severity: ErrorSeverity = ErrorSeverity.MEDIUM
|
||||
platform: Optional[str] = None
|
||||
session_id: Optional[str] = None
|
||||
account_id: Optional[str] = None
|
||||
correlation_id: Optional[str] = None
|
||||
|
||||
# Impact-Metriken
|
||||
user_impact: bool = True
|
||||
system_impact: bool = False
|
||||
data_loss: bool = False
|
||||
|
||||
def add_recovery_attempt(self, attempt: RecoveryAttempt):
|
||||
"""Fügt einen Wiederherstellungsversuch hinzu"""
|
||||
self.recovery_attempts.append(attempt)
|
||||
self.recovery_attempted = True
|
||||
if attempt.successful:
|
||||
self.recovery_successful = True
|
||||
|
||||
def get_recovery_success_rate(self) -> float:
|
||||
"""Berechnet die Erfolgsrate der Wiederherstellungsversuche"""
|
||||
if not self.recovery_attempts:
|
||||
return 0.0
|
||||
successful = sum(1 for attempt in self.recovery_attempts if attempt.successful)
|
||||
return successful / len(self.recovery_attempts)
|
||||
|
||||
def is_critical(self) -> bool:
|
||||
"""Prüft ob der Fehler kritisch ist"""
|
||||
return self.severity == ErrorSeverity.CRITICAL or self.data_loss
|
||||
|
||||
def should_retry(self) -> bool:
|
||||
"""Entscheidet ob ein Retry sinnvoll ist"""
|
||||
recoverable_types = [
|
||||
ErrorType.NETWORK,
|
||||
ErrorType.TIMEOUT,
|
||||
ErrorType.RATE_LIMIT,
|
||||
ErrorType.PROXY
|
||||
]
|
||||
return (self.error_type in recoverable_types and
|
||||
len(self.recovery_attempts) < 3 and
|
||||
not self.recovery_successful)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Konvertiert Event zu Dictionary für Serialisierung"""
|
||||
return {
|
||||
'error_id': self.error_id,
|
||||
'timestamp': self.timestamp.isoformat(),
|
||||
'error_type': self.error_type.value,
|
||||
'error_message': self.error_message,
|
||||
'stack_trace': self.stack_trace,
|
||||
'context': {
|
||||
'url': self.context.url,
|
||||
'action': self.context.action,
|
||||
'step_name': self.context.step_name,
|
||||
'screenshot_path': self.context.screenshot_path,
|
||||
'additional_data': self.context.additional_data
|
||||
},
|
||||
'recovery_attempted': self.recovery_attempted,
|
||||
'recovery_successful': self.recovery_successful,
|
||||
'recovery_attempts': [
|
||||
{
|
||||
'strategy': attempt.strategy,
|
||||
'timestamp': attempt.timestamp.isoformat(),
|
||||
'successful': attempt.successful,
|
||||
'error_message': attempt.error_message,
|
||||
'duration_seconds': attempt.duration_seconds
|
||||
}
|
||||
for attempt in self.recovery_attempts
|
||||
],
|
||||
'severity': self.severity.value,
|
||||
'platform': self.platform,
|
||||
'session_id': self.session_id,
|
||||
'account_id': self.account_id,
|
||||
'correlation_id': self.correlation_id,
|
||||
'user_impact': self.user_impact,
|
||||
'system_impact': self.system_impact,
|
||||
'data_loss': self.data_loss,
|
||||
'recovery_success_rate': self.get_recovery_success_rate()
|
||||
}
|
||||
435
domain/entities/method_rotation.py
Normale Datei
435
domain/entities/method_rotation.py
Normale Datei
@ -0,0 +1,435 @@
|
||||
"""
|
||||
Domain entities for method rotation system.
|
||||
These entities represent the core business logic and rules for method rotation.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional, Any
|
||||
import uuid
|
||||
import json
|
||||
|
||||
|
||||
class RiskLevel(Enum):
|
||||
"""Risk levels for method strategies"""
|
||||
LOW = "LOW"
|
||||
MEDIUM = "MEDIUM"
|
||||
HIGH = "HIGH"
|
||||
|
||||
|
||||
class RotationEventType(Enum):
|
||||
"""Types of rotation events"""
|
||||
SUCCESS = "SUCCESS"
|
||||
FAILURE = "FAILURE"
|
||||
ROTATION = "ROTATION"
|
||||
COOLDOWN = "COOLDOWN"
|
||||
CONFIG_CHANGE = "CONFIG_CHANGE"
|
||||
EMERGENCY_MODE = "EMERGENCY_MODE"
|
||||
|
||||
|
||||
class RotationStrategy(Enum):
|
||||
"""Rotation strategy types"""
|
||||
SEQUENTIAL = "sequential" # Try methods in order
|
||||
RANDOM = "random" # Random method selection
|
||||
ADAPTIVE = "adaptive" # Learn from success patterns
|
||||
SMART = "smart" # AI-driven method selection
|
||||
|
||||
|
||||
@dataclass
|
||||
class MethodStrategy:
|
||||
"""
|
||||
Represents a registration/login method strategy for a platform.
|
||||
Contains configuration, performance metrics, and business rules.
|
||||
"""
|
||||
strategy_id: str
|
||||
platform: str
|
||||
method_name: str
|
||||
priority: int = 5 # 1-10, higher = preferred
|
||||
success_rate: float = 0.0
|
||||
failure_rate: float = 0.0
|
||||
last_success: Optional[datetime] = None
|
||||
last_failure: Optional[datetime] = None
|
||||
cooldown_period: int = 0 # seconds
|
||||
max_daily_attempts: int = 10
|
||||
risk_level: RiskLevel = RiskLevel.MEDIUM
|
||||
is_active: bool = True
|
||||
configuration: Dict[str, Any] = field(default_factory=dict)
|
||||
tags: List[str] = field(default_factory=list)
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
updated_at: datetime = field(default_factory=datetime.now)
|
||||
|
||||
def __post_init__(self):
|
||||
"""Validate and normalize data after initialization"""
|
||||
if not self.strategy_id:
|
||||
self.strategy_id = f"{self.platform}_{self.method_name}_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
# Ensure priority is within valid range
|
||||
self.priority = max(1, min(10, self.priority))
|
||||
|
||||
# Ensure rates are valid percentages
|
||||
self.success_rate = max(0.0, min(1.0, self.success_rate))
|
||||
self.failure_rate = max(0.0, min(1.0, self.failure_rate))
|
||||
|
||||
@property
|
||||
def is_on_cooldown(self) -> bool:
|
||||
"""Check if method is currently on cooldown"""
|
||||
if not self.last_failure or self.cooldown_period == 0:
|
||||
return False
|
||||
|
||||
cooldown_until = self.last_failure + timedelta(seconds=self.cooldown_period)
|
||||
return datetime.now() < cooldown_until
|
||||
|
||||
@property
|
||||
def cooldown_remaining_seconds(self) -> int:
|
||||
"""Get remaining cooldown time in seconds"""
|
||||
if not self.is_on_cooldown:
|
||||
return 0
|
||||
|
||||
cooldown_until = self.last_failure + timedelta(seconds=self.cooldown_period)
|
||||
remaining = cooldown_until - datetime.now()
|
||||
return max(0, int(remaining.total_seconds()))
|
||||
|
||||
@property
|
||||
def effectiveness_score(self) -> float:
|
||||
"""Calculate overall effectiveness score for method selection"""
|
||||
base_score = self.priority / 10.0
|
||||
|
||||
# Adjust for success rate
|
||||
if self.success_rate > 0:
|
||||
base_score *= (1 + self.success_rate)
|
||||
|
||||
# Penalize for high failure rate
|
||||
if self.failure_rate > 0.5:
|
||||
base_score *= (1 - self.failure_rate * 0.5)
|
||||
|
||||
# Penalize for high risk
|
||||
risk_penalties = {
|
||||
RiskLevel.LOW: 0.0,
|
||||
RiskLevel.MEDIUM: 0.1,
|
||||
RiskLevel.HIGH: 0.3
|
||||
}
|
||||
base_score *= (1 - risk_penalties.get(self.risk_level, 0.1))
|
||||
|
||||
# Penalize if on cooldown
|
||||
if self.is_on_cooldown:
|
||||
base_score *= 0.1
|
||||
|
||||
# Penalize if inactive
|
||||
if not self.is_active:
|
||||
base_score = 0.0
|
||||
|
||||
return max(0.0, min(1.0, base_score))
|
||||
|
||||
def update_performance(self, success: bool, execution_time: float = 0.0):
|
||||
"""Update performance metrics based on execution result"""
|
||||
self.updated_at = datetime.now()
|
||||
|
||||
if success:
|
||||
self.last_success = datetime.now()
|
||||
# Update success rate with exponential moving average
|
||||
self.success_rate = 0.8 * self.success_rate + 0.2 * 1.0
|
||||
self.failure_rate = 0.8 * self.failure_rate + 0.2 * 0.0
|
||||
else:
|
||||
self.last_failure = datetime.now()
|
||||
# Update failure rate with exponential moving average
|
||||
self.success_rate = 0.8 * self.success_rate + 0.2 * 0.0
|
||||
self.failure_rate = 0.8 * self.failure_rate + 0.2 * 1.0
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for serialization"""
|
||||
return {
|
||||
'strategy_id': self.strategy_id,
|
||||
'platform': self.platform,
|
||||
'method_name': self.method_name,
|
||||
'priority': self.priority,
|
||||
'success_rate': self.success_rate,
|
||||
'failure_rate': self.failure_rate,
|
||||
'last_success': self.last_success.isoformat() if self.last_success else None,
|
||||
'last_failure': self.last_failure.isoformat() if self.last_failure else None,
|
||||
'cooldown_period': self.cooldown_period,
|
||||
'max_daily_attempts': self.max_daily_attempts,
|
||||
'risk_level': self.risk_level.value,
|
||||
'is_active': self.is_active,
|
||||
'configuration': self.configuration,
|
||||
'tags': self.tags,
|
||||
'created_at': self.created_at.isoformat(),
|
||||
'updated_at': self.updated_at.isoformat()
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class RotationSession:
|
||||
"""
|
||||
Represents an active rotation session for account creation/login.
|
||||
Tracks the current state and history of method attempts.
|
||||
"""
|
||||
session_id: str
|
||||
platform: str
|
||||
account_id: Optional[str] = None
|
||||
current_method: str = ""
|
||||
attempted_methods: List[str] = field(default_factory=list)
|
||||
session_start: datetime = field(default_factory=datetime.now)
|
||||
last_rotation: Optional[datetime] = None
|
||||
rotation_count: int = 0
|
||||
success_count: int = 0
|
||||
failure_count: int = 0
|
||||
is_active: bool = True
|
||||
rotation_reason: Optional[str] = None
|
||||
fingerprint_id: Optional[str] = None
|
||||
session_metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self):
|
||||
"""Validate and normalize data after initialization"""
|
||||
if not self.session_id:
|
||||
self.session_id = f"session_{uuid.uuid4().hex}"
|
||||
|
||||
@property
|
||||
def session_duration(self) -> timedelta:
|
||||
"""Get total session duration"""
|
||||
return datetime.now() - self.session_start
|
||||
|
||||
@property
|
||||
def success_rate(self) -> float:
|
||||
"""Calculate session success rate"""
|
||||
total_attempts = self.success_count + self.failure_count
|
||||
if total_attempts == 0:
|
||||
return 0.0
|
||||
return self.success_count / total_attempts
|
||||
|
||||
@property
|
||||
def should_rotate(self) -> bool:
|
||||
"""Determine if rotation should occur based on failure patterns"""
|
||||
# Rotate after 2 consecutive failures
|
||||
if self.failure_count >= 2 and self.success_count == 0:
|
||||
return True
|
||||
|
||||
# Rotate if failure rate is high and we have alternatives
|
||||
if len(self.attempted_methods) < 3 and self.success_rate < 0.3:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def add_attempt(self, method_name: str, success: bool, error_message: Optional[str] = None):
|
||||
"""Record a method attempt"""
|
||||
if method_name not in self.attempted_methods:
|
||||
self.attempted_methods.append(method_name)
|
||||
|
||||
self.current_method = method_name
|
||||
|
||||
if success:
|
||||
self.success_count += 1
|
||||
else:
|
||||
self.failure_count += 1
|
||||
|
||||
# Add to metadata
|
||||
attempt_data = {
|
||||
'method': method_name,
|
||||
'success': success,
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'error': error_message
|
||||
}
|
||||
|
||||
if 'attempts' not in self.session_metadata:
|
||||
self.session_metadata['attempts'] = []
|
||||
self.session_metadata['attempts'].append(attempt_data)
|
||||
|
||||
def rotate_to_method(self, new_method: str, reason: str):
|
||||
"""Rotate to a new method"""
|
||||
self.current_method = new_method
|
||||
self.last_rotation = datetime.now()
|
||||
self.rotation_count += 1
|
||||
self.rotation_reason = reason
|
||||
|
||||
def complete_session(self, success: bool):
|
||||
"""Mark session as completed"""
|
||||
self.is_active = False
|
||||
self.session_metadata['completed_at'] = datetime.now().isoformat()
|
||||
self.session_metadata['final_success'] = success
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for serialization"""
|
||||
return {
|
||||
'session_id': self.session_id,
|
||||
'platform': self.platform,
|
||||
'account_id': self.account_id,
|
||||
'current_method': self.current_method,
|
||||
'attempted_methods': self.attempted_methods,
|
||||
'session_start': self.session_start.isoformat(),
|
||||
'last_rotation': self.last_rotation.isoformat() if self.last_rotation else None,
|
||||
'rotation_count': self.rotation_count,
|
||||
'success_count': self.success_count,
|
||||
'failure_count': self.failure_count,
|
||||
'is_active': self.is_active,
|
||||
'rotation_reason': self.rotation_reason,
|
||||
'fingerprint_id': self.fingerprint_id,
|
||||
'session_metadata': self.session_metadata
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class RotationEvent:
|
||||
"""
|
||||
Represents a specific event in the rotation system.
|
||||
Used for detailed logging and analytics.
|
||||
"""
|
||||
event_id: str
|
||||
session_id: str
|
||||
method_name: str
|
||||
event_type: RotationEventType
|
||||
timestamp: datetime = field(default_factory=datetime.now)
|
||||
details: Dict[str, Any] = field(default_factory=dict)
|
||||
error_message: Optional[str] = None
|
||||
performance_metrics: Dict[str, float] = field(default_factory=dict)
|
||||
correlation_id: Optional[str] = None
|
||||
|
||||
def __post_init__(self):
|
||||
"""Validate and normalize data after initialization"""
|
||||
if not self.event_id:
|
||||
self.event_id = f"event_{uuid.uuid4().hex}"
|
||||
|
||||
@classmethod
|
||||
def create_success_event(cls, session_id: str, method_name: str,
|
||||
execution_time: float = 0.0, **kwargs) -> 'RotationEvent':
|
||||
"""Create a success event"""
|
||||
return cls(
|
||||
event_id=f"success_{uuid.uuid4().hex[:8]}",
|
||||
session_id=session_id,
|
||||
method_name=method_name,
|
||||
event_type=RotationEventType.SUCCESS,
|
||||
performance_metrics={'execution_time': execution_time},
|
||||
details=kwargs
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create_failure_event(cls, session_id: str, method_name: str,
|
||||
error_message: str, **kwargs) -> 'RotationEvent':
|
||||
"""Create a failure event"""
|
||||
return cls(
|
||||
event_id=f"failure_{uuid.uuid4().hex[:8]}",
|
||||
session_id=session_id,
|
||||
method_name=method_name,
|
||||
event_type=RotationEventType.FAILURE,
|
||||
error_message=error_message,
|
||||
details=kwargs
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create_rotation_event(cls, session_id: str, from_method: str,
|
||||
to_method: str, reason: str, **kwargs) -> 'RotationEvent':
|
||||
"""Create a rotation event"""
|
||||
return cls(
|
||||
event_id=f"rotation_{uuid.uuid4().hex[:8]}",
|
||||
session_id=session_id,
|
||||
method_name=to_method,
|
||||
event_type=RotationEventType.ROTATION,
|
||||
details={
|
||||
'from_method': from_method,
|
||||
'to_method': to_method,
|
||||
'reason': reason,
|
||||
**kwargs
|
||||
}
|
||||
)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for serialization"""
|
||||
return {
|
||||
'event_id': self.event_id,
|
||||
'session_id': self.session_id,
|
||||
'method_name': self.method_name,
|
||||
'event_type': self.event_type.value,
|
||||
'timestamp': self.timestamp.isoformat(),
|
||||
'details': self.details,
|
||||
'error_message': self.error_message,
|
||||
'performance_metrics': self.performance_metrics,
|
||||
'correlation_id': self.correlation_id
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlatformMethodState:
|
||||
"""
|
||||
Represents the rotation state for a specific platform.
|
||||
Tracks preferences, blocks, and daily limits.
|
||||
"""
|
||||
platform: str
|
||||
last_successful_method: Optional[str] = None
|
||||
last_successful_at: Optional[datetime] = None
|
||||
preferred_methods: List[str] = field(default_factory=list)
|
||||
blocked_methods: List[str] = field(default_factory=list)
|
||||
daily_attempt_counts: Dict[str, int] = field(default_factory=dict)
|
||||
reset_date: datetime = field(default_factory=lambda: datetime.now().replace(hour=0, minute=0, second=0, microsecond=0))
|
||||
rotation_strategy: RotationStrategy = RotationStrategy.ADAPTIVE
|
||||
emergency_mode: bool = False
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
updated_at: datetime = field(default_factory=datetime.now)
|
||||
|
||||
def is_method_available(self, method_name: str, max_daily_attempts: int) -> bool:
|
||||
"""Check if a method is available for use"""
|
||||
# Check if method is blocked
|
||||
if method_name in self.blocked_methods:
|
||||
return False
|
||||
|
||||
# Check daily limits
|
||||
current_attempts = self.daily_attempt_counts.get(method_name, 0)
|
||||
if current_attempts >= max_daily_attempts:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def increment_daily_attempts(self, method_name: str):
|
||||
"""Increment daily attempt count for a method"""
|
||||
# Reset counts if it's a new day
|
||||
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
if today > self.reset_date:
|
||||
self.daily_attempt_counts = {}
|
||||
self.reset_date = today
|
||||
|
||||
self.daily_attempt_counts[method_name] = self.daily_attempt_counts.get(method_name, 0) + 1
|
||||
self.updated_at = datetime.now()
|
||||
|
||||
def record_success(self, method_name: str):
|
||||
"""Record a successful method execution"""
|
||||
self.last_successful_method = method_name
|
||||
self.last_successful_at = datetime.now()
|
||||
self.updated_at = datetime.now()
|
||||
|
||||
# Move successful method to front of preferred list
|
||||
if method_name in self.preferred_methods:
|
||||
self.preferred_methods.remove(method_name)
|
||||
self.preferred_methods.insert(0, method_name)
|
||||
|
||||
def block_method(self, method_name: str, reason: str):
|
||||
"""Temporarily block a method"""
|
||||
if method_name not in self.blocked_methods:
|
||||
self.blocked_methods.append(method_name)
|
||||
|
||||
self.metadata[f'block_reason_{method_name}'] = reason
|
||||
self.metadata[f'blocked_at_{method_name}'] = datetime.now().isoformat()
|
||||
self.updated_at = datetime.now()
|
||||
|
||||
def unblock_method(self, method_name: str):
|
||||
"""Remove method from blocked list"""
|
||||
if method_name in self.blocked_methods:
|
||||
self.blocked_methods.remove(method_name)
|
||||
|
||||
# Clean up metadata
|
||||
self.metadata.pop(f'block_reason_{method_name}', None)
|
||||
self.metadata.pop(f'blocked_at_{method_name}', None)
|
||||
self.updated_at = datetime.now()
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for serialization"""
|
||||
return {
|
||||
'platform': self.platform,
|
||||
'last_successful_method': self.last_successful_method,
|
||||
'last_successful_at': self.last_successful_at.isoformat() if self.last_successful_at else None,
|
||||
'preferred_methods': self.preferred_methods,
|
||||
'blocked_methods': self.blocked_methods,
|
||||
'daily_attempt_counts': self.daily_attempt_counts,
|
||||
'reset_date': self.reset_date.isoformat(),
|
||||
'rotation_strategy': self.rotation_strategy.value,
|
||||
'emergency_mode': self.emergency_mode,
|
||||
'metadata': self.metadata,
|
||||
'updated_at': self.updated_at.isoformat()
|
||||
}
|
||||
36
domain/entities/rate_limit_policy.py
Normale Datei
36
domain/entities/rate_limit_policy.py
Normale Datei
@ -0,0 +1,36 @@
|
||||
"""
|
||||
Rate Limit Policy Entity - Definiert Geschwindigkeitsregeln für verschiedene Aktionen
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class RateLimitPolicy:
|
||||
"""Definiert Geschwindigkeitsregeln für verschiedene Aktionen"""
|
||||
|
||||
min_delay: float
|
||||
max_delay: float
|
||||
adaptive: bool = True
|
||||
backoff_multiplier: float = 1.5
|
||||
max_retries: int = 3
|
||||
|
||||
def __post_init__(self):
|
||||
"""Validierung der Policy-Parameter"""
|
||||
if self.min_delay < 0:
|
||||
raise ValueError("min_delay muss >= 0 sein")
|
||||
if self.max_delay < self.min_delay:
|
||||
raise ValueError("max_delay muss >= min_delay sein")
|
||||
if self.backoff_multiplier < 1.0:
|
||||
raise ValueError("backoff_multiplier muss >= 1.0 sein")
|
||||
if self.max_retries < 0:
|
||||
raise ValueError("max_retries muss >= 0 sein")
|
||||
|
||||
def calculate_backoff_delay(self, attempt: int) -> float:
|
||||
"""Berechnet Verzögerung basierend auf Versuchsnummer"""
|
||||
if not self.adaptive:
|
||||
return self.min_delay
|
||||
|
||||
delay = self.min_delay * (self.backoff_multiplier ** attempt)
|
||||
return min(delay, self.max_delay)
|
||||
96
domain/exceptions.py
Normale Datei
96
domain/exceptions.py
Normale Datei
@ -0,0 +1,96 @@
|
||||
"""
|
||||
Domain-spezifische Exceptions für AccountForger
|
||||
"""
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
|
||||
class AccountForgerException(Exception):
|
||||
"""Basis-Exception für alle AccountForger-spezifischen Fehler"""
|
||||
|
||||
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
self.details = details or {}
|
||||
|
||||
|
||||
class AccountCreationException(AccountForgerException):
|
||||
"""Fehler bei Account-Erstellung"""
|
||||
|
||||
def __init__(self, message: str, platform: Optional[str] = None,
|
||||
error_type: Optional[str] = None, recovery_suggestion: Optional[str] = None):
|
||||
details = {
|
||||
"platform": platform,
|
||||
"error_type": error_type,
|
||||
"recovery_suggestion": recovery_suggestion
|
||||
}
|
||||
super().__init__(message, details)
|
||||
self.platform = platform
|
||||
self.error_type = error_type
|
||||
self.recovery_suggestion = recovery_suggestion
|
||||
|
||||
@property
|
||||
def user_friendly_message(self) -> str:
|
||||
"""Gibt eine benutzerfreundliche Fehlermeldung zurück"""
|
||||
if self.recovery_suggestion:
|
||||
return f"{self.message}\n\nLösungsvorschlag: {self.recovery_suggestion}"
|
||||
return self.message
|
||||
|
||||
|
||||
class FingerprintException(AccountForgerException):
|
||||
"""Fehler bei Fingerprint-Operationen"""
|
||||
pass
|
||||
|
||||
|
||||
class SessionException(AccountForgerException):
|
||||
"""Fehler bei Session-Operationen"""
|
||||
pass
|
||||
|
||||
|
||||
class RateLimitException(AccountCreationException):
|
||||
"""Rate-Limit wurde erreicht"""
|
||||
|
||||
def __init__(self, platform: str, retry_after: Optional[int] = None):
|
||||
message = f"Zu viele Anfragen an {platform}"
|
||||
recovery = f"Bitte warten Sie {retry_after} Sekunden" if retry_after else "Bitte warten Sie einige Minuten"
|
||||
super().__init__(
|
||||
message=message,
|
||||
platform=platform,
|
||||
error_type="rate_limit",
|
||||
recovery_suggestion=recovery
|
||||
)
|
||||
self.retry_after = retry_after
|
||||
|
||||
|
||||
class CaptchaRequiredException(AccountCreationException):
|
||||
"""Captcha-Verifizierung erforderlich"""
|
||||
|
||||
def __init__(self, platform: str):
|
||||
super().__init__(
|
||||
message=f"{platform} erfordert Captcha-Verifizierung",
|
||||
platform=platform,
|
||||
error_type="captcha",
|
||||
recovery_suggestion="Versuchen Sie es später erneut oder nutzen Sie einen anderen Proxy"
|
||||
)
|
||||
|
||||
|
||||
class ValidationException(AccountForgerException):
|
||||
"""Validierungsfehler"""
|
||||
|
||||
def __init__(self, field: str, message: str):
|
||||
super().__init__(f"Validierungsfehler bei {field}: {message}")
|
||||
self.field = field
|
||||
|
||||
|
||||
class ProxyException(AccountForgerException):
|
||||
"""Proxy-bezogene Fehler"""
|
||||
pass
|
||||
|
||||
|
||||
class NetworkException(AccountForgerException):
|
||||
"""Netzwerk-bezogene Fehler"""
|
||||
|
||||
def __init__(self, message: str = "Netzwerkfehler aufgetreten"):
|
||||
super().__init__(
|
||||
message=message,
|
||||
details={"recovery_suggestion": "Überprüfen Sie Ihre Internetverbindung"}
|
||||
)
|
||||
17
domain/repositories/__init__.py
Normale Datei
17
domain/repositories/__init__.py
Normale Datei
@ -0,0 +1,17 @@
|
||||
"""
|
||||
Domain repository interfaces.
|
||||
|
||||
These interfaces define the contracts for data persistence,
|
||||
following the Dependency Inversion Principle.
|
||||
Infrastructure layer will implement these interfaces.
|
||||
"""
|
||||
|
||||
from .fingerprint_repository import IFingerprintRepository
|
||||
from .analytics_repository import IAnalyticsRepository
|
||||
from .rate_limit_repository import IRateLimitRepository
|
||||
|
||||
__all__ = [
|
||||
'IFingerprintRepository',
|
||||
'IAnalyticsRepository',
|
||||
'IRateLimitRepository'
|
||||
]
|
||||
79
domain/repositories/analytics_repository.py
Normale Datei
79
domain/repositories/analytics_repository.py
Normale Datei
@ -0,0 +1,79 @@
|
||||
"""
|
||||
Analytics repository interface.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Dict, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from domain.entities.account_creation_event import AccountCreationEvent
|
||||
from domain.entities.error_event import ErrorEvent
|
||||
from domain.value_objects.error_summary import ErrorSummary
|
||||
|
||||
|
||||
class IAnalyticsRepository(ABC):
|
||||
"""Interface for analytics data persistence."""
|
||||
|
||||
@abstractmethod
|
||||
def save_account_event(self, event: AccountCreationEvent) -> None:
|
||||
"""Save an account creation event."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def save_error_event(self, event: ErrorEvent) -> None:
|
||||
"""Save an error event."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_account_events(self, platform: str = None,
|
||||
start_date: datetime = None,
|
||||
end_date: datetime = None) -> List[AccountCreationEvent]:
|
||||
"""Get account creation events with optional filters."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_error_events(self, platform: str = None,
|
||||
error_type: str = None,
|
||||
start_date: datetime = None,
|
||||
end_date: datetime = None) -> List[ErrorEvent]:
|
||||
"""Get error events with optional filters."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_success_rate(self, platform: str = None,
|
||||
start_date: datetime = None,
|
||||
end_date: datetime = None) -> float:
|
||||
"""Calculate success rate for account creation."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_error_summary(self, platform: str = None,
|
||||
days: int = 7) -> ErrorSummary:
|
||||
"""Get error summary for specified period."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_platform_statistics(self) -> Dict[str, Dict[str, int]]:
|
||||
"""Get statistics grouped by platform."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_hourly_distribution(self, platform: str = None,
|
||||
days: int = 7) -> Dict[int, int]:
|
||||
"""Get hourly distribution of account creation."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_fingerprint_performance(self, fingerprint_id: str) -> Dict[str, any]:
|
||||
"""Get performance metrics for a specific fingerprint."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_proxy_performance(self, days: int = 7) -> Dict[str, Dict[str, int]]:
|
||||
"""Get proxy performance metrics."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def cleanup_old_events(self, days_to_keep: int = 30) -> int:
|
||||
"""Remove events older than specified days. Returns count deleted."""
|
||||
pass
|
||||
63
domain/repositories/fingerprint_repository.py
Normale Datei
63
domain/repositories/fingerprint_repository.py
Normale Datei
@ -0,0 +1,63 @@
|
||||
"""
|
||||
Fingerprint repository interface.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
|
||||
from domain.entities.browser_fingerprint import BrowserFingerprint
|
||||
|
||||
|
||||
class IFingerprintRepository(ABC):
|
||||
"""Interface for fingerprint persistence."""
|
||||
|
||||
@abstractmethod
|
||||
def save(self, fingerprint: BrowserFingerprint) -> str:
|
||||
"""Save a fingerprint and return its ID."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_by_id(self, fingerprint_id: str) -> Optional[BrowserFingerprint]:
|
||||
"""Find a fingerprint by ID."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_by_account_id(self, account_id: str) -> Optional[BrowserFingerprint]:
|
||||
"""Find a fingerprint associated with an account."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_all(self) -> List[BrowserFingerprint]:
|
||||
"""Find all fingerprints."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update(self, fingerprint: BrowserFingerprint) -> bool:
|
||||
"""Update an existing fingerprint."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete(self, fingerprint_id: str) -> bool:
|
||||
"""Delete a fingerprint by ID."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_by_platform(self, platform: str) -> List[BrowserFingerprint]:
|
||||
"""Find all fingerprints for a specific platform."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def exists(self, fingerprint_id: str) -> bool:
|
||||
"""Check if a fingerprint exists."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def count(self) -> int:
|
||||
"""Count total fingerprints."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_recent(self, limit: int = 10) -> List[BrowserFingerprint]:
|
||||
"""Find most recently created fingerprints."""
|
||||
pass
|
||||
310
domain/repositories/method_rotation_repository.py
Normale Datei
310
domain/repositories/method_rotation_repository.py
Normale Datei
@ -0,0 +1,310 @@
|
||||
"""
|
||||
Repository interfaces for method rotation system.
|
||||
These interfaces define the contracts for data access without implementation details.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime, date
|
||||
from typing import List, Optional, Dict, Any
|
||||
from domain.entities.method_rotation import (
|
||||
MethodStrategy, RotationSession, RotationEvent, PlatformMethodState
|
||||
)
|
||||
|
||||
|
||||
class IMethodStrategyRepository(ABC):
|
||||
"""Interface for method strategy data access"""
|
||||
|
||||
@abstractmethod
|
||||
def save(self, strategy: MethodStrategy) -> None:
|
||||
"""Save or update a method strategy"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_by_id(self, strategy_id: str) -> Optional[MethodStrategy]:
|
||||
"""Find a strategy by its ID"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_by_platform(self, platform: str) -> List[MethodStrategy]:
|
||||
"""Find all strategies for a platform"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_active_by_platform(self, platform: str) -> List[MethodStrategy]:
|
||||
"""Find all active strategies for a platform, ordered by effectiveness"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_by_platform_and_method(self, platform: str, method_name: str) -> Optional[MethodStrategy]:
|
||||
"""Find a specific method strategy"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update_performance_metrics(self, strategy_id: str, success: bool,
|
||||
execution_time: float = 0.0) -> None:
|
||||
"""Update performance metrics for a strategy"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_next_available_method(self, platform: str,
|
||||
excluded_methods: List[str] = None,
|
||||
max_risk_level: str = "HIGH") -> Optional[MethodStrategy]:
|
||||
"""Get the next best available method for a platform"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def disable_method(self, platform: str, method_name: str, reason: str) -> None:
|
||||
"""Disable a method temporarily or permanently"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def enable_method(self, platform: str, method_name: str) -> None:
|
||||
"""Re-enable a disabled method"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_platform_statistics(self, platform: str) -> Dict[str, Any]:
|
||||
"""Get aggregated statistics for all methods on a platform"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def cleanup_old_data(self, days_to_keep: int = 90) -> int:
|
||||
"""Clean up old performance data and return number of records removed"""
|
||||
pass
|
||||
|
||||
|
||||
class IRotationSessionRepository(ABC):
|
||||
"""Interface for rotation session data access"""
|
||||
|
||||
@abstractmethod
|
||||
def save(self, session: RotationSession) -> None:
|
||||
"""Save or update a rotation session"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_by_id(self, session_id: str) -> Optional[RotationSession]:
|
||||
"""Find a session by its ID"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_active_session(self, platform: str, account_id: Optional[str] = None) -> Optional[RotationSession]:
|
||||
"""Find an active session for a platform/account"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_active_sessions_by_platform(self, platform: str) -> List[RotationSession]:
|
||||
"""Find all active sessions for a platform"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update_session_metrics(self, session_id: str, success: bool,
|
||||
method_name: str, error_message: Optional[str] = None) -> None:
|
||||
"""Update session metrics after a method attempt"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def archive_session(self, session_id: str, final_success: bool = False) -> None:
|
||||
"""Mark a session as completed/archived"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_session_history(self, platform: str, limit: int = 100) -> List[RotationSession]:
|
||||
"""Get recent session history for a platform"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_session_statistics(self, platform: str, days: int = 30) -> Dict[str, Any]:
|
||||
"""Get session statistics for a platform over specified days"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def cleanup_old_sessions(self, days_to_keep: int = 30) -> int:
|
||||
"""Clean up old session data and return number of records removed"""
|
||||
pass
|
||||
|
||||
|
||||
class IRotationEventRepository(ABC):
|
||||
"""Interface for rotation event data access"""
|
||||
|
||||
@abstractmethod
|
||||
def save(self, event: RotationEvent) -> None:
|
||||
"""Save a rotation event"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def save_batch(self, events: List[RotationEvent]) -> None:
|
||||
"""Save multiple events in a batch for performance"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_by_session(self, session_id: str) -> List[RotationEvent]:
|
||||
"""Find all events for a specific session"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_by_method(self, platform: str, method_name: str,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None) -> List[RotationEvent]:
|
||||
"""Find events for a specific method within date range"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_recent_failures(self, platform: str, method_name: str,
|
||||
hours: int = 24) -> List[RotationEvent]:
|
||||
"""Find recent failure events for a method"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_event_statistics(self, platform: str,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None) -> Dict[str, Any]:
|
||||
"""Get event statistics for analysis"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_error_patterns(self, platform: str, method_name: str,
|
||||
days: int = 7) -> Dict[str, int]:
|
||||
"""Get error patterns for a method to identify common issues"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def cleanup_old_events(self, days_to_keep: int = 90) -> int:
|
||||
"""Clean up old event data and return number of records removed"""
|
||||
pass
|
||||
|
||||
|
||||
class IPlatformMethodStateRepository(ABC):
|
||||
"""Interface for platform method state data access"""
|
||||
|
||||
@abstractmethod
|
||||
def save(self, state: PlatformMethodState) -> None:
|
||||
"""Save or update platform method state"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_by_platform(self, platform: str) -> Optional[PlatformMethodState]:
|
||||
"""Find method state for a platform"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_or_create_state(self, platform: str) -> PlatformMethodState:
|
||||
"""Get existing state or create new one with defaults"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update_daily_attempts(self, platform: str, method_name: str) -> None:
|
||||
"""Increment daily attempt counter for a method"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def reset_daily_counters(self, platform: str) -> None:
|
||||
"""Reset daily attempt counters (typically called at midnight)"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def block_method(self, platform: str, method_name: str, reason: str) -> None:
|
||||
"""Block a method temporarily"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def unblock_method(self, platform: str, method_name: str) -> None:
|
||||
"""Unblock a previously blocked method"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def record_method_success(self, platform: str, method_name: str) -> None:
|
||||
"""Record successful method execution"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_preferred_method_order(self, platform: str) -> List[str]:
|
||||
"""Get preferred method order for a platform"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_emergency_mode(self, platform: str, enabled: bool) -> None:
|
||||
"""Enable/disable emergency mode for a platform"""
|
||||
pass
|
||||
|
||||
|
||||
class IMethodPerformanceRepository(ABC):
|
||||
"""Interface for method performance analytics data access"""
|
||||
|
||||
@abstractmethod
|
||||
def record_daily_performance(self, platform: str, method_name: str,
|
||||
success: bool, execution_time: float = 0.0) -> None:
|
||||
"""Record performance data for daily aggregation"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_daily_performance(self, platform: str, method_name: str,
|
||||
start_date: date, end_date: date) -> List[Dict[str, Any]]:
|
||||
"""Get daily performance data for a method within date range"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_method_trends(self, platform: str, days: int = 30) -> Dict[str, Any]:
|
||||
"""Get performance trends for all methods on a platform"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_success_rate_history(self, platform: str, method_name: str,
|
||||
days: int = 30) -> List[Dict[str, Any]]:
|
||||
"""Get success rate history for trend analysis"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_peak_usage_patterns(self, platform: str, method_name: str) -> Dict[str, Any]:
|
||||
"""Get usage patterns to identify peak hours and optimize timing"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def aggregate_daily_stats(self, target_date: date) -> int:
|
||||
"""Aggregate raw performance data into daily statistics"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def cleanup_old_performance_data(self, days_to_keep: int = 365) -> int:
|
||||
"""Clean up old performance data and return number of records removed"""
|
||||
pass
|
||||
|
||||
|
||||
class IMethodCooldownRepository(ABC):
|
||||
"""Interface for method cooldown data access"""
|
||||
|
||||
@abstractmethod
|
||||
def add_cooldown(self, platform: str, method_name: str,
|
||||
cooldown_until: datetime, reason: str) -> None:
|
||||
"""Add a cooldown period for a method"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def remove_cooldown(self, platform: str, method_name: str) -> None:
|
||||
"""Remove cooldown for a method"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def is_method_on_cooldown(self, platform: str, method_name: str) -> bool:
|
||||
"""Check if a method is currently on cooldown"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_cooldown_info(self, platform: str, method_name: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get cooldown information for a method"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_active_cooldowns(self, platform: str) -> List[Dict[str, Any]]:
|
||||
"""Get all active cooldowns for a platform"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def cleanup_expired_cooldowns(self) -> int:
|
||||
"""Remove expired cooldowns and return number of records removed"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def extend_cooldown(self, platform: str, method_name: str,
|
||||
additional_seconds: int) -> None:
|
||||
"""Extend existing cooldown period"""
|
||||
pass
|
||||
75
domain/repositories/rate_limit_repository.py
Normale Datei
75
domain/repositories/rate_limit_repository.py
Normale Datei
@ -0,0 +1,75 @@
|
||||
"""
|
||||
Rate limit repository interface.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional, List, Dict
|
||||
from datetime import datetime
|
||||
|
||||
from domain.entities.rate_limit_policy import RateLimitPolicy
|
||||
|
||||
|
||||
class IRateLimitRepository(ABC):
|
||||
"""Interface for rate limit data persistence."""
|
||||
|
||||
@abstractmethod
|
||||
def save_policy(self, policy: RateLimitPolicy) -> None:
|
||||
"""Save or update a rate limit policy."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_policy(self, platform: str, action: str) -> Optional[RateLimitPolicy]:
|
||||
"""Get rate limit policy for platform and action."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_all_policies(self, platform: str = None) -> List[RateLimitPolicy]:
|
||||
"""Get all policies, optionally filtered by platform."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def record_action(self, platform: str, action: str,
|
||||
success: bool = True, proxy: str = None) -> None:
|
||||
"""Record an action for rate limit tracking."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_action_count(self, platform: str, action: str,
|
||||
window_minutes: int = 60,
|
||||
proxy: str = None) -> int:
|
||||
"""Get action count within time window."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_recent_actions(self, platform: str, action: str,
|
||||
limit: int = 100) -> List[Dict[str, any]]:
|
||||
"""Get recent actions for analysis."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def is_rate_limited(self, platform: str, action: str,
|
||||
proxy: str = None) -> bool:
|
||||
"""Check if action is currently rate limited."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_wait_time(self, platform: str, action: str,
|
||||
proxy: str = None) -> int:
|
||||
"""Get wait time in seconds before next action allowed."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def reset_limits(self, platform: str = None, action: str = None,
|
||||
proxy: str = None) -> int:
|
||||
"""Reset rate limits. Returns count of records affected."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_limit_status(self, platform: str) -> Dict[str, Dict[str, any]]:
|
||||
"""Get current rate limit status for all actions on platform."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def cleanup_old_records(self, days_to_keep: int = 7) -> int:
|
||||
"""Remove old rate limit records. Returns count deleted."""
|
||||
pass
|
||||
13
domain/services/__init__.py
Normale Datei
13
domain/services/__init__.py
Normale Datei
@ -0,0 +1,13 @@
|
||||
"""
|
||||
Domain Services - Geschäftslogik-Interfaces die nicht in Entities gehören
|
||||
"""
|
||||
|
||||
from .rate_limit_service import IRateLimitService
|
||||
from .fingerprint_service import IFingerprintService
|
||||
from .analytics_service import IAnalyticsService
|
||||
|
||||
__all__ = [
|
||||
'IRateLimitService',
|
||||
'IFingerprintService',
|
||||
'IAnalyticsService'
|
||||
]
|
||||
181
domain/services/analytics_service.py
Normale Datei
181
domain/services/analytics_service.py
Normale Datei
@ -0,0 +1,181 @@
|
||||
"""
|
||||
Analytics Service Interface - Domain Service für Analytics und Reporting
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Optional, Dict, Any, Union
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from domain.entities.account_creation_event import AccountCreationEvent
|
||||
from domain.entities.error_event import ErrorEvent
|
||||
from domain.value_objects.error_summary import ErrorSummary
|
||||
from domain.value_objects.report import Report, ReportType
|
||||
|
||||
|
||||
class IAnalyticsService(ABC):
|
||||
"""
|
||||
Interface für Analytics Service.
|
||||
Definiert die Geschäftslogik für Event-Tracking und Reporting.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def log_event(self, event: Union[AccountCreationEvent, ErrorEvent, Any]) -> None:
|
||||
"""
|
||||
Loggt ein Event für spätere Analyse.
|
||||
|
||||
Args:
|
||||
event: Zu loggendes Event
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_success_rate(self,
|
||||
timeframe: Optional[timedelta] = None,
|
||||
platform: Optional[str] = None) -> float:
|
||||
"""
|
||||
Berechnet die Erfolgsrate für Account-Erstellung.
|
||||
|
||||
Args:
|
||||
timeframe: Optional - Zeitrahmen für Berechnung
|
||||
platform: Optional - Spezifische Plattform
|
||||
|
||||
Returns:
|
||||
Erfolgsrate zwischen 0.0 und 1.0
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_common_errors(self,
|
||||
limit: int = 10,
|
||||
timeframe: Optional[timedelta] = None) -> List[ErrorSummary]:
|
||||
"""
|
||||
Holt die häufigsten Fehler.
|
||||
|
||||
Args:
|
||||
limit: Maximale Anzahl von Fehlern
|
||||
timeframe: Optional - Zeitrahmen für Analyse
|
||||
|
||||
Returns:
|
||||
Liste von Fehler-Zusammenfassungen
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def generate_report(self,
|
||||
report_type: ReportType,
|
||||
start: datetime,
|
||||
end: datetime,
|
||||
platforms: Optional[List[str]] = None) -> Report:
|
||||
"""
|
||||
Generiert einen detaillierten Report.
|
||||
|
||||
Args:
|
||||
report_type: Typ des Reports
|
||||
start: Startdatum
|
||||
end: Enddatum
|
||||
platforms: Optional - Filter für spezifische Plattformen
|
||||
|
||||
Returns:
|
||||
Generierter Report
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_real_time_metrics(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Holt Echtzeit-Metriken für Dashboard.
|
||||
|
||||
Returns:
|
||||
Dictionary mit aktuellen Metriken
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def track_performance(self,
|
||||
metric_name: str,
|
||||
value: float,
|
||||
tags: Optional[Dict[str, str]] = None) -> None:
|
||||
"""
|
||||
Trackt eine Performance-Metrik.
|
||||
|
||||
Args:
|
||||
metric_name: Name der Metrik
|
||||
value: Wert der Metrik
|
||||
tags: Optional - Zusätzliche Tags
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_account_creation_timeline(self,
|
||||
hours: int = 24,
|
||||
platform: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Holt Timeline der Account-Erstellungen.
|
||||
|
||||
Args:
|
||||
hours: Anzahl Stunden zurück
|
||||
platform: Optional - Spezifische Plattform
|
||||
|
||||
Returns:
|
||||
Timeline-Daten für Visualisierung
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def analyze_failure_patterns(self,
|
||||
timeframe: Optional[timedelta] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Analysiert Muster in Fehlern.
|
||||
|
||||
Args:
|
||||
timeframe: Optional - Zeitrahmen für Analyse
|
||||
|
||||
Returns:
|
||||
Dictionary mit Fehler-Mustern und Insights
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_platform_comparison(self,
|
||||
timeframe: Optional[timedelta] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Vergleicht Performance zwischen Plattformen.
|
||||
|
||||
Args:
|
||||
timeframe: Optional - Zeitrahmen für Vergleich
|
||||
|
||||
Returns:
|
||||
Dictionary mit Plattform-Vergleichsdaten
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def export_data(self,
|
||||
format: str = "json",
|
||||
start: Optional[datetime] = None,
|
||||
end: Optional[datetime] = None) -> bytes:
|
||||
"""
|
||||
Exportiert Analytics-Daten.
|
||||
|
||||
Args:
|
||||
format: Export-Format ("json", "csv", "excel")
|
||||
start: Optional - Startdatum
|
||||
end: Optional - Enddatum
|
||||
|
||||
Returns:
|
||||
Exportierte Daten als Bytes
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def cleanup_old_events(self, older_than: datetime) -> int:
|
||||
"""
|
||||
Bereinigt alte Events.
|
||||
|
||||
Args:
|
||||
older_than: Lösche Events älter als dieses Datum
|
||||
|
||||
Returns:
|
||||
Anzahl gelöschter Events
|
||||
"""
|
||||
pass
|
||||
152
domain/services/fingerprint_service.py
Normale Datei
152
domain/services/fingerprint_service.py
Normale Datei
@ -0,0 +1,152 @@
|
||||
"""
|
||||
Fingerprint Service Interface - Domain Service für Browser Fingerprinting
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
from domain.entities.browser_fingerprint import BrowserFingerprint
|
||||
|
||||
|
||||
class IFingerprintService(ABC):
|
||||
"""
|
||||
Interface für Fingerprint Service.
|
||||
Definiert die Geschäftslogik für Browser Fingerprint Management.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def generate_fingerprint(self,
|
||||
profile_type: Optional[str] = None,
|
||||
platform: Optional[str] = None) -> BrowserFingerprint:
|
||||
"""
|
||||
Generiert einen neuen, realistischen Browser-Fingerprint.
|
||||
|
||||
Args:
|
||||
profile_type: Optional - Typ des Profils (z.B. "mobile", "desktop")
|
||||
platform: Optional - Zielplattform (z.B. "instagram", "tiktok")
|
||||
|
||||
Returns:
|
||||
Neuer Browser-Fingerprint
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def rotate_fingerprint(self,
|
||||
current: BrowserFingerprint,
|
||||
rotation_strategy: str = "gradual") -> BrowserFingerprint:
|
||||
"""
|
||||
Rotiert einen bestehenden Fingerprint für mehr Anonymität.
|
||||
|
||||
Args:
|
||||
current: Aktueller Fingerprint
|
||||
rotation_strategy: Strategie für Rotation ("gradual", "complete", "minimal")
|
||||
|
||||
Returns:
|
||||
Neuer rotierter Fingerprint
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def validate_fingerprint(self, fingerprint: BrowserFingerprint) -> tuple[bool, List[str]]:
|
||||
"""
|
||||
Validiert einen Fingerprint auf Konsistenz und Realismus.
|
||||
|
||||
Args:
|
||||
fingerprint: Zu validierender Fingerprint
|
||||
|
||||
Returns:
|
||||
Tuple aus (ist_valide, liste_von_problemen)
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def save_fingerprint(self, fingerprint: BrowserFingerprint) -> None:
|
||||
"""
|
||||
Speichert einen Fingerprint für spätere Verwendung.
|
||||
|
||||
Args:
|
||||
fingerprint: Zu speichernder Fingerprint
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def load_fingerprint(self, fingerprint_id: str) -> Optional[BrowserFingerprint]:
|
||||
"""
|
||||
Lädt einen gespeicherten Fingerprint.
|
||||
|
||||
Args:
|
||||
fingerprint_id: ID des Fingerprints
|
||||
|
||||
Returns:
|
||||
Fingerprint oder None wenn nicht gefunden
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_fingerprint_pool(self,
|
||||
count: int = 10,
|
||||
platform: Optional[str] = None) -> List[BrowserFingerprint]:
|
||||
"""
|
||||
Holt einen Pool von Fingerprints für Rotation.
|
||||
|
||||
Args:
|
||||
count: Anzahl der gewünschten Fingerprints
|
||||
platform: Optional - Filter für spezifische Plattform
|
||||
|
||||
Returns:
|
||||
Liste von Fingerprints
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def apply_fingerprint(self,
|
||||
browser_context: Any,
|
||||
fingerprint: BrowserFingerprint) -> None:
|
||||
"""
|
||||
Wendet einen Fingerprint auf einen Browser-Kontext an.
|
||||
|
||||
Args:
|
||||
browser_context: Playwright Browser Context
|
||||
fingerprint: Anzuwendender Fingerprint
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def detect_fingerprinting(self, page_content: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Erkennt Fingerprinting-Versuche auf einer Webseite.
|
||||
|
||||
Args:
|
||||
page_content: HTML oder JavaScript Content der Seite
|
||||
|
||||
Returns:
|
||||
Dictionary mit erkannten Fingerprinting-Techniken
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_fingerprint_score(self, fingerprint: BrowserFingerprint) -> float:
|
||||
"""
|
||||
Bewertet die Qualität/Einzigartigkeit eines Fingerprints.
|
||||
|
||||
Args:
|
||||
fingerprint: Zu bewertender Fingerprint
|
||||
|
||||
Returns:
|
||||
Score zwischen 0.0 (schlecht) und 1.0 (gut)
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def cleanup_old_fingerprints(self, older_than: datetime) -> int:
|
||||
"""
|
||||
Bereinigt alte, nicht mehr verwendete Fingerprints.
|
||||
|
||||
Args:
|
||||
older_than: Lösche Fingerprints älter als dieses Datum
|
||||
|
||||
Returns:
|
||||
Anzahl gelöschter Fingerprints
|
||||
"""
|
||||
pass
|
||||
125
domain/services/rate_limit_service.py
Normale Datei
125
domain/services/rate_limit_service.py
Normale Datei
@ -0,0 +1,125 @@
|
||||
"""
|
||||
Rate Limit Service Interface - Domain Service für Rate Limiting
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from domain.value_objects.action_timing import ActionTiming, ActionType
|
||||
from domain.entities.rate_limit_policy import RateLimitPolicy
|
||||
|
||||
|
||||
class IRateLimitService(ABC):
|
||||
"""
|
||||
Interface für Rate Limit Service.
|
||||
Definiert die Geschäftslogik für adaptives Rate Limiting.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def calculate_delay(self, action_type: ActionType, context: Optional[Dict[str, Any]] = None) -> float:
|
||||
"""
|
||||
Berechnet die optimale Verzögerung für eine Aktion.
|
||||
|
||||
Args:
|
||||
action_type: Typ der auszuführenden Aktion
|
||||
context: Optionaler Kontext (z.B. Platform, Session-ID)
|
||||
|
||||
Returns:
|
||||
Verzögerung in Sekunden
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def record_action(self, timing: ActionTiming) -> None:
|
||||
"""
|
||||
Zeichnet eine ausgeführte Aktion für Analyse auf.
|
||||
|
||||
Args:
|
||||
timing: Timing-Informationen der Aktion
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def detect_rate_limit(self, response: Any) -> bool:
|
||||
"""
|
||||
Erkennt ob eine Response auf Rate Limiting hindeutet.
|
||||
|
||||
Args:
|
||||
response: HTTP Response oder Browser-Seite
|
||||
|
||||
Returns:
|
||||
True wenn Rate Limit erkannt wurde
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_policy(self, action_type: ActionType) -> RateLimitPolicy:
|
||||
"""
|
||||
Holt die aktuelle Rate Limit Policy für einen Action Type.
|
||||
|
||||
Args:
|
||||
action_type: Typ der Aktion
|
||||
|
||||
Returns:
|
||||
Rate Limit Policy
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update_policy(self, action_type: ActionType, policy: RateLimitPolicy) -> None:
|
||||
"""
|
||||
Aktualisiert die Rate Limit Policy für einen Action Type.
|
||||
|
||||
Args:
|
||||
action_type: Typ der Aktion
|
||||
policy: Neue Policy
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_statistics(self,
|
||||
action_type: Optional[ActionType] = None,
|
||||
timeframe: Optional[timedelta] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Holt Statistiken über Rate Limiting.
|
||||
|
||||
Args:
|
||||
action_type: Optional - nur für spezifischen Action Type
|
||||
timeframe: Optional - nur für bestimmten Zeitraum
|
||||
|
||||
Returns:
|
||||
Dictionary mit Statistiken
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def reset_statistics(self) -> None:
|
||||
"""Setzt alle gesammelten Statistiken zurück."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def is_action_allowed(self, action_type: ActionType) -> bool:
|
||||
"""
|
||||
Prüft ob eine Aktion basierend auf Rate Limits erlaubt ist.
|
||||
|
||||
Args:
|
||||
action_type: Typ der Aktion
|
||||
|
||||
Returns:
|
||||
True wenn Aktion erlaubt ist
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def wait_if_needed(self, action_type: ActionType) -> float:
|
||||
"""
|
||||
Wartet die notwendige Zeit bevor eine Aktion ausgeführt werden kann.
|
||||
|
||||
Args:
|
||||
action_type: Typ der Aktion
|
||||
|
||||
Returns:
|
||||
Tatsächlich gewartete Zeit in Sekunden
|
||||
"""
|
||||
pass
|
||||
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