"""fandata CLI commands.""" import csv import random import string import time from collections.abc import Iterator, Sequence import typer from app.adapters import aws_kms from app.adapters.db import db from app.core import encrypter from app.dsp.models import DSPClient from app.fandata.models import FanCredentials, FanCredentialsRow app = typer.Typer(no_args_is_help=True) def _token_pool() -> list[str]: return [ encrypter.encrypt("AQA" + "".join(random.choices(string.digits, k=32))) for _ in range(10) ] def _iter_rows( n: int, overlap: int, clients: Sequence[DSPClient], tokens: list[str], ) -> Iterator[FanCredentialsRow]: overlap_count = round(n * overlap / 100) single_count = n - overlap_count for i in range(overlap_count): user_id = f"fake_user_{i}" for client in clients: yield FanCredentialsRow( dsp_user_id=user_id, dsp_id=client.dsp_id, client_id=client.client_id, email=f"{user_id}@example.com", refresh_token_encrypted=tokens[i % len(tokens)], ) for i in range(single_count): user_id = f"fake_user_{overlap_count + i}" client = clients[i % len(clients)] yield FanCredentialsRow( dsp_user_id=user_id, dsp_id=client.dsp_id, client_id=client.client_id, email=f"{user_id}@example.com", refresh_token_encrypted=tokens[(overlap_count + i) % len(tokens)], ) @app.command("fill-creds") def fill_creds( n: int = typer.Option( 1000, "--limit", "-n", help="Number of unique users to generate" ), dsp_id: str | None = typer.Option( None, "--dsp-type", "-d", help="DSP type (default: all)" ), overlap: int = typer.Option( 0, "--overlap", "-o", min=0, max=100, help="% of users that get credentials for ALL DSP clients (0 = each user assigned to one client)", ), batch_size: int = typer.Option( 10_000, "--batch-size", "-b", help="Rows per INSERT batch" ), ) -> None: """Generate fan_credentials rows for dev/testing.""" with db.autocommit(): q = DSPClient.query if dsp_id is not None: q = q.where(DSPClient.dsp_id == dsp_id) clients: Sequence[DSPClient] = q.all() if not clients: typer.secho("No DSP clients found", fg=typer.colors.RED, err=True) raise typer.Exit(1) overlap_count = round(n * overlap / 100) single_count = n - overlap_count total_rows = overlap_count * len(clients) + single_count typer.echo( f"Users: {n:,} | overlap: {overlap}% ({overlap_count:,} users x {len(clients)} clients)" f" | total rows: {total_rows:,} | batch: {batch_size:,}" ) tokens = _token_pool() inserted = 0 batch: list[FanCredentialsRow] = [] t0 = time.monotonic() for row in _iter_rows(n, overlap, clients, tokens): batch.append(row) if len(batch) == batch_size: with db.transaction(): FanCredentials.query.upsert(batch) inserted += len(batch) batch = [] elapsed = time.monotonic() - t0 rps = round(inserted / elapsed) if elapsed > 0 else 0 typer.echo( f"\r {inserted:>12,} / {total_rows:,} ({rps:,} rows/s)", nl=False ) if batch: with db.transaction(): FanCredentials.query.upsert(batch) inserted += len(batch) elapsed = time.monotonic() - t0 rps = round(inserted / elapsed) if elapsed > 0 else 0 typer.secho( f"\nDone. {inserted:,} rows in {elapsed:.1f}s ({rps:,} rows/s).", fg=typer.colors.GREEN, ) @app.command("load-creds") def load_creds( csv_file: str = typer.Argument(help="Path to CSV file (e.g. songwhip_presave.csv)"), dsp_client_name: str = typer.Option( "spotify_songwhip", "--client", "-c", help="DSP client name" ), batch_size: int = typer.Option( 10_000, "--batch-size", "-b", help="Rows per INSERT batch" ), ) -> None: """Load fan credentials from a Songwhip presave CSV export.""" with db.autocommit(): client = DSPClient.query.where(DSPClient.name == dsp_client_name).one_or_none() if client is None: typer.secho( f"DSP client '{dsp_client_name}' not found", fg=typer.colors.RED, err=True ) raise typer.Exit(1) rows: list[FanCredentialsRow] = [] with open(csv_file, newline="") as f: all_rows = list(csv.DictReader(f)) with typer.progressbar(all_rows, label="Decrypting tokens") as progress: for row in progress: dsp_user_id = row["TASK_ID"].split("spotify-presave-", 1)[-1] rows.append( FanCredentialsRow( dsp_user_id=dsp_user_id, dsp_id=client.dsp_id, client_id=client.client_id, email=row["EMAIL"], refresh_token_encrypted=encrypter.encrypt( aws_kms.decrypt(row["TOKEN"]) ), ) ) inserted = 0 with typer.progressbar(length=len(rows), label="Upserting credentials") as progress: for batch_start in range(0, len(rows), batch_size): batch = rows[batch_start : batch_start + batch_size] with db.transaction(): FanCredentials.query.upsert(batch) inserted += len(batch) progress.update(len(batch)) typer.secho( f"\nDone. Inserted/updated {inserted:,} credentials.", fg=typer.colors.GREEN, )