from datetime import date, timedelta from typing import Any, Dict, List, Tuple, Union from charts import config from charts.connectors import snowflake from charts.constants.common import CHART_EXPECTED_UPDATE_FREQUENCY, COUNTRY_GLOBAL, ChartDigestSoundRecordingItems, \ CHART_DIGEST_SOUND_RECORDING_NEEDS_SORTING from charts.logic import sql_loader, chart from charts.logic.chart import CHART_RANKINGS_COLUMN_NAMES from charts.utils.db.mapper import map_db_result_with_column_names from charts.utils.chart_definition_key import split_definition_key from charts.utils.filter_items import filter_items from charts.utils.multikeysort import multikeysort def sound_recording_entry_condition(item: Dict[str, Any]) -> bool: """ Check if a sound recording is an entry Args: item: Dict item representing a single sound recording in a chart Returns: boolean """ return item["newlyEntered"] def sound_recording_major_move_condition(item: Dict[str, Any]) -> bool: """ Check if a sound recording is a major move Args: item: Dict item representing a single sound recording in a chart Returns: boolean """ # Trend can be null for ENTRIES so in that case will take a 0 trend = item["trend"] or 0 return abs(trend) >= config.SOUND_RECORDING_MAJOR_MOVE CHART_DIGEST_SOUND_RECORDINGS_FILTER_CONDITIONS_MAPPING = { ChartDigestSoundRecordingItems.ENTRY.value: sound_recording_entry_condition, ChartDigestSoundRecordingItems.MAJOR_MOVE.value: sound_recording_major_move_condition } def filter_and_sort( all_items: List[Dict[str, Any]], filter_condition: str = ChartDigestSoundRecordingItems.ALL.value ) -> List[Dict[str, Any]]: """Filter and sort items if needed Args: all_items: List of dict items describing sound recording state in a chart filter_condition: a string value -> one of ChartDigestSoundRecordingItems values Returns: List[Dict[str, Any]] """ # If ALL items are requested then there`s no need to filter and to sort # The result is sorted on query level by position in ASC order if filter_condition != ChartDigestSoundRecordingItems.ALL.value: # filter all_items based on passed filter_condition by getting a condition calculation from a mapping all_items = filter_items(all_items, CHART_DIGEST_SOUND_RECORDINGS_FILTER_CONDITIONS_MAPPING[filter_condition]) if filter_condition == CHART_DIGEST_SOUND_RECORDING_NEEDS_SORTING: # Here we path "-trend" to make a sorting in DESC order all_items = multikeysort(all_items, ["-trend"]) return all_items def _get_chart_data( chart_id: str = None, definition_key: str = None, country_code: str = COUNTRY_GLOBAL ) -> Dict[str, Union[str, Union[str, date]]]: """ Get a single chart data with latest available chart date by chart_id or definition_key + country_code Args: chart_id: unique chart_id definition_key: chart definition key {platform}_{type}_{frequency} like spotify_viral_daily e.t.c country_code: global or two digit country code Returns: """ params = { "chart_id": chart_id, "definition_key": definition_key, "country_code": country_code } column_names = ( "chart_id", "definition_key", "latest_chart_date", ) sql = sql_loader.load_query("get_chart_data_with_latest_available_date") raw_db_result = snowflake.fetchall(sql, params) return {c: v for c, v in zip(column_names, raw_db_result[0] if raw_db_result else [None, None, None])} def _get_chart_digest_entry_track_isrc(chart_id: str, chart_date: date, chart_frequency: str) -> List[str]: """Get a list of chart tracklist entries isrc Args: chart_id: unique chart_id chart_date: date of a chart to get entries for chart_frequency: chart frequency to calculate days between chart updates Returns: """ previous_update_chart_date = chart_date - timedelta(days=CHART_EXPECTED_UPDATE_FREQUENCY[chart_frequency]) params = { "chart_id": chart_id, "chart_date": chart_date, "previous_chart_date": previous_update_chart_date } sql = sql_loader.load_query("get_chart_digest_entry_tracks") raw_db_result = snowflake.fetchall(sql, params) return [item[0] for item in raw_db_result] def _chart_is_available_for_previous_updated(chart_id: str, chart_frequency: str) -> bool: """Check if chart data is available for two latest consecutive updates Args: chart_id: unique chart id chart_frequency: "daily", "weekly" Returns: Bool that represents whether we have data for a previous chart update date or we have a missing data gap """ # Get available chart dates in DESC order chart_dates = chart.get_chart_available_dates(chart_id) # If we have only one date record for a chart it means that this chart is new and has no previous available data if len(chart_dates) <= 1: return False latest_chart_date, previous_chart_date = chart_dates[0], chart_dates[1] days_between_updates = (date.fromisoformat(latest_chart_date) - date.fromisoformat(previous_chart_date)).days return days_between_updates == CHART_EXPECTED_UPDATE_FREQUENCY[chart_frequency] def get_chart_digest_tracks_with_entries( definition_key: str = None, country_code: str = COUNTRY_GLOBAL, chart_id: str = None, chart_date: date = None, limit: int = None, offset: int = None ) -> Tuple[List[Dict[str, Any]], List[str]]: """Get chart tracklist with entries and reentries by "definition_key" + "country_code" for tha latest available chart date Args: definition_key: custom chart key built from {platform}_{type}_{frequency} definition_key example: spotify_viral_daily, apple_default_daily e.t.c. country_code: a two-letter country code market example: "global", "us", "es", "ua" ... e.t.c chart_id: string chart identifier chart_date: requested chart date tracklist. Latest available chart date will be assigned if not passed offset: limit: Returns: A Tuple of: List of Dict objects where every object represents a single track in a requested chart. The order of a tracklist is by position of a track in a chart. & List of entry and re-entry track isrc`s """ # Get all needed chart data based on provided params chart_data_with_latest_chart_date = _get_chart_data(chart_id, definition_key, country_code) # Return an empty array for tracklist & entries if no chart is found for provided params if not chart_data_with_latest_chart_date or chart_data_with_latest_chart_date == {'chart_id': None, 'definition_key': None, 'latest_chart_date': None}: return [], [] chart_id, definition_key, latest_chart_date = ( chart_data_with_latest_chart_date["chart_id"], chart_data_with_latest_chart_date["definition_key"], chart_data_with_latest_chart_date["latest_chart_date"] ) _, _, frequency = split_definition_key(definition_key) # Check if chart is a latest available chart for requested chart_date is_latest_chart = latest_chart_date == chart_date if chart_date else True # _chart_is_available_for_previous_updated check is added to check for missing data gaps because if there`s no data # in a DB for previous update we must not show entries cause every track will be treated like entry no_missing_data_gap = ( _chart_is_available_for_previous_updated(chart_id, frequency)) if is_latest_chart else False # Get a list of entry and reentry tracks in a chart if we requested the latest available chart date tracklist # We will show entry & reentry only for latest tracklist entries_isrc_list = ( _get_chart_digest_entry_track_isrc(chart_id, latest_chart_date, frequency) ) if is_latest_chart and no_missing_data_gap else [] params = { "chart_id": chart_id, "chart_date": chart_date or latest_chart_date, "limit": limit, "offset": offset, } sql = sql_loader.load_query("get_chart_rankings") raw_db_result = snowflake.fetchall(sql, params) return map_db_result_with_column_names(raw_db_result, CHART_RANKINGS_COLUMN_NAMES), entries_isrc_list