""" Utilities for manipulating data in Redis. """ import simplejson as json from flask import g from typing import List import src.legacy.redis_db.keys as keys from src.cache import redis_client from src.cache.constants import COMPARED_TRACKS_LIMIT, CacheMode from src.cache.exceptions import ComparedTracksLimitError, DuplicateComparedTracks from src.legacy.redis_db.keys import get_key def get_compared_tracks(user_id: str) -> List[dict]: """ Retrieve state of comparison page for a user. Comparison page state is stored as a list of dictionaries serialized to JSON. Each dictionary contains at least two keys: apple_id and spotify_id. There can be other optional payload (e.g. isrc). apple_id and spotify_id are unique across all the dictionaries, because user cannot compare a track with itself. One of these keys can be null, but not both. Stored data has expiration time, which is reset with every call to this function. Args: user_id: ID of a user. Returns: List[dict]: Comparison page state as list of tracks data. """ key = get_key(keys.COMPARED_TRACKS, user_id) pipe = redis_client.pipeline() pipe.expire(key, keys.COMPARED_TRACKS_TTL) pipe.get(key) pipe_data = pipe.execute() track_list_json = None if pipe_data and len(pipe_data) > 1: track_list_json = pipe_data[1] if track_list_json: return json.loads(track_list_json.decode()) else: return [] def add_compared_track(user_id: str, track: dict) -> bool: """ Update saved state of user's comparison page when new track is added. Comparison page state is stored as a JSON string. It has expiration time, which is reset with every successful call of this function. Args: user_id: ID of a user. track: Dictionary with added track data. Must contain at least two keys: apple_id and spotify_id. At least one of them must not be None. Additional payload (e.g. isrc) is allowed. Returns: True if track was successfully added to comparison. Raises: ValueError: If either spotify_id or apple_id is missing; If both spotify_id and apple_id are None. ComparedTracksLimitError: If all slots in comparison page are occupied. DuplicateComparedTracks: If track with provided Spotify ID or Apple Music ID is already in comparison. """ if not ("spotify_id" in track and "apple_id" in track) or ( track["spotify_id"] is None and track["apple_id"] is None ): raise ValueError key = get_key(keys.COMPARED_TRACKS, user_id) track_list_json = redis_client.get(key) track_list = json.loads(track_list_json) if track_list_json else [] for i in track_list: if ( track["apple_id"] is not None and track["apple_id"] == i["apple_id"] or track["spotify_id"] is not None and track["spotify_id"] == i["spotify_id"] ): raise DuplicateComparedTracks("Track is already in comparison.") if len(track_list) >= COMPARED_TRACKS_LIMIT: raise ComparedTracksLimitError( "No more than {} tracks can be added to comparison.".format(COMPARED_TRACKS_LIMIT) ) track_list.append(track) redis_client.setex(key, keys.COMPARED_TRACKS_TTL, json.dumps(track_list)) return True def del_compared_tracks(user_id: str, spotify_ids: List[str] or None = None, apple_ids: List[int] or None = None): """Update saved state of user's comparison page when some tracks are removed from comparison. Args: user_id (str): ID of a user. spotify_ids (List[str] or None): Spotify ID list of removing tracks. apple_ids (List[int] or None): Apple Music ID list of removing tracks. """ if not spotify_ids and not apple_ids: return if not spotify_ids: spotify_ids = [] if not apple_ids: apple_ids = [] key = get_key(keys.COMPARED_TRACKS, user_id) track_list_json = redis_client.get(key) if not track_list_json: return track_list = [ i for i in json.loads(track_list_json) if i["spotify_id"] not in spotify_ids and i["apple_id"] not in apple_ids ] if not track_list: redis_client.delete(key) return redis_client.setex(key, keys.COMPARED_TRACKS_TTL, json.dumps(track_list)) def del_compared_track(user_id: str, spotify_id: str = None, apple_id: int = None): """ Update saved state of user's comparison page when some track is removed from comparison. Track can be deleted by either Spotify ID or Apple Music ID. If track with provided ID is not present in comparison page nothing happens. Each successful call of this function resets expiration time of stored data. Args: user_id: ID of a user. spotify_id: Spotify ID of the removed track. apple_id: Apple Music ID of the removed track. """ del_compared_tracks(user_id, [spotify_id], [apple_id]) def delete_cached_response(key: str): """Delete cached response value. Args: key: Cache key. """ redis_client.delete(get_key(key)) class CacheModeManager: """Simple context manager to change cache mode before executing any func/method that uses cache to chose whether to use it or to get data from db. """ def __init__(self, selected_cache_mode): self._cache_mode = g._cache_mode self.selected_cache_mode = selected_cache_mode def __enter__(self): g._cache_mode = CacheMode(self.selected_cache_mode) if self.selected_cache_mode else self._cache_mode def __exit__(self, exc_type, exc_val, exc_tb): g._cache_mode = self._cache_mode