""" DynamoDB Client for Heavy Rotation App Fetches Spotify presave refresh tokens from DynamoDB production table Ported from streamlit-dynamo-test/app_improved.py NOTE: No caching decorators - AWS credentials are temporary and expire """ import logging import streamlit as st import boto3 from boto3.dynamodb.types import TypeDeserializer from typing import List, Dict, Any, Optional, Tuple # Configure module logger logger = logging.getLogger(__name__) def load_aws_credentials() -> Tuple[Optional[str], Optional[str], str, Optional[str]]: """ Load AWS credentials from Streamlit secrets Returns: Tuple of (access_key, secret_key, region, session_token) """ try: aws_access_key = st.secrets.get("AWS_ACCESS_KEY_ID") aws_secret_key = st.secrets.get("AWS_SECRET_ACCESS_KEY") aws_session_token = st.secrets.get("AWS_SESSION_TOKEN") # For temporary credentials aws_region = st.secrets.get("AWS_REGION", "us-east-1") if not aws_access_key or not aws_secret_key: st.error("❌ AWS credentials not found in secrets.toml") st.info(""" **Setup Required:** 1. Create `secrets.toml` file with AWS credentials 2. Upload to Snowflake stage with your app code 3. Restart the app Example secrets.toml: ```toml AWS_ACCESS_KEY_ID = "AKIA..." AWS_SECRET_ACCESS_KEY = "wJalr..." AWS_SESSION_TOKEN = "FwoGZ..." # Required for temporary credentials AWS_REGION = "us-east-1" ``` """) return None, None, "us-east-1", None return aws_access_key, aws_secret_key, aws_region, aws_session_token except Exception as e: st.error(f"❌ Error loading credentials: {str(e)}") return None, None, "us-east-1", None def create_dynamodb_client(access_key: str, secret_key: str, region: str, session_token: Optional[str] = None): """ Create and return a DynamoDB client Args: access_key: AWS access key secret_key: AWS secret key region: AWS region session_token: AWS session token for temporary credentials Returns: boto3 DynamoDB client or None on error """ try: client_kwargs = { 'service_name': 'dynamodb', 'aws_access_key_id': access_key, 'aws_secret_access_key': secret_key, 'region_name': region, # Connection pooling for better performance 'config': boto3.session.Config( retries={'max_attempts': 3, 'mode': 'adaptive'} ) } # Add session token if using temporary credentials if session_token: client_kwargs['aws_session_token'] = session_token client = boto3.client(**client_kwargs) # Test connection by describing the table try: client.describe_table(TableName='songwhip-release-tasks-production') return client except client.exceptions.ResourceNotFoundException: st.error("❌ Table 'songwhip-release-tasks-production' not found") return None except Exception as e: import traceback error_details = traceback.format_exc() st.error(f"⚠️ Warning: Could not verify table access: {str(e)}") with st.expander("🔍 Debug: Full Error Details"): st.code(error_details) # Return client anyway - might still work for queries return client except Exception as e: st.error(f"❌ Error creating DynamoDB client: {str(e)}") st.info(""" **Possible Issues:** - Check AWS credentials are correct - Verify External Access Integration includes AWS endpoints - Ensure IAM permissions for DynamoDB access """) return None def deserialize_dynamodb_item(item: Dict[str, Any]) -> Dict[str, Any]: """Deserialize DynamoDB item from low-level format to Python dict""" deserializer = TypeDeserializer() return {k: deserializer.deserialize(v) for k, v in item.items()} def fetch_refresh_tokens( client, album_id: str, limit: int = 50, presave_version: str = 'V1' ) -> Tuple[List[str], List[Dict[str, Any]], Optional[str]]: """ Fetch refresh tokens from DynamoDB for a given presave campaign Args: client: boto3 DynamoDB client album_id: The presave ID to query (e.g., "6644715" for V1, "511" for V2) limit: Maximum number of tokens to fetch (default: 50) presave_version: 'V1' for albums (group:album{id}), 'V2' for prereleases (group:prerelease{id}) Returns: Tuple of (refresh_tokens list, raw_items list, error_message) """ table_name = 'songwhip-release-tasks-production' # V2 presaves use 'prerelease' prefix, V1 uses 'album' normalized_version = presave_version.strip('*').upper() if normalized_version == 'V2': partition_key = f'group:prerelease{album_id}' else: partition_key = f'group:album{album_id}' sort_key_prefix = 'task:spotify-presave' try: # Query DynamoDB response = client.query( TableName=table_name, KeyConditionExpression='partitionKey = :pk AND begins_with(sortKey, :sk)', ExpressionAttributeValues={ ':pk': {'S': partition_key}, ':sk': {'S': sort_key_prefix} }, Limit=limit, # Only fetch the fields we need to reduce data transfer ProjectionExpression='partitionKey, sortKey, refreshToken' ) items = response.get('Items', []) # Log query metadata for debugging (server-side only) count = response.get('Count', 0) scanned_count = response.get('ScannedCount', 0) last_evaluated_key = response.get('LastEvaluatedKey') logger.debug( f"DynamoDB Query: partition_key={partition_key}, " f"sort_key_prefix={sort_key_prefix}, limit={limit}" ) logger.debug( f"Items returned: {count}, Items scanned: {scanned_count}, " f"Has more results: {bool(last_evaluated_key)}" ) if not items: return [], [], None # Deserialize items and extract refresh tokens deserialized_items = [deserialize_dynamodb_item(item) for item in items] refresh_tokens = [ item.get('refreshToken') for item in deserialized_items if item.get('refreshToken') ] return refresh_tokens, deserialized_items, None except Exception as e: error_msg = str(e) return [], [], error_msg