""" Redis-based ID resolution and caching helpers for Aether. """ import datetime import random import redis import logging from app.config import settings log = logging.getLogger(__name__) def redis_lookup_id_random( record_id_random: int|str, table_name: str, check_int_id: bool = False, log_lvl: int = logging.WARNING, # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL minutes: int = 30, # Expire the Redis key after 8 minutes reset_rate: int = 10, # 1 in 10 chance of resetting the Redis key ) -> str|int|bool|None: """ Looks up a record ID in Redis, falling back to SQL if not found. Resolves 'id_random' (URL-safe string) to internal integer 'id'. """ from app.db_sql import sql_select, get_id_random log.setLevel(log_lvl) if isinstance(record_id_random, str) and len(record_id_random) >= 11 and len(record_id_random) <= 22: pass elif isinstance(record_id_random, int): record_id = record_id_random if check_int_id: log.info(f'Checking the int ID if exists. Table Name: {table_name} ID: {record_id}') if get_id_random_result := get_id_random( record_id = record_id, table_name = table_name, ): log.info(f'The int ID exists. Returning the int ID. ID Random: {get_id_random_result}') return record_id else: log.info(f'The int ID does not exists. Returning False. Table Name: {table_name} ID: {record_id}') return False else: log.debug(f'Not checking if the int ID exists. Returning the int ID. ID: {record_id}') return record_id elif record_id_random is None: log.info(f'No record ID was passed. Returning None') return None else: log.error(f'Unexpected data type or string format: {type(record_id_random)} Expected type is a string 11 or 22 characters long.') return False if record_id_random and table_name: if len(record_id_random) < 11: log.error(f'The length of id_random is too short: {record_id_random} ({len(record_id_random)} chars)') return False elif len(record_id_random) > 22: log.error(f'The length of id_random is too long: {record_id_random} ({len(record_id_random)} chars)') return False elif record_id_random: log.error(f'Missing table_name to select from for id_random "{record_id_random}"') return False elif table_name: log.error(f'Missing id_random to select from table "{table_name}"') return False else: log.error('Missing table_name and record_id_random') return False r = redis.Redis(host=settings.REDIS['server'], port=settings.REDIS['port'], db=7, password=None, decode_responses=True) key_name = f'{table_name}:{record_id_random}' record_id = r.get(key_name) if record_id and random.randint(1, reset_rate) == 1: log.warning(f'Redis: Randomly (1/{reset_rate}) setting record_id to None. Key="{key_name}" value="{record_id}" TTL={r.ttl(key_name)} seconds') record_id = None if record_id: r.setex(key_name, datetime.timedelta(minutes=minutes), value=record_id) log.info(f'Redis: Entry found for: Key="{key_name}" value="{record_id}" TTL={r.ttl(key_name)} seconds') return int(record_id) elif table_name: data = { 'id_random': record_id_random } sql = f"SELECT id FROM `{table_name}` AS `table` WHERE `table`.id_random = :id_random;" if select_results := sql_select(sql=sql, data=data): log.debug(f'SQL: SELECT result: {select_results}') if isinstance(select_results, dict): log.info(f"""SQL: Found ID Random for: {str(record_id_random)} = {str(select_results.get('id'))}""") if record_id := select_results.get('id'): r.setex(key_name, datetime.timedelta(minutes=minutes), value=record_id) return int(record_id) else: log.error('The SQL result was not what was expected. The ID field was not found.') return False else: log.error(f'SQL: More than one record found in "{table_name}". Duplicate id_random!') return redis_lookup_id_random(record_id_random=record_id_random, table_name=table_name) else: log.warning(f'SQL: ID Random "{record_id_random}" not found in "{table_name}". Returning None.') return None log.error('Unexpected state in redis_lookup_id_random.') return False def get_id_random( record_id: int, table_name: str, log_lvl: int = logging.WARNING, # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL ) -> str|bool|None: """ Looks up the 'id_random' for a given internal integer ID. """ from app.db_sql import sql_select log.setLevel(log_lvl) data = { 'id': record_id } sql = f"SELECT id_random FROM `{table_name}` AS `table` WHERE `table`.id = :id;" if select_results := sql_select(sql=sql, data=data): if isinstance(select_results, dict): if record_id_random := select_results.get('id_random'): return str(record_id_random) else: log.error('The SQL result was not what was expected.') return False elif isinstance(select_results, list): log.exception('More than one record may have been found. Duplicate ID!') return False else: log.exception(f'Got an unexpected result while trying to look up the ID.') return False elif select_results is None: return None else: return False def reset_redis(): """Flushes the Redis database used for ID caching.""" r = redis.Redis(host=settings.REDIS['server'], port=settings.REDIS['port'], db=7, password=None, decode_responses=True) r.flushdb() return True def lookup_id_random_pop( obj_data: dict, log_lvl: int = logging.WARNING, # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL ): """ Look up and resolve id_random values to their id Remove the unneeded *_id_random key from the dict """ log.setLevel(log_lvl) # Common prefixes for ID resolution id_prefixes = [ 'account', 'activity_log', 'address', 'address_location', 'archive', 'contact', 'contact_1', 'contact_2', 'cont_edu_cert', 'cont_edu_cert_person', 'event', 'event_id_random_only', 'event_abstract', 'event_badge', 'event_badge_template', 'event_exhibit', 'event_file', 'event_location', 'event_person', 'event_person_profile', 'event_presentation', 'event_presenter', 'event_registration', 'event_session', 'event_track', 'grant', 'hosted_file', 'journal', 'journal_entry', 'membership_group', 'membership_person_group', 'membership_person', 'membership_type', 'membership_person_type', 'order', 'order_line', 'order_cart', 'order_cart_line', 'organization', 'page', 'person', 'poc_event_person', 'poc_person', 'post', 'product', 'sponsorship', 'sponsorship_cfg', 'site', 'user' ] for prefix in id_prefixes: key_random = f'{prefix}_id_random' key_id = f'{prefix}_id' # Table name mapping table = prefix if prefix == 'address_location': table = 'address' elif prefix in ['contact_1', 'contact_2']: table = 'contact' elif prefix == 'event_id_random_only': table = 'event' elif prefix == 'poc_event_person': table = 'event_person' elif prefix == 'poc_person': table = 'person' resolved_id = None # Scenario A: Legacy suffix (e.g., account_id_random: "abc") if key_random in obj_data: resolved_id = redis_lookup_id_random(record_id_random=obj_data[key_random], table_name=table) obj_data.pop(key_random) # Scenario B: Vision naming (e.g., account_id: "abc") # Only resolve if it's a string of the correct length (random ID format) elif key_id in obj_data and isinstance(obj_data[key_id], str) and 11 <= len(obj_data[key_id]) <= 22: resolved_id = redis_lookup_id_random(record_id_random=obj_data[key_id], table_name=table) if resolved_id is not None: # Set the target ID field target_id_key = key_id if prefix == 'event_id_random_only': target_id_key = 'event_id_only' obj_data[target_id_key] = resolved_id # Removed the short prefix version (e.g., obj_data['account'] = 1) # as it causes 'Unknown column' errors in direct table inserts. # Polymorphic links polymorphic = [ ('for_type', 'for_id_random', 'for_id'), ('link_to_type', 'link_to_id_random', 'link_to_id'), ('object_type', 'object_id_random', 'object_id'), ('to_object_type', 'to_object_id_random', 'to_object_id'), ('from_object_type', 'from_object_id_random', 'from_object_id') ] for type_key, rand_key, id_key in polymorphic: # Handle random key if present if type_key in obj_data and rand_key in obj_data: obj_data[id_key] = redis_lookup_id_random( record_id_random=obj_data.get(rand_key), table_name=obj_data.get(type_key) ) obj_data.pop(rand_key) # Handle Vision naming (id_key contains the string) elif type_key in obj_data and id_key in obj_data and isinstance(obj_data[id_key], str) and 11 <= len(obj_data[id_key]) <= 22: obj_data[id_key] = redis_lookup_id_random( record_id_random=obj_data.get(id_key), table_name=obj_data.get(type_key) ) return obj_data def get_account_id_w_for_type_id( for_type: str, # This is the table name for_id: int|str, ) -> bool|int|None: """Helper to find an account_id associated with an object.""" from app.db_sql import sql_select log.setLevel(logging.WARNING) if fid := redis_lookup_id_random(record_id_random=for_id, table_name=for_type): data = {'for_id': fid} sql = f"SELECT account_id FROM `{for_type}` WHERE id = :for_id LIMIT 1;" if result := sql_select(data=data, sql=sql): return result.get('account_id') return False return None