from dataclasses import dataclass from emval import validate_email from pydantic import SecretStr from sqlalchemy import or_ from resonance_engine.adapters.db import db from resonance_engine.dsp.enums import DSPClientName, DSPId from resonance_engine.dsp.exceptions import UnknownDSPClientError from resonance_engine.dsp.models import DSPClient from resonance_engine.fandata import collector from resonance_engine.fandata.enums import FanConnectionStatus from resonance_engine.fandata.exceptions import ( FanConnectionNotFoundError, InvalidFanEmailError, ) from resonance_engine.fandata.models import ( FanConnection, FanConnectionRow, FanConnectionViewFilter, ) from resonance_engine.fandata.types import FanRecord, FanView, FanViewPaginated from resonance_engine.fandata.utils import make_fan_id from resonance_engine.utils.encrypter import encrypter @dataclass(kw_only=True) class GetFansRequest: dsp_id: DSPId | None = None dsp_client_names: list[DSPClientName] | None = None status: FanConnectionStatus | None = None search: str | None = None is_collected: bool | None = None cursor: str | None = None limit: int = 50 @db.autocommit def get_fans(request: GetFansRequest) -> FanViewPaginated: return FanConnection.query.view_paginate( FanConnectionViewFilter( dsp_id=request.dsp_id, dsp_client_names=( tuple(request.dsp_client_names) if request.dsp_client_names else None ), status=request.status, search=request.search, is_collected=request.is_collected, ), cursor=request.cursor, limit=request.limit, ) @dataclass(kw_only=True) class CreateFanConnectionRequest: email: str dsp_client_name: DSPClientName token: SecretStr @db.transaction def create_fan_connection(request: CreateFanConnectionRequest) -> FanConnection: try: validate_email(request.email, deliverable_address=False) except Exception as exc: raise InvalidFanEmailError from exc dsp_client = DSPClient.query.where( DSPClient.name == request.dsp_client_name ).one_or_none() if dsp_client is None: raise UnknownDSPClientError(request.dsp_client_name) fan_id = make_fan_id(request.email) FanConnection.query.connect( [ { "fan_id": fan_id, "dsp_id": dsp_client.dsp_id, "dsp_client_id": dsp_client.id, "token_encrypted": encrypter.encrypt(request.token.get_secret_value()), } ] ) return FanConnection.query.where( FanConnection.fan_id == fan_id, FanConnection.dsp_id == dsp_client.dsp_id, FanConnection.dsp_client_id == dsp_client.id, ).one() @dataclass(kw_only=True) class BatchCreateFanConnectionRequest: items: list[CreateFanConnectionRequest] @db.transaction def batch_create_fan_connections( request: BatchCreateFanConnectionRequest, ) -> int: clients: dict[DSPClientName, DSPClient] = { client.name: client for client in DSPClient.query.where().all() } rows: list[FanConnectionRow] = [] for item in request.items: try: # Syntax-only validation validate_email(item.email, deliverable_address=False) except Exception: continue if item.dsp_client_name not in clients: continue rows.append( { "fan_id": make_fan_id(item.email), "dsp_id": clients[item.dsp_client_name].dsp_id, "dsp_client_id": clients[item.dsp_client_name].id, "token_encrypted": encrypter.encrypt(item.token.get_secret_value()), } ) if not rows: return 0 FanConnection.query.connect(rows) return len(rows) @dataclass(kw_only=True) class CollectFanRequest: fan_id: str dsp_client_name: DSPClientName force: bool = False def collect_fan(request: CollectFanRequest) -> FanView: with db.autocommit(): dsp_client = DSPClient.query.where( DSPClient.name == request.dsp_client_name ).one_or_none() if dsp_client is None: raise UnknownDSPClientError(request.dsp_client_name) connection = FanConnection.query.where( # accept a raw fan_id or an email (hashed to its fan_id). or_( FanConnection.fan_id == request.fan_id, FanConnection.fan_id == make_fan_id(request.fan_id), ), FanConnection.dsp_client_id == dsp_client.id, ).one_or_none() if connection is None: raise FanConnectionNotFoundError( fan_id=request.fan_id, dsp_client_id=dsp_client.id, ) collector.collect_fan( fan=FanRecord( fan_id=connection.fan_id, token_encrypted=connection.token_encrypted, ), client=dsp_client, force=request.force, ) with db.autocommit(): result = FanConnection.query.view_paginate( FanConnectionViewFilter( dsp_id=dsp_client.dsp_id, dsp_client_id=dsp_client.id, search=connection.fan_id, ), limit=1, ) if not result.items: raise FanConnectionNotFoundError( fan_id=request.fan_id, dsp_client_id=dsp_client.id, ) return result.items[0]