Initial commit
Dieser Commit ist enthalten in:
13
infrastructure/repositories/__init__.py
Normale Datei
13
infrastructure/repositories/__init__.py
Normale Datei
@ -0,0 +1,13 @@
|
||||
"""
|
||||
Infrastructure Repositories - Datenpersistierung und -zugriff
|
||||
"""
|
||||
|
||||
from .fingerprint_repository import FingerprintRepository
|
||||
from .analytics_repository import AnalyticsRepository
|
||||
from .rate_limit_repository import RateLimitRepository
|
||||
|
||||
__all__ = [
|
||||
'FingerprintRepository',
|
||||
'AnalyticsRepository',
|
||||
'RateLimitRepository'
|
||||
]
|
||||
179
infrastructure/repositories/account_repository.py
Normale Datei
179
infrastructure/repositories/account_repository.py
Normale Datei
@ -0,0 +1,179 @@
|
||||
"""
|
||||
Account Repository - Zugriff auf Account-Daten in der Datenbank
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import json
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from infrastructure.repositories.base_repository import BaseRepository
|
||||
|
||||
|
||||
class AccountRepository(BaseRepository):
|
||||
"""Repository für Account-Datenzugriff"""
|
||||
|
||||
def get_by_id(self, account_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Holt einen Account nach ID.
|
||||
|
||||
Args:
|
||||
account_id: Account ID
|
||||
|
||||
Returns:
|
||||
Dict mit Account-Daten oder None
|
||||
"""
|
||||
# Sichere Abfrage die mit verschiedenen Schema-Versionen funktioniert
|
||||
query = "SELECT * FROM accounts WHERE id = ?"
|
||||
|
||||
rows = self._execute_query(query, (account_id,))
|
||||
|
||||
if not rows:
|
||||
return None
|
||||
|
||||
return self._row_to_account(rows[0])
|
||||
|
||||
def get_by_username(self, username: str, platform: str = None) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Holt einen Account nach Username.
|
||||
|
||||
Args:
|
||||
username: Username
|
||||
platform: Optional platform filter
|
||||
|
||||
Returns:
|
||||
Dict mit Account-Daten oder None
|
||||
"""
|
||||
if platform:
|
||||
query = "SELECT * FROM accounts WHERE username = ? AND platform = ?"
|
||||
params = (username, platform)
|
||||
else:
|
||||
query = "SELECT * FROM accounts WHERE username = ?"
|
||||
params = (username,)
|
||||
|
||||
rows = self._execute_query(query, params)
|
||||
|
||||
if not rows:
|
||||
return None
|
||||
|
||||
return self._row_to_account(rows[0])
|
||||
|
||||
def get_all(self, platform: str = None, status: str = None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Holt alle Accounts mit optionalen Filtern.
|
||||
|
||||
Args:
|
||||
platform: Optional platform filter
|
||||
status: Optional status filter
|
||||
|
||||
Returns:
|
||||
Liste von Account-Dicts
|
||||
"""
|
||||
query = "SELECT * FROM accounts WHERE 1=1"
|
||||
params = []
|
||||
|
||||
if platform:
|
||||
query += " AND platform = ?"
|
||||
params.append(platform)
|
||||
|
||||
if status:
|
||||
query += " AND status = ?"
|
||||
params.append(status)
|
||||
|
||||
query += " ORDER BY created_at DESC"
|
||||
|
||||
rows = self._execute_query(query, params)
|
||||
return [self._row_to_account(row) for row in rows]
|
||||
|
||||
def update_fingerprint_id(self, account_id: int, fingerprint_id: str) -> bool:
|
||||
"""
|
||||
Aktualisiert die Fingerprint ID eines Accounts.
|
||||
|
||||
Args:
|
||||
account_id: Account ID
|
||||
fingerprint_id: Neue Fingerprint ID
|
||||
|
||||
Returns:
|
||||
True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
query = "UPDATE accounts SET fingerprint_id = ? WHERE id = ?"
|
||||
return self._execute_update(query, (fingerprint_id, account_id)) > 0
|
||||
|
||||
def update_session_id(self, account_id: int, session_id: str) -> bool:
|
||||
"""
|
||||
Aktualisiert die Session ID eines Accounts.
|
||||
|
||||
Args:
|
||||
account_id: Account ID
|
||||
session_id: Neue Session ID
|
||||
|
||||
Returns:
|
||||
True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
query = """
|
||||
UPDATE accounts
|
||||
SET session_id = ?, last_session_update = datetime('now')
|
||||
WHERE id = ?
|
||||
"""
|
||||
return self._execute_update(query, (session_id, account_id)) > 0
|
||||
|
||||
def update_status(self, account_id: int, status: str) -> bool:
|
||||
"""
|
||||
Aktualisiert den Status eines Accounts.
|
||||
|
||||
Args:
|
||||
account_id: Account ID
|
||||
status: Neuer Status
|
||||
|
||||
Returns:
|
||||
True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
query = "UPDATE accounts SET status = ? WHERE id = ?"
|
||||
return self._execute_update(query, (status, account_id)) > 0
|
||||
|
||||
def _row_to_account(self, row) -> Dict[str, Any]:
|
||||
"""Konvertiert eine Datenbankzeile zu einem Account-Dict"""
|
||||
# sqlite3.Row unterstützt dict() Konvertierung direkt
|
||||
if hasattr(row, 'keys'):
|
||||
# Es ist ein sqlite3.Row Objekt
|
||||
account = dict(row)
|
||||
else:
|
||||
# Fallback für normale Tuples
|
||||
# Hole die tatsächlichen Spaltennamen aus der Datenbank
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("PRAGMA table_info(accounts)")
|
||||
columns_info = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
# Extrahiere Spaltennamen
|
||||
columns = [col[1] for col in columns_info]
|
||||
|
||||
# Erstelle Dict mit vorhandenen Spalten
|
||||
account = {}
|
||||
for i, value in enumerate(row):
|
||||
if i < len(columns):
|
||||
account[columns[i]] = value
|
||||
|
||||
# Parse metadata wenn vorhanden und die Spalte existiert
|
||||
if 'metadata' in account and account.get('metadata'):
|
||||
try:
|
||||
metadata = json.loads(account['metadata'])
|
||||
account['metadata'] = metadata
|
||||
# Extrahiere platform aus metadata wenn vorhanden
|
||||
if isinstance(metadata, dict) and 'platform' in metadata:
|
||||
account['platform'] = metadata['platform']
|
||||
except:
|
||||
account['metadata'] = {}
|
||||
|
||||
# Setze Standardwerte für fehlende Felder
|
||||
if 'platform' not in account:
|
||||
# Standardmäßig auf instagram setzen
|
||||
account['platform'] = 'instagram'
|
||||
|
||||
# Stelle sicher dass wichtige Felder existieren
|
||||
for field in ['fingerprint_id', 'metadata']:
|
||||
if field not in account:
|
||||
account[field] = None
|
||||
|
||||
return account
|
||||
306
infrastructure/repositories/analytics_repository.py
Normale Datei
306
infrastructure/repositories/analytics_repository.py
Normale Datei
@ -0,0 +1,306 @@
|
||||
"""
|
||||
Analytics Repository - Persistierung von Analytics und Events
|
||||
"""
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
from typing import List, Optional, Dict, Any, Union
|
||||
from datetime import datetime, timedelta
|
||||
from collections import defaultdict
|
||||
|
||||
from infrastructure.repositories.base_repository import BaseRepository
|
||||
from domain.entities.account_creation_event import AccountCreationEvent, WorkflowStep
|
||||
from domain.entities.error_event import ErrorEvent, ErrorType
|
||||
from domain.value_objects.error_summary import ErrorSummary
|
||||
|
||||
|
||||
class AnalyticsRepository(BaseRepository):
|
||||
"""Repository für Analytics Events und Reporting"""
|
||||
|
||||
def save_account_creation_event(self, event: AccountCreationEvent) -> None:
|
||||
"""Speichert ein Account Creation Event"""
|
||||
query = """
|
||||
INSERT INTO account_creation_analytics (
|
||||
event_id, timestamp, account_id, session_id, fingerprint_id,
|
||||
duration_seconds, success, error_type, error_message,
|
||||
workflow_steps, metadata, total_retry_count, network_requests,
|
||||
screenshots_taken, proxy_used, proxy_type, browser_type,
|
||||
headless, success_rate
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
# Serialisiere komplexe Daten
|
||||
workflow_steps_json = self._serialize_json([
|
||||
step.to_dict() for step in event.steps_completed
|
||||
])
|
||||
|
||||
metadata = {
|
||||
'platform': event.account_data.platform if event.account_data else None,
|
||||
'username': event.account_data.username if event.account_data else None,
|
||||
'email': event.account_data.email if event.account_data else None,
|
||||
'additional': event.account_data.metadata if event.account_data else {}
|
||||
}
|
||||
|
||||
params = (
|
||||
event.event_id,
|
||||
event.timestamp,
|
||||
event.account_data.username if event.account_data else None,
|
||||
event.session_id,
|
||||
event.fingerprint_id,
|
||||
event.duration.total_seconds() if event.duration else 0,
|
||||
event.success,
|
||||
event.error_details.error_type if event.error_details else None,
|
||||
event.error_details.error_message if event.error_details else None,
|
||||
workflow_steps_json,
|
||||
self._serialize_json(metadata),
|
||||
event.total_retry_count,
|
||||
event.network_requests,
|
||||
event.screenshots_taken,
|
||||
event.proxy_used,
|
||||
event.proxy_type,
|
||||
event.browser_type,
|
||||
event.headless,
|
||||
event.get_success_rate()
|
||||
)
|
||||
|
||||
self._execute_insert(query, params)
|
||||
|
||||
def save_error_event(self, event: ErrorEvent) -> None:
|
||||
"""Speichert ein Error Event"""
|
||||
query = """
|
||||
INSERT INTO error_events (
|
||||
error_id, timestamp, error_type, error_message, stack_trace,
|
||||
context, recovery_attempted, recovery_successful, recovery_attempts,
|
||||
severity, platform, session_id, account_id, correlation_id,
|
||||
user_impact, system_impact, data_loss
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
context_json = self._serialize_json({
|
||||
'url': event.context.url,
|
||||
'action': event.context.action,
|
||||
'step_name': event.context.step_name,
|
||||
'screenshot_path': event.context.screenshot_path,
|
||||
'additional_data': event.context.additional_data
|
||||
})
|
||||
|
||||
recovery_attempts_json = self._serialize_json([
|
||||
{
|
||||
'strategy': attempt.strategy,
|
||||
'timestamp': attempt.timestamp.isoformat(),
|
||||
'successful': attempt.successful,
|
||||
'error_message': attempt.error_message,
|
||||
'duration_seconds': attempt.duration_seconds
|
||||
}
|
||||
for attempt in event.recovery_attempts
|
||||
])
|
||||
|
||||
params = (
|
||||
event.error_id,
|
||||
event.timestamp,
|
||||
event.error_type.value,
|
||||
event.error_message,
|
||||
event.stack_trace,
|
||||
context_json,
|
||||
event.recovery_attempted,
|
||||
event.recovery_successful,
|
||||
recovery_attempts_json,
|
||||
event.severity.value,
|
||||
event.platform,
|
||||
event.session_id,
|
||||
event.account_id,
|
||||
event.correlation_id,
|
||||
event.user_impact,
|
||||
event.system_impact,
|
||||
event.data_loss
|
||||
)
|
||||
|
||||
self._execute_insert(query, params)
|
||||
|
||||
def get_success_rate(self, timeframe: Optional[timedelta] = None,
|
||||
platform: Optional[str] = None) -> float:
|
||||
"""Berechnet die Erfolgsrate"""
|
||||
query = """
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successful
|
||||
FROM account_creation_analytics
|
||||
WHERE 1=1
|
||||
"""
|
||||
params = []
|
||||
|
||||
if timeframe:
|
||||
query += " AND timestamp > datetime('now', '-' || ? || ' seconds')"
|
||||
params.append(int(timeframe.total_seconds()))
|
||||
|
||||
if platform:
|
||||
query += " AND json_extract(metadata, '$.platform') = ?"
|
||||
params.append(platform)
|
||||
|
||||
row = self._execute_query(query, tuple(params))[0]
|
||||
|
||||
if row['total'] > 0:
|
||||
return row['successful'] / row['total']
|
||||
return 0.0
|
||||
|
||||
def get_common_errors(self, limit: int = 10,
|
||||
timeframe: Optional[timedelta] = None) -> List[ErrorSummary]:
|
||||
"""Holt die häufigsten Fehler"""
|
||||
query = """
|
||||
SELECT
|
||||
error_type,
|
||||
COUNT(*) as error_count,
|
||||
MIN(timestamp) as first_occurrence,
|
||||
MAX(timestamp) as last_occurrence,
|
||||
AVG(CASE WHEN recovery_successful = 1 THEN 1.0 ELSE 0.0 END) as recovery_rate,
|
||||
GROUP_CONCAT(DISTINCT session_id) as sessions,
|
||||
GROUP_CONCAT(DISTINCT account_id) as accounts,
|
||||
SUM(user_impact) as total_user_impact,
|
||||
SUM(system_impact) as total_system_impact,
|
||||
SUM(data_loss) as data_loss_incidents
|
||||
FROM error_events
|
||||
WHERE 1=1
|
||||
"""
|
||||
params = []
|
||||
|
||||
if timeframe:
|
||||
query += " AND timestamp > datetime('now', '-' || ? || ' seconds')"
|
||||
params.append(int(timeframe.total_seconds()))
|
||||
|
||||
query += " GROUP BY error_type ORDER BY error_count DESC LIMIT ?"
|
||||
params.append(limit)
|
||||
|
||||
rows = self._execute_query(query, tuple(params))
|
||||
|
||||
summaries = []
|
||||
for row in rows:
|
||||
# Hole zusätzliche Details für diesen Fehlertyp
|
||||
detail_query = """
|
||||
SELECT
|
||||
json_extract(context, '$.url') as url,
|
||||
json_extract(context, '$.action') as action,
|
||||
json_extract(context, '$.step_name') as step,
|
||||
COUNT(*) as count
|
||||
FROM error_events
|
||||
WHERE error_type = ?
|
||||
GROUP BY url, action, step
|
||||
ORDER BY count DESC
|
||||
LIMIT 5
|
||||
"""
|
||||
details = self._execute_query(detail_query, (row['error_type'],))
|
||||
|
||||
urls = []
|
||||
actions = []
|
||||
steps = []
|
||||
|
||||
for detail in details:
|
||||
if detail['url']:
|
||||
urls.append(detail['url'])
|
||||
if detail['action']:
|
||||
actions.append(detail['action'])
|
||||
if detail['step']:
|
||||
steps.append(detail['step'])
|
||||
|
||||
summary = ErrorSummary(
|
||||
error_type=row['error_type'],
|
||||
error_count=row['error_count'],
|
||||
first_occurrence=self._parse_datetime(row['first_occurrence']),
|
||||
last_occurrence=self._parse_datetime(row['last_occurrence']),
|
||||
affected_sessions=row['sessions'].split(',') if row['sessions'] else [],
|
||||
affected_accounts=row['accounts'].split(',') if row['accounts'] else [],
|
||||
avg_recovery_time=0.0, # TODO: Berechnen aus recovery_attempts
|
||||
recovery_success_rate=row['recovery_rate'] or 0.0,
|
||||
most_common_urls=urls,
|
||||
most_common_actions=actions,
|
||||
most_common_steps=steps,
|
||||
total_user_impact=row['total_user_impact'] or 0,
|
||||
total_system_impact=row['total_system_impact'] or 0,
|
||||
data_loss_incidents=row['data_loss_incidents'] or 0
|
||||
)
|
||||
|
||||
summaries.append(summary)
|
||||
|
||||
return summaries
|
||||
|
||||
def get_timeline_data(self, metric: str, hours: int = 24,
|
||||
platform: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""Holt Timeline-Daten für Graphen"""
|
||||
# Erstelle stündliche Buckets
|
||||
query = """
|
||||
SELECT
|
||||
strftime('%Y-%m-%d %H:00:00', timestamp) as hour,
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successful,
|
||||
AVG(duration_seconds) as avg_duration
|
||||
FROM account_creation_analytics
|
||||
WHERE timestamp > datetime('now', '-' || ? || ' hours')
|
||||
"""
|
||||
params = [hours]
|
||||
|
||||
if platform:
|
||||
query += " AND json_extract(metadata, '$.platform') = ?"
|
||||
params.append(platform)
|
||||
|
||||
query += " GROUP BY hour ORDER BY hour"
|
||||
|
||||
rows = self._execute_query(query, tuple(params))
|
||||
|
||||
timeline = []
|
||||
for row in rows:
|
||||
data = {
|
||||
'timestamp': row['hour'],
|
||||
'total': row['total'],
|
||||
'successful': row['successful'],
|
||||
'success_rate': row['successful'] / row['total'] if row['total'] > 0 else 0,
|
||||
'avg_duration': row['avg_duration']
|
||||
}
|
||||
timeline.append(data)
|
||||
|
||||
return timeline
|
||||
|
||||
def get_platform_stats(self, timeframe: Optional[timedelta] = None) -> Dict[str, Dict[str, Any]]:
|
||||
"""Holt Statistiken pro Plattform"""
|
||||
query = """
|
||||
SELECT
|
||||
json_extract(metadata, '$.platform') as platform,
|
||||
COUNT(*) as total_attempts,
|
||||
SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successful,
|
||||
AVG(duration_seconds) as avg_duration,
|
||||
AVG(total_retry_count) as avg_retries
|
||||
FROM account_creation_analytics
|
||||
WHERE json_extract(metadata, '$.platform') IS NOT NULL
|
||||
"""
|
||||
params = []
|
||||
|
||||
if timeframe:
|
||||
query += " AND timestamp > datetime('now', '-' || ? || ' seconds')"
|
||||
params.append(int(timeframe.total_seconds()))
|
||||
|
||||
query += " GROUP BY platform"
|
||||
|
||||
rows = self._execute_query(query, tuple(params))
|
||||
|
||||
stats = {}
|
||||
for row in rows:
|
||||
stats[row['platform']] = {
|
||||
'total_attempts': row['total_attempts'],
|
||||
'successful_accounts': row['successful'],
|
||||
'failed_attempts': row['total_attempts'] - row['successful'],
|
||||
'success_rate': row['successful'] / row['total_attempts'] if row['total_attempts'] > 0 else 0,
|
||||
'avg_duration_seconds': row['avg_duration'],
|
||||
'avg_retries': row['avg_retries']
|
||||
}
|
||||
|
||||
return stats
|
||||
|
||||
def cleanup_old_events(self, older_than: datetime) -> int:
|
||||
"""Bereinigt alte Events"""
|
||||
count1 = self._execute_delete(
|
||||
"DELETE FROM account_creation_analytics WHERE timestamp < ?",
|
||||
(older_than,)
|
||||
)
|
||||
count2 = self._execute_delete(
|
||||
"DELETE FROM error_events WHERE timestamp < ?",
|
||||
(older_than,)
|
||||
)
|
||||
return count1 + count2
|
||||
112
infrastructure/repositories/base_repository.py
Normale Datei
112
infrastructure/repositories/base_repository.py
Normale Datei
@ -0,0 +1,112 @@
|
||||
"""
|
||||
Base Repository - Abstrakte Basis für alle Repositories
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, List, Any, Optional, Union
|
||||
from datetime import datetime
|
||||
from contextlib import contextmanager
|
||||
|
||||
from config.paths import PathConfig
|
||||
|
||||
logger = logging.getLogger("base_repository")
|
||||
|
||||
|
||||
class BaseRepository:
|
||||
"""Basis-Repository mit gemeinsamen Datenbankfunktionen"""
|
||||
|
||||
def __init__(self, db_path: str = None):
|
||||
"""
|
||||
Initialisiert das Repository.
|
||||
|
||||
Args:
|
||||
db_path: Pfad zur Datenbank (falls None, wird PathConfig.MAIN_DB verwendet)
|
||||
"""
|
||||
self.db_path = db_path if db_path is not None else PathConfig.MAIN_DB
|
||||
self._ensure_schema()
|
||||
|
||||
def _ensure_schema(self):
|
||||
"""Stellt sicher dass das erweiterte Schema existiert"""
|
||||
try:
|
||||
if PathConfig.file_exists(PathConfig.SCHEMA_V2):
|
||||
with open(PathConfig.SCHEMA_V2, "r", encoding='utf-8') as f:
|
||||
schema_sql = f.read()
|
||||
|
||||
with self.get_connection() as conn:
|
||||
conn.executescript(schema_sql)
|
||||
conn.commit()
|
||||
logger.info("Schema v2 erfolgreich geladen")
|
||||
else:
|
||||
logger.warning(f"schema_v2.sql nicht gefunden unter {PathConfig.SCHEMA_V2}, nutze existierendes Schema")
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Schema-Update: {e}")
|
||||
|
||||
@contextmanager
|
||||
def get_connection(self):
|
||||
"""Context Manager für Datenbankverbindungen"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
try:
|
||||
yield conn
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def _serialize_json(self, data: Any) -> str:
|
||||
"""Serialisiert Daten zu JSON"""
|
||||
if data is None:
|
||||
return None
|
||||
return json.dumps(data, default=str)
|
||||
|
||||
def _deserialize_json(self, data: str) -> Any:
|
||||
"""Deserialisiert JSON zu Python-Objekten"""
|
||||
if data is None:
|
||||
return None
|
||||
try:
|
||||
return json.loads(data)
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f"Fehler beim JSON-Parsing: {data}")
|
||||
return None
|
||||
|
||||
def _parse_datetime(self, dt_string: str) -> Optional[datetime]:
|
||||
"""Parst einen Datetime-String"""
|
||||
if not dt_string:
|
||||
return None
|
||||
try:
|
||||
# SQLite datetime format
|
||||
return datetime.strptime(dt_string, "%Y-%m-%d %H:%M:%S")
|
||||
except ValueError:
|
||||
try:
|
||||
# ISO format
|
||||
return datetime.fromisoformat(dt_string.replace('Z', '+00:00'))
|
||||
except:
|
||||
logger.error(f"Konnte Datetime nicht parsen: {dt_string}")
|
||||
return None
|
||||
|
||||
def _execute_query(self, query: str, params: tuple = ()) -> List[sqlite3.Row]:
|
||||
"""Führt eine SELECT-Query aus"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.execute(query, params)
|
||||
return cursor.fetchall()
|
||||
|
||||
def _execute_insert(self, query: str, params: tuple = ()) -> int:
|
||||
"""Führt eine INSERT-Query aus und gibt die ID zurück"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.execute(query, params)
|
||||
conn.commit()
|
||||
return cursor.lastrowid
|
||||
|
||||
def _execute_update(self, query: str, params: tuple = ()) -> int:
|
||||
"""Führt eine UPDATE-Query aus und gibt affected rows zurück"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.execute(query, params)
|
||||
conn.commit()
|
||||
return cursor.rowcount
|
||||
|
||||
def _execute_delete(self, query: str, params: tuple = ()) -> int:
|
||||
"""Führt eine DELETE-Query aus und gibt affected rows zurück"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.execute(query, params)
|
||||
conn.commit()
|
||||
return cursor.rowcount
|
||||
273
infrastructure/repositories/fingerprint_repository.py
Normale Datei
273
infrastructure/repositories/fingerprint_repository.py
Normale Datei
@ -0,0 +1,273 @@
|
||||
"""
|
||||
Fingerprint Repository - Persistierung von Browser Fingerprints
|
||||
"""
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
from infrastructure.repositories.base_repository import BaseRepository
|
||||
from domain.entities.browser_fingerprint import (
|
||||
BrowserFingerprint, CanvasNoise, WebRTCConfig,
|
||||
HardwareConfig, NavigatorProperties, StaticComponents
|
||||
)
|
||||
|
||||
|
||||
class FingerprintRepository(BaseRepository):
|
||||
"""Repository für Browser Fingerprint Persistierung"""
|
||||
|
||||
def save(self, fingerprint: BrowserFingerprint) -> None:
|
||||
"""Speichert einen Fingerprint in der Datenbank"""
|
||||
query = """
|
||||
INSERT OR REPLACE INTO browser_fingerprints (
|
||||
id, canvas_noise_config, webrtc_config, fonts,
|
||||
hardware_config, navigator_props, webgl_vendor,
|
||||
webgl_renderer, audio_context_config, timezone,
|
||||
timezone_offset, plugins, created_at, last_rotated,
|
||||
platform_specific, static_components, rotation_seed,
|
||||
account_bound
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
audio_config = {
|
||||
'base_latency': fingerprint.audio_context_base_latency,
|
||||
'output_latency': fingerprint.audio_context_output_latency,
|
||||
'sample_rate': fingerprint.audio_context_sample_rate
|
||||
}
|
||||
|
||||
|
||||
params = (
|
||||
fingerprint.fingerprint_id,
|
||||
self._serialize_json({
|
||||
'noise_level': fingerprint.canvas_noise.noise_level,
|
||||
'seed': fingerprint.canvas_noise.seed,
|
||||
'algorithm': fingerprint.canvas_noise.algorithm
|
||||
}),
|
||||
self._serialize_json({
|
||||
'enabled': fingerprint.webrtc_config.enabled,
|
||||
'ice_servers': fingerprint.webrtc_config.ice_servers,
|
||||
'local_ip_mask': fingerprint.webrtc_config.local_ip_mask,
|
||||
'disable_webrtc': fingerprint.webrtc_config.disable_webrtc
|
||||
}),
|
||||
self._serialize_json(fingerprint.font_list),
|
||||
self._serialize_json({
|
||||
'hardware_concurrency': fingerprint.hardware_config.hardware_concurrency,
|
||||
'device_memory': fingerprint.hardware_config.device_memory,
|
||||
'max_touch_points': fingerprint.hardware_config.max_touch_points,
|
||||
'screen_resolution': fingerprint.hardware_config.screen_resolution,
|
||||
'color_depth': fingerprint.hardware_config.color_depth,
|
||||
'pixel_ratio': fingerprint.hardware_config.pixel_ratio
|
||||
}),
|
||||
self._serialize_json({
|
||||
'platform': fingerprint.navigator_props.platform,
|
||||
'vendor': fingerprint.navigator_props.vendor,
|
||||
'vendor_sub': fingerprint.navigator_props.vendor_sub,
|
||||
'product': fingerprint.navigator_props.product,
|
||||
'product_sub': fingerprint.navigator_props.product_sub,
|
||||
'app_name': fingerprint.navigator_props.app_name,
|
||||
'app_version': fingerprint.navigator_props.app_version,
|
||||
'user_agent': fingerprint.navigator_props.user_agent,
|
||||
'language': fingerprint.navigator_props.language,
|
||||
'languages': fingerprint.navigator_props.languages,
|
||||
'online': fingerprint.navigator_props.online,
|
||||
'do_not_track': fingerprint.navigator_props.do_not_track
|
||||
}),
|
||||
fingerprint.webgl_vendor,
|
||||
fingerprint.webgl_renderer,
|
||||
self._serialize_json(audio_config),
|
||||
fingerprint.timezone,
|
||||
fingerprint.timezone_offset,
|
||||
self._serialize_json(fingerprint.plugins),
|
||||
fingerprint.created_at,
|
||||
fingerprint.last_rotated,
|
||||
self._serialize_json(fingerprint.platform_specific_config), # platform_specific
|
||||
self._serialize_json(fingerprint.static_components.to_dict() if fingerprint.static_components else None),
|
||||
fingerprint.rotation_seed,
|
||||
fingerprint.account_bound
|
||||
)
|
||||
|
||||
self._execute_insert(query, params)
|
||||
|
||||
def find_by_id(self, fingerprint_id: str) -> Optional[BrowserFingerprint]:
|
||||
"""Findet einen Fingerprint nach ID"""
|
||||
query = "SELECT * FROM browser_fingerprints WHERE id = ?"
|
||||
rows = self._execute_query(query, (fingerprint_id,))
|
||||
|
||||
if not rows:
|
||||
return None
|
||||
|
||||
return self._row_to_fingerprint(rows[0])
|
||||
|
||||
def find_all(self, limit: int = 100) -> List[BrowserFingerprint]:
|
||||
"""Holt alle Fingerprints (mit Limit)"""
|
||||
query = "SELECT * FROM browser_fingerprints ORDER BY created_at DESC LIMIT ?"
|
||||
rows = self._execute_query(query, (limit,))
|
||||
|
||||
return [self._row_to_fingerprint(row) for row in rows]
|
||||
|
||||
def find_recent(self, hours: int = 24) -> List[BrowserFingerprint]:
|
||||
"""Findet kürzlich erstellte Fingerprints"""
|
||||
query = """
|
||||
SELECT * FROM browser_fingerprints
|
||||
WHERE created_at > datetime('now', '-' || ? || ' hours')
|
||||
ORDER BY created_at DESC
|
||||
"""
|
||||
rows = self._execute_query(query, (hours,))
|
||||
|
||||
return [self._row_to_fingerprint(row) for row in rows]
|
||||
|
||||
def update_last_rotated(self, fingerprint_id: str, timestamp: datetime) -> None:
|
||||
"""Aktualisiert den last_rotated Timestamp"""
|
||||
query = "UPDATE browser_fingerprints SET last_rotated = ? WHERE id = ?"
|
||||
self._execute_update(query, (timestamp, fingerprint_id))
|
||||
|
||||
def delete_older_than(self, timestamp: datetime) -> int:
|
||||
"""Löscht Fingerprints älter als timestamp"""
|
||||
query = "DELETE FROM browser_fingerprints WHERE created_at < ?"
|
||||
return self._execute_delete(query, (timestamp,))
|
||||
|
||||
def get_random_fingerprints(self, count: int = 10) -> List[BrowserFingerprint]:
|
||||
"""Holt zufällige Fingerprints für Pool"""
|
||||
query = """
|
||||
SELECT * FROM browser_fingerprints
|
||||
ORDER BY RANDOM()
|
||||
LIMIT ?
|
||||
"""
|
||||
rows = self._execute_query(query, (count,))
|
||||
|
||||
return [self._row_to_fingerprint(row) for row in rows]
|
||||
|
||||
def link_to_account(self, fingerprint_id: str, account_id: str, primary: bool = True) -> None:
|
||||
"""Links a fingerprint to an account using simple 1:1 relationship"""
|
||||
query = """
|
||||
UPDATE accounts SET fingerprint_id = ? WHERE id = ?
|
||||
"""
|
||||
self._execute_update(query, (fingerprint_id, account_id))
|
||||
|
||||
def get_primary_fingerprint_for_account(self, account_id: str) -> Optional[str]:
|
||||
"""Gets the fingerprint ID for an account (1:1 relationship)"""
|
||||
query = """
|
||||
SELECT fingerprint_id FROM accounts
|
||||
WHERE id = ? AND fingerprint_id IS NOT NULL
|
||||
"""
|
||||
rows = self._execute_query(query, (account_id,))
|
||||
return dict(rows[0])['fingerprint_id'] if rows else None
|
||||
|
||||
def get_fingerprints_for_account(self, account_id: str) -> List[BrowserFingerprint]:
|
||||
"""Gets the fingerprint associated with an account (1:1 relationship)"""
|
||||
fingerprint_id = self.get_primary_fingerprint_for_account(account_id)
|
||||
if fingerprint_id:
|
||||
fingerprint = self.find_by_id(fingerprint_id)
|
||||
return [fingerprint] if fingerprint else []
|
||||
return []
|
||||
|
||||
def update_fingerprint_stats(self, fingerprint_id: str, account_id: str,
|
||||
success: bool) -> None:
|
||||
"""Updates fingerprint last used timestamp (simplified for 1:1)"""
|
||||
# Update the fingerprint's last used time
|
||||
query = """
|
||||
UPDATE browser_fingerprints
|
||||
SET last_rotated = datetime('now')
|
||||
WHERE id = ?
|
||||
"""
|
||||
self._execute_update(query, (fingerprint_id,))
|
||||
|
||||
# Also update account's last login
|
||||
query = """
|
||||
UPDATE accounts
|
||||
SET last_login = datetime('now')
|
||||
WHERE id = ? AND fingerprint_id = ?
|
||||
"""
|
||||
self._execute_update(query, (account_id, fingerprint_id))
|
||||
|
||||
def _row_to_fingerprint(self, row: sqlite3.Row) -> BrowserFingerprint:
|
||||
"""Konvertiert eine Datenbankzeile zu einem Fingerprint"""
|
||||
# Canvas Noise
|
||||
canvas_config = self._deserialize_json(row['canvas_noise_config'])
|
||||
canvas_noise = CanvasNoise(
|
||||
noise_level=canvas_config.get('noise_level', 0.02),
|
||||
seed=canvas_config.get('seed', 42),
|
||||
algorithm=canvas_config.get('algorithm', 'gaussian')
|
||||
)
|
||||
|
||||
# WebRTC Config
|
||||
webrtc_config_data = self._deserialize_json(row['webrtc_config'])
|
||||
webrtc_config = WebRTCConfig(
|
||||
enabled=webrtc_config_data.get('enabled', True),
|
||||
ice_servers=webrtc_config_data.get('ice_servers', []),
|
||||
local_ip_mask=webrtc_config_data.get('local_ip_mask', '10.0.0.x'),
|
||||
disable_webrtc=webrtc_config_data.get('disable_webrtc', False)
|
||||
)
|
||||
|
||||
# Hardware Config
|
||||
hw_config = self._deserialize_json(row['hardware_config'])
|
||||
hardware_config = HardwareConfig(
|
||||
hardware_concurrency=hw_config.get('hardware_concurrency', 4),
|
||||
device_memory=hw_config.get('device_memory', 8),
|
||||
max_touch_points=hw_config.get('max_touch_points', 0),
|
||||
screen_resolution=tuple(hw_config.get('screen_resolution', [1920, 1080])),
|
||||
color_depth=hw_config.get('color_depth', 24),
|
||||
pixel_ratio=hw_config.get('pixel_ratio', 1.0)
|
||||
)
|
||||
|
||||
# Navigator Properties
|
||||
nav_props = self._deserialize_json(row['navigator_props'])
|
||||
navigator_props = NavigatorProperties(
|
||||
platform=nav_props.get('platform', 'Win32'),
|
||||
vendor=nav_props.get('vendor', 'Google Inc.'),
|
||||
vendor_sub=nav_props.get('vendor_sub', ''),
|
||||
product=nav_props.get('product', 'Gecko'),
|
||||
product_sub=nav_props.get('product_sub', '20030107'),
|
||||
app_name=nav_props.get('app_name', 'Netscape'),
|
||||
app_version=nav_props.get('app_version', '5.0'),
|
||||
user_agent=nav_props.get('user_agent', ''),
|
||||
language=nav_props.get('language', 'de-DE'),
|
||||
languages=nav_props.get('languages', ['de-DE', 'de', 'en-US', 'en']),
|
||||
online=nav_props.get('online', True),
|
||||
do_not_track=nav_props.get('do_not_track', '1')
|
||||
)
|
||||
|
||||
# Audio Context
|
||||
audio_config = self._deserialize_json(row['audio_context_config']) or {}
|
||||
|
||||
# Static Components
|
||||
static_components = None
|
||||
if 'static_components' in row.keys() and row['static_components']:
|
||||
sc_data = self._deserialize_json(row['static_components'])
|
||||
if sc_data:
|
||||
static_components = StaticComponents(
|
||||
device_type=sc_data.get('device_type', 'desktop'),
|
||||
os_family=sc_data.get('os_family', 'windows'),
|
||||
browser_family=sc_data.get('browser_family', 'chromium'),
|
||||
gpu_vendor=sc_data.get('gpu_vendor', 'Intel Inc.'),
|
||||
gpu_model=sc_data.get('gpu_model', 'Intel Iris OpenGL Engine'),
|
||||
cpu_architecture=sc_data.get('cpu_architecture', 'x86_64'),
|
||||
base_fonts=sc_data.get('base_fonts', []),
|
||||
base_resolution=tuple(sc_data.get('base_resolution', [1920, 1080])),
|
||||
base_timezone=sc_data.get('base_timezone', 'Europe/Berlin')
|
||||
)
|
||||
|
||||
|
||||
return BrowserFingerprint(
|
||||
fingerprint_id=row['id'],
|
||||
canvas_noise=canvas_noise,
|
||||
webrtc_config=webrtc_config,
|
||||
font_list=self._deserialize_json(row['fonts']) or [],
|
||||
hardware_config=hardware_config,
|
||||
navigator_props=navigator_props,
|
||||
created_at=self._parse_datetime(row['created_at']),
|
||||
last_rotated=self._parse_datetime(row['last_rotated']),
|
||||
webgl_vendor=row['webgl_vendor'],
|
||||
webgl_renderer=row['webgl_renderer'],
|
||||
audio_context_base_latency=audio_config.get('base_latency', 0.0),
|
||||
audio_context_output_latency=audio_config.get('output_latency', 0.0),
|
||||
audio_context_sample_rate=audio_config.get('sample_rate', 48000),
|
||||
timezone=row['timezone'],
|
||||
timezone_offset=row['timezone_offset'],
|
||||
plugins=self._deserialize_json(row['plugins']) or [],
|
||||
static_components=static_components,
|
||||
rotation_seed=row['rotation_seed'] if 'rotation_seed' in row.keys() else None,
|
||||
account_bound=row['account_bound'] if 'account_bound' in row.keys() else False,
|
||||
platform_specific_config=self._deserialize_json(row['platform_specific'] if 'platform_specific' in row.keys() else '{}') or {}
|
||||
)
|
||||
282
infrastructure/repositories/method_strategy_repository.py
Normale Datei
282
infrastructure/repositories/method_strategy_repository.py
Normale Datei
@ -0,0 +1,282 @@
|
||||
"""
|
||||
SQLite implementation of method strategy repository.
|
||||
Handles persistence and retrieval of method strategies with performance optimization.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
from domain.entities.method_rotation import MethodStrategy, RiskLevel
|
||||
from domain.repositories.method_rotation_repository import IMethodStrategyRepository
|
||||
from database.db_manager import DatabaseManager
|
||||
|
||||
|
||||
class MethodStrategyRepository(IMethodStrategyRepository):
|
||||
"""SQLite implementation of method strategy repository"""
|
||||
|
||||
def __init__(self, db_manager):
|
||||
self.db_manager = db_manager
|
||||
|
||||
def save(self, strategy: MethodStrategy) -> None:
|
||||
"""Save or update a method strategy"""
|
||||
strategy.updated_at = datetime.now()
|
||||
|
||||
query = """
|
||||
INSERT OR REPLACE INTO method_strategies (
|
||||
id, platform, method_name, priority, success_rate, failure_rate,
|
||||
last_success, last_failure, cooldown_period, max_daily_attempts,
|
||||
risk_level, is_active, configuration, tags, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
params = (
|
||||
strategy.strategy_id,
|
||||
strategy.platform,
|
||||
strategy.method_name,
|
||||
strategy.priority,
|
||||
strategy.success_rate,
|
||||
strategy.failure_rate,
|
||||
strategy.last_success.isoformat() if strategy.last_success else None,
|
||||
strategy.last_failure.isoformat() if strategy.last_failure else None,
|
||||
strategy.cooldown_period,
|
||||
strategy.max_daily_attempts,
|
||||
strategy.risk_level.value,
|
||||
strategy.is_active,
|
||||
json.dumps(strategy.configuration),
|
||||
json.dumps(strategy.tags),
|
||||
strategy.created_at.isoformat(),
|
||||
strategy.updated_at.isoformat()
|
||||
)
|
||||
|
||||
self.db_manager.execute_query(query, params)
|
||||
|
||||
def find_by_id(self, strategy_id: str) -> Optional[MethodStrategy]:
|
||||
"""Find a strategy by its ID"""
|
||||
query = "SELECT * FROM method_strategies WHERE id = ?"
|
||||
result = self.db_manager.fetch_one(query, (strategy_id,))
|
||||
return self._row_to_strategy(result) if result else None
|
||||
|
||||
def find_by_platform(self, platform: str) -> List[MethodStrategy]:
|
||||
"""Find all strategies for a platform"""
|
||||
query = """
|
||||
SELECT * FROM method_strategies
|
||||
WHERE platform = ?
|
||||
ORDER BY priority DESC, success_rate DESC
|
||||
"""
|
||||
results = self.db_manager.fetch_all(query, (platform,))
|
||||
return [self._row_to_strategy(row) for row in results]
|
||||
|
||||
def find_active_by_platform(self, platform: str) -> List[MethodStrategy]:
|
||||
"""Find all active strategies for a platform, ordered by effectiveness"""
|
||||
query = """
|
||||
SELECT * FROM method_strategies
|
||||
WHERE platform = ? AND is_active = 1
|
||||
ORDER BY priority DESC, success_rate DESC, last_success DESC
|
||||
"""
|
||||
results = self.db_manager.fetch_all(query, (platform,))
|
||||
strategies = [self._row_to_strategy(row) for row in results]
|
||||
|
||||
# Sort by effectiveness score
|
||||
strategies.sort(key=lambda s: s.effectiveness_score, reverse=True)
|
||||
return strategies
|
||||
|
||||
def find_by_platform_and_method(self, platform: str, method_name: str) -> Optional[MethodStrategy]:
|
||||
"""Find a specific method strategy"""
|
||||
query = "SELECT * FROM method_strategies WHERE platform = ? AND method_name = ?"
|
||||
result = self.db_manager.fetch_one(query, (platform, method_name))
|
||||
return self._row_to_strategy(result) if result else None
|
||||
|
||||
def update_performance_metrics(self, strategy_id: str, success: bool,
|
||||
execution_time: float = 0.0) -> None:
|
||||
"""Update performance metrics for a strategy"""
|
||||
strategy = self.find_by_id(strategy_id)
|
||||
if not strategy:
|
||||
return
|
||||
|
||||
strategy.update_performance(success, execution_time)
|
||||
self.save(strategy)
|
||||
|
||||
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"""
|
||||
if excluded_methods is None:
|
||||
excluded_methods = []
|
||||
|
||||
# Build query with exclusions
|
||||
placeholders = ','.join(['?' for _ in excluded_methods])
|
||||
exclusion_clause = f"AND method_name NOT IN ({placeholders})" if excluded_methods else ""
|
||||
|
||||
# Build risk level clause
|
||||
risk_clause = "'LOW', 'MEDIUM'"
|
||||
if max_risk_level == 'HIGH':
|
||||
risk_clause += ", 'HIGH'"
|
||||
|
||||
query = f"""
|
||||
SELECT * FROM method_strategies
|
||||
WHERE platform = ?
|
||||
AND is_active = 1
|
||||
AND risk_level IN ({risk_clause})
|
||||
{exclusion_clause}
|
||||
ORDER BY priority DESC, success_rate DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
|
||||
params = [platform] + excluded_methods
|
||||
result = self.db_manager.fetch_one(query, params)
|
||||
|
||||
if not result:
|
||||
return None
|
||||
|
||||
strategy = self._row_to_strategy(result)
|
||||
|
||||
# Check if method is on cooldown
|
||||
if strategy.is_on_cooldown:
|
||||
# Try to find another method
|
||||
excluded_methods.append(strategy.method_name)
|
||||
return self.get_next_available_method(platform, excluded_methods, max_risk_level)
|
||||
|
||||
return strategy
|
||||
|
||||
def disable_method(self, platform: str, method_name: str, reason: str) -> None:
|
||||
"""Disable a method temporarily or permanently"""
|
||||
query = """
|
||||
UPDATE method_strategies
|
||||
SET is_active = 0, updated_at = ?
|
||||
WHERE platform = ? AND method_name = ?
|
||||
"""
|
||||
self.db_manager.execute_query(query, (datetime.now().isoformat(), platform, method_name))
|
||||
|
||||
# Log the reason in configuration
|
||||
strategy = self.find_by_platform_and_method(platform, method_name)
|
||||
if strategy:
|
||||
strategy.configuration['disabled_reason'] = reason
|
||||
strategy.configuration['disabled_at'] = datetime.now().isoformat()
|
||||
self.save(strategy)
|
||||
|
||||
def enable_method(self, platform: str, method_name: str) -> None:
|
||||
"""Re-enable a disabled method"""
|
||||
query = """
|
||||
UPDATE method_strategies
|
||||
SET is_active = 1, updated_at = ?
|
||||
WHERE platform = ? AND method_name = ?
|
||||
"""
|
||||
self.db_manager.execute_query(query, (datetime.now().isoformat(), platform, method_name))
|
||||
|
||||
# Clear disabled reason from configuration
|
||||
strategy = self.find_by_platform_and_method(platform, method_name)
|
||||
if strategy:
|
||||
strategy.configuration.pop('disabled_reason', None)
|
||||
strategy.configuration.pop('disabled_at', None)
|
||||
self.save(strategy)
|
||||
|
||||
def get_platform_statistics(self, platform: str) -> Dict[str, Any]:
|
||||
"""Get aggregated statistics for all methods on a platform"""
|
||||
query = """
|
||||
SELECT
|
||||
COUNT(*) as total_methods,
|
||||
COUNT(CASE WHEN is_active = 1 THEN 1 END) as active_methods,
|
||||
AVG(success_rate) as avg_success_rate,
|
||||
MAX(success_rate) as best_success_rate,
|
||||
MIN(success_rate) as worst_success_rate,
|
||||
AVG(priority) as avg_priority,
|
||||
COUNT(CASE WHEN last_success > datetime('now', '-24 hours') THEN 1 END) as recent_successes,
|
||||
COUNT(CASE WHEN last_failure > datetime('now', '-24 hours') THEN 1 END) as recent_failures
|
||||
FROM method_strategies
|
||||
WHERE platform = ?
|
||||
"""
|
||||
|
||||
result = self.db_manager.fetch_one(query, (platform,))
|
||||
|
||||
if not result:
|
||||
return {}
|
||||
|
||||
return {
|
||||
'total_methods': result[0] or 0,
|
||||
'active_methods': result[1] or 0,
|
||||
'avg_success_rate': round(result[2] or 0.0, 3),
|
||||
'best_success_rate': result[3] or 0.0,
|
||||
'worst_success_rate': result[4] or 0.0,
|
||||
'avg_priority': round(result[5] or 0.0, 1),
|
||||
'recent_successes_24h': result[6] or 0,
|
||||
'recent_failures_24h': result[7] or 0
|
||||
}
|
||||
|
||||
def cleanup_old_data(self, days_to_keep: int = 90) -> int:
|
||||
"""Clean up old performance data and return number of records removed"""
|
||||
# This implementation doesn't remove strategies but resets old performance data
|
||||
cutoff_date = datetime.now() - timedelta(days=days_to_keep)
|
||||
|
||||
query = """
|
||||
UPDATE method_strategies
|
||||
SET last_success = NULL, last_failure = NULL, success_rate = 0.0, failure_rate = 0.0
|
||||
WHERE (last_success < ? OR last_failure < ?)
|
||||
AND (last_success IS NOT NULL OR last_failure IS NOT NULL)
|
||||
"""
|
||||
|
||||
cursor = self.db_manager.execute_query(query, (cutoff_date.isoformat(), cutoff_date.isoformat()))
|
||||
return cursor.rowcount if cursor else 0
|
||||
|
||||
def get_methods_by_risk_level(self, platform: str, risk_level: RiskLevel) -> List[MethodStrategy]:
|
||||
"""Get methods filtered by risk level"""
|
||||
query = """
|
||||
SELECT * FROM method_strategies
|
||||
WHERE platform = ? AND risk_level = ? AND is_active = 1
|
||||
ORDER BY priority DESC, success_rate DESC
|
||||
"""
|
||||
results = self.db_manager.fetch_all(query, (platform, risk_level.value))
|
||||
return [self._row_to_strategy(row) for row in results]
|
||||
|
||||
def get_emergency_methods(self, platform: str) -> List[MethodStrategy]:
|
||||
"""Get only the most reliable methods for emergency mode"""
|
||||
query = """
|
||||
SELECT * FROM method_strategies
|
||||
WHERE platform = ?
|
||||
AND is_active = 1
|
||||
AND risk_level = 'LOW'
|
||||
AND success_rate > 0.5
|
||||
ORDER BY success_rate DESC, priority DESC
|
||||
LIMIT 2
|
||||
"""
|
||||
results = self.db_manager.fetch_all(query, (platform,))
|
||||
return [self._row_to_strategy(row) for row in results]
|
||||
|
||||
def bulk_update_priorities(self, platform: str, priority_updates: Dict[str, int]) -> None:
|
||||
"""Bulk update method priorities for a platform"""
|
||||
query = """
|
||||
UPDATE method_strategies
|
||||
SET priority = ?, updated_at = ?
|
||||
WHERE platform = ? AND method_name = ?
|
||||
"""
|
||||
|
||||
params_list = [
|
||||
(priority, datetime.now().isoformat(), platform, method_name)
|
||||
for method_name, priority in priority_updates.items()
|
||||
]
|
||||
|
||||
with self.db_manager.get_connection() as conn:
|
||||
conn.executemany(query, params_list)
|
||||
conn.commit()
|
||||
|
||||
def _row_to_strategy(self, row) -> MethodStrategy:
|
||||
"""Convert database row to MethodStrategy entity"""
|
||||
return MethodStrategy(
|
||||
strategy_id=row[0],
|
||||
platform=row[1],
|
||||
method_name=row[2],
|
||||
priority=row[3],
|
||||
success_rate=row[4],
|
||||
failure_rate=row[5],
|
||||
last_success=datetime.fromisoformat(row[6]) if row[6] else None,
|
||||
last_failure=datetime.fromisoformat(row[7]) if row[7] else None,
|
||||
cooldown_period=row[8],
|
||||
max_daily_attempts=row[9],
|
||||
risk_level=RiskLevel(row[10]),
|
||||
is_active=bool(row[11]),
|
||||
configuration=json.loads(row[12]) if row[12] else {},
|
||||
tags=json.loads(row[13]) if row[13] else [],
|
||||
created_at=datetime.fromisoformat(row[14]),
|
||||
updated_at=datetime.fromisoformat(row[15])
|
||||
)
|
||||
233
infrastructure/repositories/platform_method_state_repository.py
Normale Datei
233
infrastructure/repositories/platform_method_state_repository.py
Normale Datei
@ -0,0 +1,233 @@
|
||||
"""
|
||||
SQLite implementation of platform method state repository.
|
||||
Handles persistence and retrieval of platform-specific rotation states.
|
||||
"""
|
||||
|
||||
import json
|
||||
from datetime import datetime, date
|
||||
from typing import List, Optional
|
||||
|
||||
from domain.entities.method_rotation import PlatformMethodState, RotationStrategy
|
||||
from domain.repositories.method_rotation_repository import IPlatformMethodStateRepository
|
||||
from database.db_manager import DatabaseManager
|
||||
|
||||
|
||||
class PlatformMethodStateRepository(IPlatformMethodStateRepository):
|
||||
"""SQLite implementation of platform method state repository"""
|
||||
|
||||
def __init__(self, db_manager):
|
||||
self.db_manager = db_manager
|
||||
|
||||
def save(self, state: PlatformMethodState) -> None:
|
||||
"""Save or update platform method state"""
|
||||
state.updated_at = datetime.now()
|
||||
|
||||
query = """
|
||||
INSERT OR REPLACE INTO platform_method_states (
|
||||
id, platform, last_successful_method, last_successful_at,
|
||||
preferred_methods, blocked_methods, daily_attempt_counts,
|
||||
reset_date, rotation_strategy, emergency_mode, metadata, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
state_id = f"state_{state.platform}"
|
||||
|
||||
params = (
|
||||
state_id,
|
||||
state.platform,
|
||||
state.last_successful_method,
|
||||
state.last_successful_at.isoformat() if state.last_successful_at else None,
|
||||
json.dumps(state.preferred_methods),
|
||||
json.dumps(state.blocked_methods),
|
||||
json.dumps(state.daily_attempt_counts),
|
||||
state.reset_date.isoformat(),
|
||||
state.rotation_strategy.value,
|
||||
state.emergency_mode,
|
||||
json.dumps(state.metadata),
|
||||
state.updated_at.isoformat()
|
||||
)
|
||||
|
||||
self.db_manager.execute_query(query, params)
|
||||
|
||||
def find_by_platform(self, platform: str) -> Optional[PlatformMethodState]:
|
||||
"""Find method state for a platform"""
|
||||
query = "SELECT * FROM platform_method_states WHERE platform = ?"
|
||||
result = self.db_manager.fetch_one(query, (platform,))
|
||||
return self._row_to_state(result) if result else None
|
||||
|
||||
def get_or_create_state(self, platform: str) -> PlatformMethodState:
|
||||
"""Get existing state or create new one with defaults"""
|
||||
state = self.find_by_platform(platform)
|
||||
if state:
|
||||
return state
|
||||
|
||||
# Create new state with defaults
|
||||
new_state = PlatformMethodState(
|
||||
platform=platform,
|
||||
preferred_methods=self._get_default_methods(platform),
|
||||
rotation_strategy=RotationStrategy.ADAPTIVE,
|
||||
reset_date=datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
)
|
||||
self.save(new_state)
|
||||
return new_state
|
||||
|
||||
def update_daily_attempts(self, platform: str, method_name: str) -> None:
|
||||
"""Increment daily attempt counter for a method"""
|
||||
state = self.get_or_create_state(platform)
|
||||
state.increment_daily_attempts(method_name)
|
||||
self.save(state)
|
||||
|
||||
def reset_daily_counters(self, platform: str) -> None:
|
||||
"""Reset daily attempt counters (typically called at midnight)"""
|
||||
state = self.find_by_platform(platform)
|
||||
if not state:
|
||||
return
|
||||
|
||||
state.daily_attempt_counts = {}
|
||||
state.reset_date = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
self.save(state)
|
||||
|
||||
def block_method(self, platform: str, method_name: str, reason: str) -> None:
|
||||
"""Block a method temporarily"""
|
||||
state = self.get_or_create_state(platform)
|
||||
state.block_method(method_name, reason)
|
||||
self.save(state)
|
||||
|
||||
def unblock_method(self, platform: str, method_name: str) -> None:
|
||||
"""Unblock a previously blocked method"""
|
||||
state = self.get_or_create_state(platform)
|
||||
state.unblock_method(method_name)
|
||||
self.save(state)
|
||||
|
||||
def record_method_success(self, platform: str, method_name: str) -> None:
|
||||
"""Record successful method execution"""
|
||||
state = self.get_or_create_state(platform)
|
||||
state.record_success(method_name)
|
||||
self.save(state)
|
||||
|
||||
def get_preferred_method_order(self, platform: str) -> List[str]:
|
||||
"""Get preferred method order for a platform"""
|
||||
state = self.find_by_platform(platform)
|
||||
if not state:
|
||||
return self._get_default_methods(platform)
|
||||
return state.preferred_methods
|
||||
|
||||
def set_emergency_mode(self, platform: str, enabled: bool) -> None:
|
||||
"""Enable/disable emergency mode for a platform"""
|
||||
state = self.get_or_create_state(platform)
|
||||
state.emergency_mode = enabled
|
||||
|
||||
if enabled:
|
||||
# In emergency mode, prefer only low-risk methods
|
||||
state.metadata['emergency_activated_at'] = datetime.now().isoformat()
|
||||
state.metadata['pre_emergency_preferred'] = state.preferred_methods.copy()
|
||||
# Filter to only include low-risk methods
|
||||
emergency_methods = [m for m in state.preferred_methods if m in ['email', 'standard_registration']]
|
||||
if emergency_methods:
|
||||
state.preferred_methods = emergency_methods
|
||||
else:
|
||||
# Restore previous preferred methods
|
||||
if 'pre_emergency_preferred' in state.metadata:
|
||||
state.preferred_methods = state.metadata.pop('pre_emergency_preferred')
|
||||
state.metadata.pop('emergency_activated_at', None)
|
||||
|
||||
self.save(state)
|
||||
|
||||
def get_daily_attempt_counts(self, platform: str) -> dict:
|
||||
"""Get current daily attempt counts for all methods"""
|
||||
state = self.find_by_platform(platform)
|
||||
if not state:
|
||||
return {}
|
||||
return state.daily_attempt_counts.copy()
|
||||
|
||||
def is_method_available(self, platform: str, method_name: str, max_daily_attempts: int) -> bool:
|
||||
"""Check if a method is available for use"""
|
||||
state = self.find_by_platform(platform)
|
||||
if not state:
|
||||
return True
|
||||
return state.is_method_available(method_name, max_daily_attempts)
|
||||
|
||||
def get_blocked_methods(self, platform: str) -> List[str]:
|
||||
"""Get list of currently blocked methods"""
|
||||
state = self.find_by_platform(platform)
|
||||
if not state:
|
||||
return []
|
||||
return state.blocked_methods.copy()
|
||||
|
||||
def update_rotation_strategy(self, platform: str, strategy: RotationStrategy) -> None:
|
||||
"""Update rotation strategy for a platform"""
|
||||
state = self.get_or_create_state(platform)
|
||||
state.rotation_strategy = strategy
|
||||
state.metadata['strategy_changed_at'] = datetime.now().isoformat()
|
||||
self.save(state)
|
||||
|
||||
def bulk_reset_daily_counters(self) -> int:
|
||||
"""Reset daily counters for all platforms (maintenance operation)"""
|
||||
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
query = """
|
||||
UPDATE platform_method_states
|
||||
SET daily_attempt_counts = '{}',
|
||||
reset_date = ?,
|
||||
updated_at = ?
|
||||
WHERE reset_date < ?
|
||||
"""
|
||||
|
||||
cursor = self.db_manager.execute_query(query, (
|
||||
today.isoformat(),
|
||||
datetime.now().isoformat(),
|
||||
today.isoformat()
|
||||
))
|
||||
return cursor.rowcount if cursor else 0
|
||||
|
||||
def get_all_platform_states(self) -> List[PlatformMethodState]:
|
||||
"""Get states for all platforms"""
|
||||
query = "SELECT * FROM platform_method_states ORDER BY platform"
|
||||
results = self.db_manager.fetch_all(query)
|
||||
return [self._row_to_state(row) for row in results]
|
||||
|
||||
def cleanup_emergency_modes(self, hours_threshold: int = 24) -> int:
|
||||
"""Automatically disable emergency modes that have been active too long"""
|
||||
cutoff_time = datetime.now() - datetime.timedelta(hours=hours_threshold)
|
||||
|
||||
query = """
|
||||
SELECT platform FROM platform_method_states
|
||||
WHERE emergency_mode = 1
|
||||
AND JSON_EXTRACT(metadata, '$.emergency_activated_at') < ?
|
||||
"""
|
||||
|
||||
results = self.db_manager.fetch_all(query, (cutoff_time.isoformat(),))
|
||||
count = 0
|
||||
|
||||
for row in results:
|
||||
platform = row[0]
|
||||
self.set_emergency_mode(platform, False)
|
||||
count += 1
|
||||
|
||||
return count
|
||||
|
||||
def _row_to_state(self, row) -> PlatformMethodState:
|
||||
"""Convert database row to PlatformMethodState entity"""
|
||||
return PlatformMethodState(
|
||||
platform=row[1],
|
||||
last_successful_method=row[2],
|
||||
last_successful_at=datetime.fromisoformat(row[3]) if row[3] else None,
|
||||
preferred_methods=json.loads(row[4]) if row[4] else [],
|
||||
blocked_methods=json.loads(row[5]) if row[5] else [],
|
||||
daily_attempt_counts=json.loads(row[6]) if row[6] else {},
|
||||
reset_date=datetime.fromisoformat(row[7]),
|
||||
rotation_strategy=RotationStrategy(row[8]),
|
||||
emergency_mode=bool(row[9]),
|
||||
metadata=json.loads(row[10]) if row[10] else {},
|
||||
updated_at=datetime.fromisoformat(row[11])
|
||||
)
|
||||
|
||||
def _get_default_methods(self, platform: str) -> List[str]:
|
||||
"""Get default method order for a platform"""
|
||||
default_methods = {
|
||||
'instagram': ['email', 'phone', 'social_login'],
|
||||
'tiktok': ['email', 'phone'],
|
||||
'x': ['email', 'phone'],
|
||||
'gmail': ['standard_registration', 'recovery_registration']
|
||||
}
|
||||
return default_methods.get(platform, ['email'])
|
||||
252
infrastructure/repositories/rate_limit_repository.py
Normale Datei
252
infrastructure/repositories/rate_limit_repository.py
Normale Datei
@ -0,0 +1,252 @@
|
||||
"""
|
||||
Rate Limit Repository - Persistierung von Rate Limit Events und Policies
|
||||
"""
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime, timedelta
|
||||
from collections import defaultdict
|
||||
|
||||
from infrastructure.repositories.base_repository import BaseRepository
|
||||
from domain.entities.rate_limit_policy import RateLimitPolicy
|
||||
from domain.value_objects.action_timing import ActionTiming, ActionType
|
||||
|
||||
|
||||
class RateLimitRepository(BaseRepository):
|
||||
"""Repository für Rate Limit Daten"""
|
||||
|
||||
def save_timing(self, timing: ActionTiming) -> None:
|
||||
"""Speichert ein Action Timing Event"""
|
||||
query = """
|
||||
INSERT INTO rate_limit_events (
|
||||
timestamp, action_type, duration_ms, success, response_code,
|
||||
session_id, url, element_selector, error_message, retry_count, metadata
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
params = (
|
||||
timing.timestamp,
|
||||
timing.action_type.value,
|
||||
int(timing.duration_ms),
|
||||
timing.success,
|
||||
timing.metadata.get('response_code') if timing.metadata else None,
|
||||
timing.metadata.get('session_id') if timing.metadata else None,
|
||||
timing.url,
|
||||
timing.element_selector,
|
||||
timing.error_message,
|
||||
timing.retry_count,
|
||||
self._serialize_json(timing.metadata) if timing.metadata else None
|
||||
)
|
||||
|
||||
self._execute_insert(query, params)
|
||||
|
||||
def get_recent_timings(self, action_type: Optional[ActionType] = None,
|
||||
hours: int = 1) -> List[ActionTiming]:
|
||||
"""Holt kürzliche Timing-Events"""
|
||||
query = """
|
||||
SELECT * FROM rate_limit_events
|
||||
WHERE timestamp > datetime('now', '-' || ? || ' hours')
|
||||
"""
|
||||
params = [hours]
|
||||
|
||||
if action_type:
|
||||
query += " AND action_type = ?"
|
||||
params.append(action_type.value)
|
||||
|
||||
query += " ORDER BY timestamp DESC"
|
||||
|
||||
rows = self._execute_query(query, tuple(params))
|
||||
|
||||
timings = []
|
||||
for row in rows:
|
||||
timing = ActionTiming(
|
||||
action_type=ActionType(row['action_type']),
|
||||
timestamp=self._parse_datetime(row['timestamp']),
|
||||
duration=row['duration_ms'] / 1000.0,
|
||||
success=bool(row['success']),
|
||||
url=row['url'],
|
||||
element_selector=row['element_selector'],
|
||||
error_message=row['error_message'],
|
||||
retry_count=row['retry_count'],
|
||||
metadata=self._deserialize_json(row['metadata'])
|
||||
)
|
||||
timings.append(timing)
|
||||
|
||||
return timings
|
||||
|
||||
def save_policy(self, action_type: ActionType, policy: RateLimitPolicy) -> None:
|
||||
"""Speichert oder aktualisiert eine Rate Limit Policy"""
|
||||
query = """
|
||||
INSERT OR REPLACE INTO rate_limit_policies (
|
||||
action_type, min_delay, max_delay, adaptive,
|
||||
backoff_multiplier, max_retries, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
params = (
|
||||
action_type.value,
|
||||
policy.min_delay,
|
||||
policy.max_delay,
|
||||
policy.adaptive,
|
||||
policy.backoff_multiplier,
|
||||
policy.max_retries,
|
||||
datetime.now()
|
||||
)
|
||||
|
||||
self._execute_insert(query, params)
|
||||
|
||||
def get_policy(self, action_type: ActionType) -> Optional[RateLimitPolicy]:
|
||||
"""Holt eine Rate Limit Policy"""
|
||||
query = "SELECT * FROM rate_limit_policies WHERE action_type = ?"
|
||||
rows = self._execute_query(query, (action_type.value,))
|
||||
|
||||
if not rows:
|
||||
return None
|
||||
|
||||
row = rows[0]
|
||||
return RateLimitPolicy(
|
||||
min_delay=row['min_delay'],
|
||||
max_delay=row['max_delay'],
|
||||
adaptive=bool(row['adaptive']),
|
||||
backoff_multiplier=row['backoff_multiplier'],
|
||||
max_retries=row['max_retries']
|
||||
)
|
||||
|
||||
def get_all_policies(self) -> Dict[ActionType, RateLimitPolicy]:
|
||||
"""Holt alle gespeicherten Policies"""
|
||||
query = "SELECT * FROM rate_limit_policies"
|
||||
rows = self._execute_query(query)
|
||||
|
||||
policies = {}
|
||||
for row in rows:
|
||||
try:
|
||||
action_type = ActionType(row['action_type'])
|
||||
policy = RateLimitPolicy(
|
||||
min_delay=row['min_delay'],
|
||||
max_delay=row['max_delay'],
|
||||
adaptive=bool(row['adaptive']),
|
||||
backoff_multiplier=row['backoff_multiplier'],
|
||||
max_retries=row['max_retries']
|
||||
)
|
||||
policies[action_type] = policy
|
||||
except ValueError:
|
||||
# Unbekannter ActionType
|
||||
pass
|
||||
|
||||
return policies
|
||||
|
||||
def get_statistics(self, action_type: Optional[ActionType] = None,
|
||||
timeframe: Optional[timedelta] = None) -> Dict[str, Any]:
|
||||
"""Berechnet Statistiken über Rate Limiting"""
|
||||
query = """
|
||||
SELECT
|
||||
action_type,
|
||||
COUNT(*) as total_actions,
|
||||
AVG(duration_ms) as avg_duration_ms,
|
||||
MIN(duration_ms) as min_duration_ms,
|
||||
MAX(duration_ms) as max_duration_ms,
|
||||
SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successful_actions,
|
||||
SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) as failed_actions,
|
||||
AVG(retry_count) as avg_retry_count,
|
||||
MAX(retry_count) as max_retry_count
|
||||
FROM rate_limit_events
|
||||
WHERE 1=1
|
||||
"""
|
||||
params = []
|
||||
|
||||
if timeframe:
|
||||
query += " AND timestamp > datetime('now', '-' || ? || ' seconds')"
|
||||
params.append(int(timeframe.total_seconds()))
|
||||
|
||||
if action_type:
|
||||
query += " AND action_type = ?"
|
||||
params.append(action_type.value)
|
||||
query += " GROUP BY action_type"
|
||||
else:
|
||||
query += " GROUP BY action_type"
|
||||
|
||||
rows = self._execute_query(query, tuple(params))
|
||||
|
||||
if action_type and rows:
|
||||
# Einzelne Action Type Statistik
|
||||
row = rows[0]
|
||||
return {
|
||||
'action_type': row['action_type'],
|
||||
'total_actions': row['total_actions'],
|
||||
'avg_duration_ms': row['avg_duration_ms'],
|
||||
'min_duration_ms': row['min_duration_ms'],
|
||||
'max_duration_ms': row['max_duration_ms'],
|
||||
'success_rate': row['successful_actions'] / row['total_actions'] if row['total_actions'] > 0 else 0,
|
||||
'failed_actions': row['failed_actions'],
|
||||
'avg_retry_count': row['avg_retry_count'],
|
||||
'max_retry_count': row['max_retry_count']
|
||||
}
|
||||
else:
|
||||
# Statistiken für alle Action Types
|
||||
stats = {}
|
||||
for row in rows:
|
||||
stats[row['action_type']] = {
|
||||
'total_actions': row['total_actions'],
|
||||
'avg_duration_ms': row['avg_duration_ms'],
|
||||
'min_duration_ms': row['min_duration_ms'],
|
||||
'max_duration_ms': row['max_duration_ms'],
|
||||
'success_rate': row['successful_actions'] / row['total_actions'] if row['total_actions'] > 0 else 0,
|
||||
'failed_actions': row['failed_actions'],
|
||||
'avg_retry_count': row['avg_retry_count'],
|
||||
'max_retry_count': row['max_retry_count']
|
||||
}
|
||||
return stats
|
||||
|
||||
def detect_anomalies(self, action_type: ActionType,
|
||||
threshold_multiplier: float = 2.0) -> List[Dict[str, Any]]:
|
||||
"""Erkennt Anomalien in den Timing-Daten"""
|
||||
# Berechne Durchschnitt und Standardabweichung
|
||||
query = """
|
||||
SELECT
|
||||
AVG(duration_ms) as avg_duration,
|
||||
AVG(duration_ms * duration_ms) - AVG(duration_ms) * AVG(duration_ms) as variance
|
||||
FROM rate_limit_events
|
||||
WHERE action_type = ?
|
||||
AND timestamp > datetime('now', '-1 hour')
|
||||
AND success = 1
|
||||
"""
|
||||
|
||||
row = self._execute_query(query, (action_type.value,))[0]
|
||||
|
||||
if not row['avg_duration']:
|
||||
return []
|
||||
|
||||
avg_duration = row['avg_duration']
|
||||
std_dev = (row['variance'] ** 0.5) if row['variance'] > 0 else 0
|
||||
threshold = avg_duration + (std_dev * threshold_multiplier)
|
||||
|
||||
# Finde Anomalien
|
||||
query = """
|
||||
SELECT * FROM rate_limit_events
|
||||
WHERE action_type = ?
|
||||
AND timestamp > datetime('now', '-1 hour')
|
||||
AND duration_ms > ?
|
||||
ORDER BY duration_ms DESC
|
||||
LIMIT 10
|
||||
"""
|
||||
|
||||
rows = self._execute_query(query, (action_type.value, threshold))
|
||||
|
||||
anomalies = []
|
||||
for row in rows:
|
||||
anomalies.append({
|
||||
'timestamp': row['timestamp'],
|
||||
'duration_ms': row['duration_ms'],
|
||||
'deviation': (row['duration_ms'] - avg_duration) / std_dev if std_dev > 0 else 0,
|
||||
'success': bool(row['success']),
|
||||
'url': row['url'],
|
||||
'error_message': row['error_message']
|
||||
})
|
||||
|
||||
return anomalies
|
||||
|
||||
def cleanup_old_events(self, older_than: datetime) -> int:
|
||||
"""Bereinigt alte Rate Limit Events"""
|
||||
query = "DELETE FROM rate_limit_events WHERE timestamp < ?"
|
||||
return self._execute_delete(query, (older_than,))
|
||||
254
infrastructure/repositories/rotation_session_repository.py
Normale Datei
254
infrastructure/repositories/rotation_session_repository.py
Normale Datei
@ -0,0 +1,254 @@
|
||||
"""
|
||||
SQLite implementation of rotation session repository.
|
||||
Handles persistence and retrieval of rotation sessions.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
from domain.entities.method_rotation import RotationSession
|
||||
from domain.repositories.method_rotation_repository import IRotationSessionRepository
|
||||
from database.db_manager import DatabaseManager
|
||||
|
||||
|
||||
class RotationSessionRepository(IRotationSessionRepository):
|
||||
"""SQLite implementation of rotation session repository"""
|
||||
|
||||
def __init__(self, db_manager):
|
||||
self.db_manager = db_manager
|
||||
|
||||
def save(self, session: RotationSession) -> None:
|
||||
"""Save or update a rotation session"""
|
||||
query = """
|
||||
INSERT OR REPLACE INTO rotation_sessions (
|
||||
id, platform, account_id, current_method, attempted_methods,
|
||||
session_start, last_rotation, rotation_count, success_count,
|
||||
failure_count, is_active, rotation_reason, fingerprint_id, session_metadata
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
params = (
|
||||
session.session_id,
|
||||
session.platform,
|
||||
session.account_id,
|
||||
session.current_method,
|
||||
json.dumps(session.attempted_methods),
|
||||
session.session_start.isoformat(),
|
||||
session.last_rotation.isoformat() if session.last_rotation else None,
|
||||
session.rotation_count,
|
||||
session.success_count,
|
||||
session.failure_count,
|
||||
session.is_active,
|
||||
session.rotation_reason,
|
||||
session.fingerprint_id,
|
||||
json.dumps(session.session_metadata)
|
||||
)
|
||||
|
||||
self.db_manager.execute_query(query, params)
|
||||
|
||||
def find_by_id(self, session_id: str) -> Optional[RotationSession]:
|
||||
"""Find a session by its ID"""
|
||||
query = "SELECT * FROM rotation_sessions WHERE id = ?"
|
||||
result = self.db_manager.fetch_one(query, (session_id,))
|
||||
return self._row_to_session(result) if result else None
|
||||
|
||||
def find_active_session(self, platform: str, account_id: Optional[str] = None) -> Optional[RotationSession]:
|
||||
"""Find an active session for a platform/account"""
|
||||
if account_id:
|
||||
query = """
|
||||
SELECT * FROM rotation_sessions
|
||||
WHERE platform = ? AND account_id = ? AND is_active = 1
|
||||
ORDER BY session_start DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
params = (platform, account_id)
|
||||
else:
|
||||
query = """
|
||||
SELECT * FROM rotation_sessions
|
||||
WHERE platform = ? AND is_active = 1
|
||||
ORDER BY session_start DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
params = (platform,)
|
||||
|
||||
result = self.db_manager.fetch_one(query, params)
|
||||
return self._row_to_session(result) if result else None
|
||||
|
||||
def find_active_sessions_by_platform(self, platform: str) -> List[RotationSession]:
|
||||
"""Find all active sessions for a platform"""
|
||||
query = """
|
||||
SELECT * FROM rotation_sessions
|
||||
WHERE platform = ? AND is_active = 1
|
||||
ORDER BY session_start DESC
|
||||
"""
|
||||
results = self.db_manager.fetch_all(query, (platform,))
|
||||
return [self._row_to_session(row) for row in results]
|
||||
|
||||
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"""
|
||||
session = self.find_by_id(session_id)
|
||||
if not session:
|
||||
return
|
||||
|
||||
session.add_attempt(method_name, success, error_message)
|
||||
self.save(session)
|
||||
|
||||
def archive_session(self, session_id: str, final_success: bool = False) -> None:
|
||||
"""Mark a session as completed/archived"""
|
||||
session = self.find_by_id(session_id)
|
||||
if not session:
|
||||
return
|
||||
|
||||
session.complete_session(final_success)
|
||||
self.save(session)
|
||||
|
||||
def get_session_history(self, platform: str, limit: int = 100) -> List[RotationSession]:
|
||||
"""Get recent session history for a platform"""
|
||||
query = """
|
||||
SELECT * FROM rotation_sessions
|
||||
WHERE platform = ?
|
||||
ORDER BY session_start DESC
|
||||
LIMIT ?
|
||||
"""
|
||||
results = self.db_manager.fetch_all(query, (platform, limit))
|
||||
return [self._row_to_session(row) for row in results]
|
||||
|
||||
def get_session_statistics(self, platform: str, days: int = 30) -> Dict[str, Any]:
|
||||
"""Get session statistics for a platform over specified days"""
|
||||
cutoff_date = datetime.now() - timedelta(days=days)
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
COUNT(*) as total_sessions,
|
||||
COUNT(CASE WHEN is_active = 1 THEN 1 END) as active_sessions,
|
||||
COUNT(CASE WHEN is_active = 0 AND JSON_EXTRACT(session_metadata, '$.final_success') = 1 THEN 1 END) as successful_sessions,
|
||||
COUNT(CASE WHEN is_active = 0 AND JSON_EXTRACT(session_metadata, '$.final_success') = 0 THEN 1 END) as failed_sessions,
|
||||
AVG(rotation_count) as avg_rotations,
|
||||
MAX(rotation_count) as max_rotations,
|
||||
AVG(success_count + failure_count) as avg_attempts,
|
||||
AVG(CASE WHEN success_count + failure_count > 0 THEN success_count * 1.0 / (success_count + failure_count) ELSE 0 END) as avg_success_rate
|
||||
FROM rotation_sessions
|
||||
WHERE platform = ? AND session_start >= ?
|
||||
"""
|
||||
|
||||
result = self.db_manager.fetch_one(query, (platform, cutoff_date.isoformat()))
|
||||
|
||||
if not result:
|
||||
return {}
|
||||
|
||||
return {
|
||||
'total_sessions': result[0] or 0,
|
||||
'active_sessions': result[1] or 0,
|
||||
'successful_sessions': result[2] or 0,
|
||||
'failed_sessions': result[3] or 0,
|
||||
'avg_rotations_per_session': round(result[4] or 0.0, 2),
|
||||
'max_rotations_in_session': result[5] or 0,
|
||||
'avg_attempts_per_session': round(result[6] or 0.0, 2),
|
||||
'avg_session_success_rate': round(result[7] or 0.0, 3)
|
||||
}
|
||||
|
||||
def cleanup_old_sessions(self, days_to_keep: int = 30) -> int:
|
||||
"""Clean up old session data and return number of records removed"""
|
||||
cutoff_date = datetime.now() - timedelta(days=days_to_keep)
|
||||
|
||||
query = """
|
||||
DELETE FROM rotation_sessions
|
||||
WHERE is_active = 0 AND session_start < ?
|
||||
"""
|
||||
|
||||
cursor = self.db_manager.execute_query(query, (cutoff_date.isoformat(),))
|
||||
return cursor.rowcount if cursor else 0
|
||||
|
||||
def get_method_usage_statistics(self, platform: str, days: int = 30) -> Dict[str, Any]:
|
||||
"""Get method usage statistics from sessions"""
|
||||
cutoff_date = datetime.now() - timedelta(days=days)
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
current_method,
|
||||
COUNT(*) as usage_count,
|
||||
AVG(success_count) as avg_success_count,
|
||||
AVG(failure_count) as avg_failure_count,
|
||||
AVG(rotation_count) as avg_rotation_count
|
||||
FROM rotation_sessions
|
||||
WHERE platform = ? AND session_start >= ?
|
||||
GROUP BY current_method
|
||||
ORDER BY usage_count DESC
|
||||
"""
|
||||
|
||||
results = self.db_manager.fetch_all(query, (platform, cutoff_date.isoformat()))
|
||||
|
||||
method_stats = {}
|
||||
for row in results:
|
||||
method_stats[row[0]] = {
|
||||
'usage_count': row[1],
|
||||
'avg_success_count': round(row[2] or 0.0, 2),
|
||||
'avg_failure_count': round(row[3] or 0.0, 2),
|
||||
'avg_rotation_count': round(row[4] or 0.0, 2)
|
||||
}
|
||||
|
||||
return method_stats
|
||||
|
||||
def find_sessions_by_fingerprint(self, fingerprint_id: str) -> List[RotationSession]:
|
||||
"""Find sessions associated with a specific fingerprint"""
|
||||
query = """
|
||||
SELECT * FROM rotation_sessions
|
||||
WHERE fingerprint_id = ?
|
||||
ORDER BY session_start DESC
|
||||
"""
|
||||
results = self.db_manager.fetch_all(query, (fingerprint_id,))
|
||||
return [self._row_to_session(row) for row in results]
|
||||
|
||||
def get_long_running_sessions(self, hours: int = 24) -> List[RotationSession]:
|
||||
"""Find sessions that have been running for too long"""
|
||||
cutoff_time = datetime.now() - timedelta(hours=hours)
|
||||
|
||||
query = """
|
||||
SELECT * FROM rotation_sessions
|
||||
WHERE is_active = 1 AND session_start < ?
|
||||
ORDER BY session_start ASC
|
||||
"""
|
||||
|
||||
results = self.db_manager.fetch_all(query, (cutoff_time.isoformat(),))
|
||||
return [self._row_to_session(row) for row in results]
|
||||
|
||||
def force_archive_stale_sessions(self, hours: int = 24) -> int:
|
||||
"""Force archive sessions that have been running too long"""
|
||||
cutoff_time = datetime.now() - timedelta(hours=hours)
|
||||
|
||||
query = """
|
||||
UPDATE rotation_sessions
|
||||
SET is_active = 0,
|
||||
session_metadata = JSON_SET(
|
||||
session_metadata,
|
||||
'$.completed_at', ?,
|
||||
'$.final_success', 0,
|
||||
'$.force_archived', 1
|
||||
)
|
||||
WHERE is_active = 1 AND session_start < ?
|
||||
"""
|
||||
|
||||
cursor = self.db_manager.execute_query(query, (datetime.now().isoformat(), cutoff_time.isoformat()))
|
||||
return cursor.rowcount if cursor else 0
|
||||
|
||||
def _row_to_session(self, row) -> RotationSession:
|
||||
"""Convert database row to RotationSession entity"""
|
||||
return RotationSession(
|
||||
session_id=row[0],
|
||||
platform=row[1],
|
||||
account_id=row[2],
|
||||
current_method=row[3],
|
||||
attempted_methods=json.loads(row[4]) if row[4] else [],
|
||||
session_start=datetime.fromisoformat(row[5]),
|
||||
last_rotation=datetime.fromisoformat(row[6]) if row[6] else None,
|
||||
rotation_count=row[7],
|
||||
success_count=row[8],
|
||||
failure_count=row[9],
|
||||
is_active=bool(row[10]),
|
||||
rotation_reason=row[11],
|
||||
fingerprint_id=row[12],
|
||||
session_metadata=json.loads(row[13]) if row[13] else {}
|
||||
)
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren