from typing import List from delphi_api.const import ENABLE_FAKE_DATA from delphi_api.errors import Codes, InvalidInputError from delphi_api.errors.descriptions import ITEM_NOT_FOUND from delphi_api.v3.constants import ( DSP_AMAZON, DSP_APPLE, DSP_SPOTIFY, DSP_YOUTUBE, ENABLED_DSPS, ) from delphi_api.v3.data_models.bigtable.dsp_streams import ( AmazonTrackStream, AppleTrackStream, SpotifyTrackStream, YouTubeTrackStream, ) from delphi_api.v3.data_models.bigtable.row_key_builder import RowKeyBuilder, RowKeyGroupType from delphi_api.v3.data_models.postgres_db import Artist, db from delphi_api.v3.data_models.schemas.generics import StreamDemographicSchema, StreamSchema from delphi_api.v3.enums import AggByParam, IncludeParamStreams from delphi_api.v3.view_models.bigtable_loader import BigTableLoader from delphi_api.v3.view_models.fake_utils import FakeUtils from delphi_api.v3.view_models.params import Params from delphi_api.v3.view_models.query_builder import QueryBuilder class StreamsViewModel: DATA_MODELS = { DSP_APPLE: AppleTrackStream, DSP_AMAZON: AmazonTrackStream, DSP_SPOTIFY: SpotifyTrackStream, DSP_YOUTUBE: YouTubeTrackStream, } ENABLED_DATA_MODELS = {k: v for k, v in DATA_MODELS.items() if k in ENABLED_DSPS} @staticmethod def _get_fake_streams(params: Params): """Temporary method to return data in lieu of BigTable""" return FakeUtils.get_fake_temporal_data(params) @staticmethod def load_additional_params_data(params: Params) -> Params: """ Loads any additional identity data from relational postgres data for use in Bigtable queries Notes: We must join with postgres data if the following conditions are met: - ``agg_by=isrc`` is provided, with ``artist_id`` and without ``isrc`` or ``track_id`` Returns: Params: loaded params object with potentially additional IDs from postgres """ #: agg_by=artist for artist_id is only valid with certain params #: see :meth:`StreamsViewModel.validate_params` below if params.agg_by == AggByParam.ARTIST.value: return params join_pg_data = all([ params.agg_by == AggByParam.ISRC.value, params.artist_id, not params.isrc, not params.track_id, ]) if not join_pg_data: return params # if we do not have a track_id or isrc but have artist_id, get tracks by artist artist = db.session.query(Artist).get_or_404(params.artist_id, description=ITEM_NOT_FOUND) tracks = artist.tracks_ext isrcs = set(params.isrc or []) track_ids = set(params.track_id or []) for t in tracks: isrcs.add(t.isrc) track_ids.add(t.track_id) params.isrc = list(isrcs) params.track_id = list(track_ids) # if client requests agg_by=isrc, remove track_id if params.agg_by == AggByParam.ISRC.value: params.track_id = None return params @staticmethod def validate_params(params: Params): """Check the parameters provided by the client are compatible before continuing Raises: InvalidInputError: client provided invalid or incompatible parameter combination """ err = False if not (params.artist_id or params.playlist_id or params.chart_id or params.track_id or params.isrc or params.product_id): err = ('Missing at least one identifiying parameter from set: ' '[artist_id, chart_id, playlist_id, track_id, isrc, product_id]') # if user provided query param agg_by for artist elif params.agg_by == AggByParam.ARTIST.value: # without an artist_id if not params.artist_id: err = (f'Missing required param [artist_id] with provided ' f'agg_by={AggByParam.ARTIST.value}') # or with other params if (params.playlist_id or params.chart_id or params.track_id or params.isrc or params.product_id or params.subset): err = ('Parameters [playlist_id, chart_id, track_id, isrc, product_id, subset] ' f'are not compatible with agg_by={AggByParam.ARTIST.value}') elif params.agg_by == AggByParam.ISRC.value: if params.isrc and params.track_id: err = ('Using parameters [track_id, isrc] together with ' f'agg_by={AggByParam.ARTIST.value} is not currently supported.') elif params.chart_id and params.playlist_id: err = 'Only one parameter of parameters [chart_id, playlist_id] is allowed.' if err: raise InvalidInputError({ 'code': Codes.invalid_input.value, 'description': err, }) @classmethod def get_many(cls, params: dict) -> List[dict]: """Primary method for views to get data via models""" params = Params(**params) cls.validate_params(params) params = cls.load_additional_params_data(params) if ENABLE_FAKE_DATA: result = cls._get_fake_streams(params) else: item_keys = RowKeyBuilder.get_row_keys(params, RowKeyGroupType.STREAMS) result = BigTableLoader.get_multiple(params, cls.ENABLED_DATA_MODELS, item_keys) # ensure result objects have streams data result = cls.filter_results_by_key(result, 'streams') result = QueryBuilder.sort_results(result, params) result = QueryBuilder.limit_offset_results(result, params) if params.include == IncludeParamStreams.DEMOGRAPHICS.value: return StreamDemographicSchema(many=True).dump(result) return StreamSchema(many=True).dump(result) @classmethod def filter_results_by_key(cls, results: List[dict], key: str): return [obj for obj in results if obj and key in obj.keys()]