Files
OSIT-AE-API-FastAPI/app/lib_redis_helpers.py

248 lines
10 KiB
Python

"""
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
# Also set the short prefix version (e.g., obj_data['account'] = 1) for compatibility
obj_data[f'{prefix if not prefix.endswith("_id_random_only") else prefix[:-15]+"_id_only"}'] = resolved_id
# 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