"""Token management for Marketing Cloud API. Provides a singleton TokenManager class that handles retrieval, caching, and refreshing of access tokens for different business units using Redis. Ensures efficient and secure access to the Marketing Cloud API. """ import redis import requests import config # noqa from lambdacommon.common_config import logger class TokenManager: """Singleton class to manage Marketing Cloud API tokens with Redis caching.""" _instance = None def __new__(cls): """Ensure only one instance of TokenManager exists.""" if not cls._instance: cls._instance = super(TokenManager, cls).__new__(cls) cls._instance._initialized = False return cls._instance def __init__(self): """Initialize the Redis connection for token storage if not already initialized.""" if not self._initialized: logger.debug('Initializing Redis connection') self.redis = redis.StrictRedis( host=config.REDIS_HOST, port=config.REDIS_PORT, db=0, ssl=True, decode_responses=True) self._initialized = True def get_token(self, business_unit_id): """Retrieve a valid access token for the given business unit. If a cached token exists in Redis, it is returned. Otherwise, a new token is requested from the Marketing Cloud API and cached with its TTL. Args: business_unit_id (str): The Marketing Cloud business unit/account ID. Returns: str: The access token for the business unit. """ logger.debug(f'Retrieving token for business unit {business_unit_id}') token = self.redis.get(business_unit_id) if token: logger.debug(f'Found cached token for business unit {business_unit_id}') return token # Token expired or not found logger.debug(f'No cached token found for business unit {business_unit_id}, requesting new token') token, ttl = self._request_new_token(business_unit_id) self.redis.setex(business_unit_id, ttl, token) # Set with TTL return token def _request_new_token(self, business_unit_id): """Request a new access token from the Marketing Cloud API for the given business unit. Args: business_unit_id (str): The Marketing Cloud business unit/account ID. Returns: tuple: (access_token (str), expires_in (int)) """ payload = { 'grant_type': 'client_credentials', 'client_id': config.MARKETING_CLOUD_CLIENT_ID, 'client_secret': config.MARKETING_CLOUD_CLIENT_SECRET, 'account_id': business_unit_id } headers = {'Content-Type': 'application/json'} response = requests.post(config.MARKETING_CLOUD_TOKEN_API_URL, json=payload, headers=headers) response.raise_for_status() token_data = response.json() return token_data['access_token'], token_data[ 'expires_in'] # TODO Decrease expires_in by 60 seconds to account for network delays and processing time