from datetime import date, timedelta from http import HTTPStatus from typing import Dict, List, Optional, Tuple, Union from unittest.mock import AsyncMock import pytest from aiohttp.test_utils import TestClient from apollo_utils.core.constants.dsp import DSP, DSP_SPOTIFY_APPLE_AMAZON from apollo_utils.core.constants.market import Market from pytest_mock import MockerFixture from server.client.clients.dsp_api import DspApiClient from server.client.clients.vendor_api import VendorApiClient from server.client.clients.filtr_api import FiltrApiClient def get_trackplaylistadds(count: int = 5, with_streams: bool = False, dsp_count: int = 1) -> dict: dsp_part = 847 * (1 + dsp_count) / 2 * dsp_count result = [] for index in range(1, count + 1): track_id = f"trid{index:02d}" album_id = f"alid{index:02d}" result.append( { "spotifyTrack": { "albumImageUrl": f"https://image/album{index}.jpeg", "trackUri": f"spotify:track:{track_id}", "albumUri": f"spotify:album:{album_id}", "previewUrl": f"https://preview/{index}.jpeg", "albumId": album_id, "albumName": f"AL {index}", "trackId": track_id, "trackName": f"TR {index}", "artists": [ { "name": f"AR {index}|{j}", "uri": f"spotify:artist:arid{j:02d}" } for j in range(1, min(3, index)) ], "isrc": f"isrc{index:02d}", "popularity": 50 + index, "danceability": 0.6 + index / 100, "energy": 0.7 + index / 100, "audioKey": index - 1, "loudness": -5.4 - index / 10, "audioMode": 1, "speechiness": 0.08 + index / 100, "acousticness": 0.03 + index / 100, "instrumentalness": 0.01 + index / 1000, "liveness": 0.1 + index / 100, "valence": 0.47 + index / 100, "tempo": 162.9 + index, "albumPosition": index + 1, "album": { "albumId": album_id, "name": f"AL {index}", "albumType": 0, "popularity": 60 + index, "copyright": f'({"C" * index})', "label": f"L{index}", "upc": f"UPC{index:02d}", "releaseDate": f"2022-08-2{index % 10}", "releaseDatePrecision": "0", f"smallImageFilename": f"album-{album_id}-small.jpeg" } }, "playlists": 2300 + index * 10, "totalPlaylistSubscribers": 20010900 + index * 11, **( { "streams_7_days": (14210 * index + 840) * dsp_count + dsp_part, "streams_8_14_days": (14210 * index + 252) * dsp_count + dsp_part, } if with_streams else {} ), } ) return {"items": result} def get_tracks_streams(params: dict) -> List[dict]: dsp_list, isrc_list, country_code, start_date, end_date = ( params["dsp"], params["isrc"], params.get("country_code", Market.WORLDWIDE), params["start_date"], params["end_date"], ) days_count = (end_date - start_date).days + 1 result = [] for dsp_index, dsp in enumerate(sorted(dsp_list), 1): for isrc_index, isrc in enumerate(sorted(isrc_list), 1): for day_index in range(days_count): result.append( { "country_code": country_code, "date": (start_date + timedelta(days=day_index)).isoformat(), "isrc": isrc, "streams": 2030 * isrc_index + 121 * dsp_index + day_index * 12, "dsp": dsp, } ) return result @pytest.mark.parametrize( "params,expected_status", ( ({}, HTTPStatus.BAD_REQUEST), ({"spotify_artist_id": "arid01"}, HTTPStatus.OK), ({"spotify_artist_id": "arid01", "country_code": "us", "limit": 10, "offset": 5}, HTTPStatus.OK), ({"spotify_artist_id": "arid01", "dsp": "spotify,apple,amazon"}, HTTPStatus.OK), ), ) async def test_get_artists_tracks(params, expected_status, mocker, client, auth): dsp_list = params.get("dsp", "spotify").split(",") async def async_dsp_mock(url, **kwargs): if url == "api/consumer_analytics/streams-latest-date": return {"date": "2022-11-18"} elif url == "api/delphi/streams": return get_tracks_streams(kwargs["params"]) mocked_dsp_send = mocker.patch.object(DspApiClient, "send_request", side_effect=async_dsp_mock) async_filtr_mock = AsyncMock(return_value=get_trackplaylistadds()) mocked_filtr_send = mocker.patch.object(FiltrApiClient, "send_request", side_effect=async_filtr_mock) response = await client.get("/api/artists/tracks/", params=params, headers=auth) assert response.status == expected_status if expected_status != HTTPStatus.OK: assert mocked_dsp_send.call_count == 0 assert mocked_filtr_send.call_count == 0 return mocked_filtr_send.assert_called_once_with( "trackplaylistadds", params={ "uri": "spotify:artist:arid01", "limit": params.get("limit"), "offset": params.get("offset"), "region": params.get("country_code"), }, ) assert mocked_dsp_send.call_args_list[0] == (("api/consumer_analytics/streams-latest-date",), {}) mocked_dsp_send.call_args_list[1].kwargs["params"]["isrc"] = list( sorted(mocked_dsp_send.call_args_list[1].kwargs["params"]["isrc"]) ) assert mocked_dsp_send.call_args_list[1] == ( ("api/delphi/streams",), { "params": { "isrc": [f"isrc{i:02d}" for i in range(1, 6)], "start_date": date(2022, 11, 5), "end_date": date(2022, 11, 18), "dsp": dsp_list, "country_code": params.get("country_code"), "group_by": "date", }, }, ) response_json = await response.json() assert response_json == get_trackplaylistadds(with_streams=True, dsp_count=len(dsp_list)) def get_artists_streams(params: dict, as_result: bool = False) -> List[dict]: artist_id, dsp_list, country_code_list, start_date, end_date = ( f'GRAS_{params["spotify_artist_id"]}' if as_result else params["artist_id"], params.get("dsp", "spotify").split(",") if as_result else params["dsp"], params.get("country_code", "worldwide").split(",") if as_result else params["country_code"], date.fromisoformat(params["start_date"]) if as_result else params["start_date"], date.fromisoformat(params["end_date"]) if as_result else params["end_date"], ) days_count = (end_date - start_date).days + 1 result = [] for index, country_code in enumerate(country_code_list, 1): if country_code == "_gl": country_code = "worldwide" result_item_list = [] for day_index in range(days_count): if DSP.SPOTIFY.value in dsp_list: result_item_list.append( { "artist_id": artist_id, "country_code": country_code, "date": (start_date + timedelta(days=day_index)).isoformat(), "streams": index * 201 + day_index, "spotify_streams_info": { "free_streams": index * 62 + day_index, "paid_streams": index * 51 + day_index, "repeat_play": 0, "shuffle_play": index * 38 + day_index, "offline_play": index * 27 + day_index, "completion_play": index * 49 + day_index, "skips": index * 72 + day_index, "source": { "album": index * 25 + day_index, "artist": index * 17 + day_index, "chart": index * 2 + day_index, "collection": index * 96 + day_index, "other": index * 26 + day_index, "others_playlist": index * 42 + day_index, "play_queue": index * 29 + day_index, "radio": index * 19 + day_index, "search": index * 23 + day_index, "daily_mix": index * 18 + day_index, "discover_weekly": index * 3 + day_index, "release_radar": 0 + day_index, }, "device_type": { "builtin_car_application": index * 7 + day_index, "cell_phone": index * 98 + day_index, "connected_audio_device": index * 52 + day_index, "gaming_console": index * 36 + day_index, "other": index * 16 + day_index, "personal_computer": index * 17 + day_index, "smart_tv_device": index * 28 + day_index, "tablet": index * 20 + day_index, "wearable": index + day_index, }, "operating_system": { "android": index * 92 + day_index, "blackberry": 0, "browser": index * 22 + day_index, "ios": index * 77 + day_index, "linux": index * 2 + day_index, "mac": index * 47 + day_index, "other": index * 33 + day_index, "windows": index * 44 + day_index, }, "engagement": { "lean_back": index * 99 + day_index, "lean_forward": index * 107 + day_index }, "saves": { "free": index * 75 + day_index, "premium": index * 82 + day_index, "total": index * 121 + day_index, }, }, "dsp": "spotify", } ) if DSP.APPLE.value in dsp_list: result_item_list.append( { "artist_id": artist_id, "country_code": country_code, "date": (start_date + timedelta(days=day_index)).isoformat(), "streams": index * 175 + day_index, "apple_streams_info": { "offline_play": index * 37 + day_index, "completion_play": index * 82 + day_index, "skips": index * 39 + day_index, "streams": index * 175 + day_index, "non_royalty_streams": 0, "listeners": index * 135 + day_index, "source": { "library": index * 104 + day_index, "search": index * 106 + day_index, "discovery": index * 92 + day_index, "music_kit": index * 47 + day_index, "external": index * 25 + day_index, "now_playing": index * 56 + day_index, "voice": index * 41 + day_index, "other": index * 37 + day_index, }, "container_type": { "single_track": index * 92 + day_index, "radio": index * 91 + day_index, "playlist": index * 85 + day_index, "album": index * 81 + day_index, }, "container_sub_type": { "not_applicable": index * 158 + day_index, "private_user_playlist": index * 161 + day_index, "editorial_playlist": index * 69 + day_index, "artist_playlist": index // 2 + day_index, "curator_playlist": index * 6 + day_index, "seeded_by_artist_song": index * 3 + day_index, "format_station": index // 3 + day_index, "editorial_station": index * 13 + day_index, "personal_mix_playlist": 0, "new_music_mix": index // 4 + day_index, "favorites_mix": index * 16 + day_index, "chill_mix": index * 2 + day_index, "get_up_mix": index * 3 + day_index, "friends_mix": index * 5 + day_index, "algorithm_station": index * 4 + day_index, "replay_mix": index * 21 + day_index, "auto_play_station": index * 42 + day_index, "charts_playlist": index * 2 + day_index, "artist_station": index * 36 + day_index, "personal_station": index * 28 + day_index, "album": index * 45 + day_index, }, "device_type": { "mobile": index * 95 + day_index, "desktop": index * 52 + day_index, "voice": index * 74 + day_index, "other": index * 32 + day_index, }, "os": {"ios": 0, "mac": 0, "android": 0, "windows": 0, "tvos": 0, "sonos": 0, "other": 0}, "user_type": {"trial": index * 36 + day_index, "paid": index * 72 + day_index}, "listener_engagement": { "lean_back": index * 152 + day_index, "lean_forward": index * 174 + day_index, "unknown": 0, }, }, "dsp": "apple", } ) if DSP.AMAZON.value in dsp_list: result_item_list.append( { "artist_id": artist_id, "country_code": country_code, "date": (start_date + timedelta(days=day_index)).isoformat(), "streams": index * 189 + day_index, "amazon_streams_info": { "non_royalty_streams": index * 73 + day_index, "device_type": { "cell_phone_app_mobile_web": index * 36 + day_index, "connected_home_audio_tether": index * 8 + day_index, "connected_home_tv": index * 6 + day_index, "desktop": index * 3 + day_index, "gaming_system": 0, "in_dash_car_integration": 1, "mobile_to_car_tether": 0, "other": index * 3 + day_index, "tablet_app": index * 2 + day_index, "voice_controlled_device": index * 24 + day_index, }, "engagement": { "lean_forward": index * 104 + day_index, "lean_back": index * 107 + day_index }, "operating_system": { "alexa": index * 72 + day_index, "android": index * 36 + day_index, "fire_os": index * 11 + day_index, "ios": index * 26 + day_index, "mac_osx": index * 2 + day_index, "third_party_device_os": index * 3 + day_index, "unknown": index * 14 + day_index, "windows": index * 4 + day_index, }, "referral_source_type": { "websites_or_hyperlinks": 0, "search_result": 0, "sme_promotional_marketing_inventory": 0, "sme_paid_promotion": 0, "recommended_content_promotions": 0, "not_available": index * 89 + day_index, }, "selection_source_type": { "album": index * 38 + day_index, "amf_station": index * 24 + day_index, "amf_station_seed": index * 18 + day_index, "artist": index * 35 + day_index, "auto_playlist": index * 12 + day_index, "custom_mix": 0, "error": 0, "genre": index * 4 + day_index, "gno_prime_playlist": 0, "gno_unl_playlist": 0, "golden_playlist": index * 3 + day_index, "personalize_playlist": index * 2 + day_index, "prime_playlist": index * 5 + day_index, "prime_station": index * 7 + day_index, "prime_station_seed": index * 6, "recently_added": 0, "recently_played": 0, "search": 0, "shared_playlist": index + day_index, "similarity": 0, "similarity_station": 0, "songs": index * 23 + day_index, "station": 0, "track": 0, "undefined": 0, "unknown": index, "unl_station_seed": index * 34 + day_index, "unlimited_playlist": index * 26 + day_index, "unlimited_station": index * 29 + day_index, "unlimited_station_se": 0, "user_playlist": index * 35 + day_index, }, "subscription_type": {"free": index * 52 + day_index, "paid": index * 62 + day_index}, }, "dsp": "amazon", } ) if as_result: result.append({"country_code": country_code, "data": result_item_list}) else: result.extend(result_item_list) return result @pytest.mark.parametrize( "params,expected_status", ( ({}, HTTPStatus.BAD_REQUEST), ({"spotify_artist_id": "arid01", "start_date": "2022-11-09", "end_date": "2022-11-18"}, HTTPStatus.OK), ( { "spotify_artist_id": "arid02", "start_date": "2022-11-11", "end_date": "2022-11-19", "country_code": "_gl,us", "dsp": "spotify,apple,amazon", }, HTTPStatus.OK, ), ), ) async def test_get_artists_streams(params, expected_status, mocker, client, auth): spotify_artist_id = params.get("spotify_artist_id") if spotify_artist_id: gras_artist_id = f"GRAS_{spotify_artist_id}" async def async_dsp_mock(url, **kwargs): if url == "api/delphi/search": return {"items": [{"artist_id": gras_artist_id}]} elif url == "api/delphi/streams": return get_artists_streams(kwargs["params"]) mocked_dsp_send = mocker.patch.object(DspApiClient, "send_request", side_effect=async_dsp_mock) response = await client.get("/api/artists/streams/", params=params, headers=auth) assert response.status == expected_status if expected_status != HTTPStatus.OK: assert mocked_dsp_send.call_count == 0 return assert mocked_dsp_send.call_args_list[0] == ( ("api/delphi/search",), {"params": {"index": "artist", "query": f'spotify_artist_id:"spotify:artist:{params["spotify_artist_id"]}"'}}, ) assert mocked_dsp_send.call_args_list[1] == ( ("api/delphi/streams",), { "params": { "artist_id": gras_artist_id, "dsp": params.get("dsp", "spotify").split(","), "country_code": list( ("worldwide" if i == "_gl" else i) for i in params.get("country_code", "worldwide").split(",") ), "start_date": date.fromisoformat(params["start_date"]), "end_date": date.fromisoformat(params["end_date"]), "group_by": "date", "include": "sources", "sort_by": "date", "sort_order": "asc", }, }, ) response_json = await response.json() assert response_json == get_artists_streams(params, as_result=True) def get_artists_streams_totals(params: dict) -> List[dict]: if params.get("group_by"): return [{"date": f'2022-03-1{DSP.values().index(params["dsp"])}'}] else: return [ {"country_code": country_code, "dsp": dsp, "streams": j * 1021 + i * 11} for i, country_code in enumerate(params["country_code"], 1) for j, dsp in enumerate(params["dsp"], 1) ] @pytest.mark.parametrize( "params,expected_status,expected_result", ( ({}, HTTPStatus.BAD_REQUEST, None), ( {"spotify_artist_id": "arid01"}, HTTPStatus.OK, { "first_stream_date": {"amazon": "2022-03-12", "apple": "2022-03-10", "spotify": "2022-03-11"}, "worldwide": 6159, "country": 0, }, ), ( {"spotify_artist_id": "arid02", "country_code": "us", "dsp": "spotify"}, HTTPStatus.OK, {"first_stream_date": {"spotify": "2022-03-11"}, "worldwide": 1032, "country": 1043}, ), ), ) async def test_get_artists_streams_totals(params, expected_status, expected_result, mocker, client, auth): spotify_artist_id, dsp_list, country_code = ( params.get("spotify_artist_id"), params.get("dsp", "spotify,apple,amazon").split(","), params.get("country_code"), ) if spotify_artist_id: gras_artist_id = f"GRAS_{spotify_artist_id}" async def async_dsp_mock(url, **kwargs): if url == "api/delphi/search": return {"items": [{"artist_id": gras_artist_id}]} elif url == "api/consumer_analytics/streams-latest-date": return {"date": "2022-11-27"} elif url == "api/delphi/streams": return get_artists_streams_totals(kwargs["params"]) mocked_dsp_send = mocker.patch.object(DspApiClient, "send_request", side_effect=async_dsp_mock) response = await client.get("/api/artists/streams/totals/", params=params, headers=auth) assert response.status == expected_status if expected_status != HTTPStatus.OK: assert mocked_dsp_send.call_count == 0 return assert mocked_dsp_send.call_args_list[0] == ( ("api/delphi/search",), {"params": {"index": "artist", "query": f'spotify_artist_id:"spotify:artist:{params["spotify_artist_id"]}"'}}, ) assert mocked_dsp_send.call_args_list[1] == (("api/consumer_analytics/streams-latest-date",), {}) mocked_dsp_send.call_args_list[2].kwargs["params"]["dsp"] = list( sorted(mocked_dsp_send.call_args_list[2].kwargs["params"]["dsp"]) ) assert mocked_dsp_send.call_args_list[2] == ( ("api/delphi/streams",), { "params": { "artist_id": gras_artist_id, "dsp": list(sorted(dsp_list)), "start_date": "1970-01-01", "end_date": date(2022, 11, 27), "country_code": [Market.WORLDWIDE] + ([country_code] if country_code else []), }, }, ) call_args_list = list(sorted(mocked_dsp_send.call_args_list[3:], key=lambda i: i.kwargs["params"]["dsp"])) for index, dsp in enumerate(sorted(dsp_list)): assert call_args_list[index] == ( ("api/delphi/streams",), { "params": { "artist_id": gras_artist_id, "dsp": dsp, "start_date": "1970-01-01", "end_date": date(2022, 11, 27), "group_by": "date", "limit": 1, "sort_by": "date", "sort_order": "asc", }, }, ) response_json = await response.json() assert response_json == expected_result def get_artists_demographics(params: dict, as_result: bool = False) -> List[dict]: artist_id, country_code_list, dsp_list, start_date, end_date, per_day = ( f'GRAS_{params["spotify_artist_id"]}' if as_result else params["artist_id"], params.get("country_code", "worldwide").split(",") if as_result else params["country_code"], params.get("dsp", "apple,spotify,amazon").split(",") if as_result else params["dsp"], date.fromisoformat(params["start_date"]) if as_result else params["start_date"], date.fromisoformat(params["end_date"]) if as_result else params["end_date"], params.get("per_date", False), ) days_count = (end_date - start_date).days + 1 result = [] for day in range(min(5 if per_day else 1, days_count)): for index, country_code in enumerate(country_code_list, 1): if country_code == "_gl": country_code = "worldwide" spotify_age_bands, apple_age_bands = None, None amazon_streams_info, spotify_streams_info, apple_streams_info = None, None, None index = index + day // 2 if DSP.SPOTIFY.value in dsp_list: spotify_age_bands = { "all_0_17": index * 136, "all_18_22": index * 129, "all_23_27": index * 110, "all_28_34": index * 93, "all_35_44": index * 86, "all_45_59": index * 89, "all_60_150": index * 71, "all_unknown": index * 31, "female_0_17": index * 121, "female_18_22": index * 112, "female_23_27": index * 67, "female_28_34": index * 42, "female_35_44": index * 34, "female_45_59": index * 22, "female_60_150": index * 11, "female_unknown": index * 9, "male_0_17": index * 97, "male_18_22": index * 84, "male_23_27": index * 48, "male_28_34": index * 51, "male_35_44": index * 23, "male_45_59": index * 21, "male_60_150": index * 14, "male_unknown": index * 7, "neutral_0_17": index * 25, "neutral_18_22": index * 21, "neutral_23_27": index * 17, "neutral_28_34": index * 14, "neutral_35_44": index * 11, "neutral_45_59": index * 8, "neutral_60_150": index * 4, "neutral_unknown": index, "unknown_0_17": index * 36, "unknown_18_22": index * 45, "unknown_23_27": index * 23, "unknown_28_34": index * 17, "unknown_35_44": index * 13, "unknown_45_59": index * 9, "unknown_60_150": index * 4, "unknown_unknown": index * 8, } if DSP.APPLE.value in dsp_list: apple_age_bands = { "all_0_17": None, "all_18_24": index * 95, "all_25_34": index * 92, "all_35_44": index * 85, "all_45_54": index * 56, "all_55_64": index * 36, "all_65_plus": index * 31, "all_unknown": index * 28, "male_0_17": None, "male_18_24": index * 57, "male_25_34": index * 51, "male_35_44": index * 42, "male_45_54": index * 32, "male_55_64": index * 21, "male_65_plus": index * 12, "male_unknown": None, "female_0_17": None, "female_18_24": index * 67, "female_25_34": index * 65, "female_35_44": index * 36, "female_45_54": index * 26, "female_55_64": index * 21, "female_65_plus": index * 11, "female_unknown": None, "unknown_0_17": None, "unknown_18_24": index * 25, "unknown_25_34": index * 23, "unknown_35_44": index * 21, "unknown_45_54": index * 16, "unknown_55_64": index * 9, "unknown_65_plus": index * 3, "unknown_unknown": index * 46, } result.append( { "artist_id": artist_id, "country_code": country_code, "streams": index * 182, "genders": { "female": index * 152, "male": index * 102, "unknown": index * 74, "neutral": index * 27 }, "apple_age_bands": apple_age_bands, "spotify_age_bands": spotify_age_bands, "amazon_streams_info": amazon_streams_info, "spotify_streams_info": spotify_streams_info, "apple_streams_info": apple_streams_info, **({"date": (start_date + timedelta(days=day)).isoformat()} if per_day else {}), } ) return result @pytest.mark.parametrize( "params,expected_status", ( ({}, HTTPStatus.BAD_REQUEST), ({"spotify_artist_id": "arid01", "start_date": "2022-11-09", "end_date": "2022-11-18"}, HTTPStatus.OK), ( { "spotify_artist_id": "arid02", "start_date": "2022-11-11", "end_date": "2022-11-19", "country_code": "_gl,us", "dsp": "spotify", }, HTTPStatus.OK, ), ( { "spotify_artist_id": "arid01", "start_date": "2022-11-09", "end_date": "2022-11-18", "per_day": "true", "sort_by": "date", "sort_order": "asc", }, HTTPStatus.OK, ), ), ) async def test_get_artists_demographics(params, expected_status, mocker, client, auth): spotify_artist_id = params.get("spotify_artist_id") if spotify_artist_id: gras_artist_id = f"GRAS_{spotify_artist_id}" async def async_dsp_mock(url, **kwargs): if url == "api/delphi/search": return {"items": [{"artist_id": gras_artist_id}]} elif url == "api/delphi/streams": return get_artists_demographics(kwargs["params"]) mocked_dsp_send = mocker.patch.object(DspApiClient, "send_request", side_effect=async_dsp_mock) response = await client.get("/api/artists/demographics/", params=params, headers=auth) assert response.status == expected_status if expected_status != HTTPStatus.OK: assert mocked_dsp_send.call_count == 0 return assert mocked_dsp_send.call_args_list[0] == ( ("api/delphi/search",), {"params": {"index": "artist", "query": f'spotify_artist_id:"spotify:artist:{params["spotify_artist_id"]}"'}}, ) assert mocked_dsp_send.call_args_list[1] == ( ("api/delphi/streams",), { "params": { "artist_id": gras_artist_id, "dsp": params.get("dsp", "apple,spotify,amazon").split(","), "country_code": [ ("worldwide" if i == "_gl" else i) for i in params.get("country_code", "worldwide").split(",") ], "start_date": date.fromisoformat(params["start_date"]), "end_date": date.fromisoformat(params["end_date"]), "include": params.get("include", "demographics").split(","), "group_by": ("date" if params.get("per_day", False) else None), "sort_by": params.get("sort_by"), "sort_order": params.get("sort_order"), }, }, ) response_json = await response.json() assert response_json == get_artists_demographics(params, as_result=True) def get_streams(dsp_list: Optional[Union[List[str], str]]) -> List[Dict[str, str or int]]: if dsp_list: dsp_list = dsp_list.split(",") else: dsp_list = list(DSP_SPOTIFY_APPLE_AMAZON.values()) result = [] for i, dsp in enumerate(dsp_list): total = 0 for j, market in enumerate(("us", "ca", "gb", "it", "dk")): streams = 10000 + 1000 * i * (-1 if i % 2 else 1) - 100 * j * (-1 if j % 3 else 1) result.append({"country_code": market, "dsp": dsp, "streams": streams}) total += streams result.append({"country_code": "worldwide", "dsp": dsp, "streams": total}) return result @pytest.mark.parametrize( "params,expected_status,expected_result", ( ({}, HTTPStatus.BAD_REQUEST, {}), ({"start_date": "2021-08-01", "end_date": "2021-08-03"}, HTTPStatus.BAD_REQUEST, {}), ({"spotify_artist_id": "sp_ar_id"}, HTTPStatus.BAD_REQUEST, {}), ( {"spotify_artist_id": "sp_ar_id", "start_date": "2021-08-05", "end_date": "2021-08-03"}, HTTPStatus.BAD_REQUEST, {}, ), ( {"spotify_artist_id": "sp_ar_id", "dsp": "spotify", "start_date": "2021-08-01", "end_date": "2021-08-03"}, HTTPStatus.OK, { "spotify": [ {"market": "dk", "streams": 10400}, {"market": "gb", "streams": 10200}, {"market": "ca", "streams": 10100}, {"market": "us", "streams": 10000}, {"market": "it", "streams": 9700}, ], }, ), ( { "spotify_artist_id": "sp_ar_id", "dsp": "spotify", "start_date": "2021-08-01", "end_date": "2021-08-03", "limit": 2, "include_worldwide": "true", }, HTTPStatus.OK, {"spotify": [{"market": "worldwide", "streams": 50400}, {"market": "dk", "streams": 10400}]}, ), ( {"spotify_artist_id": "sp_ar_id", "start_date": "2021-08-01", "end_date": "2021-08-03", "limit": 2}, HTTPStatus.OK, { "amazon": [{"market": "dk", "streams": 12400}, {"market": "gb", "streams": 12200}], "apple": [{"market": "dk", "streams": 10400}, {"market": "gb", "streams": 10200}], "spotify": [{"market": "dk", "streams": 9400}, {"market": "gb", "streams": 9200}], }, ), ( {"spotify_artist_id": "sp_ar_id", "start_date": "2021-08-01", "end_date": "2021-08-03", "combine": "true"}, HTTPStatus.OK, { "all": [ {"market": "dk", "streams": 32200}, {"market": "gb", "streams": 31600}, {"market": "ca", "streams": 31300}, {"market": "us", "streams": 31000}, {"market": "it", "streams": 30100}, ], }, ), ( { "spotify_artist_id": "sp_ar_id", "start_date": "2021-08-01", "end_date": "2021-08-03", "combine": "true", "dsp": "spotify,apple", }, HTTPStatus.OK, { "all": [ {"market": "dk", "streams": 19800}, {"market": "gb", "streams": 19400}, {"market": "ca", "streams": 19200}, {"market": "us", "streams": 19000}, {"market": "it", "streams": 18400}, ], }, ), ), ) async def test_get_artists_markets_top( params: dict, expected_status: int, expected_result: Dict[str, List[dict]], mocker: MockerFixture, auth: dict, client: TestClient, ): call_count = 2 if expected_status == HTTPStatus.OK else 0 spotify_artist_id = params.get("spotify_artist_id") if spotify_artist_id: gras_artist_id = f"GRAS_{spotify_artist_id}" async def dsp_send(url: str, *args, **kwargs): if url == "api/delphi/search": return {"items": [{"artist_id": gras_artist_id}]} else: return get_streams(params.get("dsp")) mocked_dsp_send = mocker.patch.object(DspApiClient, "send_request", side_effect=dsp_send) response = await client.get("/api/artists/markets/top/", params=params, headers=auth) assert response.status == expected_status assert mocked_dsp_send.call_count == call_count if not call_count: return assert mocked_dsp_send.call_args_list[0] == ( ("api/delphi/search",), {"params": {"index": "artist", "query": f'spotify_artist_id:"spotify:artist:{params["spotify_artist_id"]}"'}}, ) assert mocked_dsp_send.call_args_list[1] == ( ("api/delphi/streams",), { "params": { "artist_id": gras_artist_id, "dsp": params.get("dsp", "apple,spotify,amazon").split(","), "start_date": date.fromisoformat(params["start_date"]), "end_date": date.fromisoformat(params["end_date"]), }, }, ) response = await response.json() assert response == expected_result def get_isrc(index: int) -> str: return f"ISRC{index:08d}" def get_spotify_track_name(index: int) -> str: return f"SpTrN{index}" def get_spotify_track(index: int) -> dict: return { "id": f"sp_tr_id_{index}", "name": get_spotify_track_name(index), "uri": f"spotify:track:sp_tr_id_{index}", "external_ids": {"isrc": get_isrc(index)}, } def get_apple_track_name(index: int) -> str: return f"ApTrN{index}" def get_apple_track(index: int) -> dict: return { "id": f"ap_tr_id_{index}", "attributes": {"isrc": get_isrc(index), "name": get_apple_track_name(index), "artistName": f"ApArN{index}"}, } def get_artist_isrc_streams(country_code_list: Union[List[str], str], limit: int) -> List[Dict[str, str or int]]: if isinstance(country_code_list, str): country_code_list = country_code_list.split(",") country_code_count = len(country_code_list) return [ { "artist_id": f"GRAS_ar_id_{index}", "country_code": country_code_list[index % country_code_count], "isrc": get_isrc(index), "streams": 600300 - index * 2211, } for index in range(1, limit + 1) ] def get_artist_top_tracks_result( country_code_list: Union[List[str], str], limit: int, spotify_count: int, apple_count: int, include_list: str, ) -> Dict[str, List[dict]]: include_list = include_list.split(",") result = get_artist_isrc_streams(country_code_list, limit) for index, item in enumerate(result, 1): del item["artist_id"] del item["country_code"] if index <= spotify_count: name_func = get_spotify_track_name meta_func = get_spotify_track elif index <= spotify_count + apple_count: name_func = get_apple_track_name meta_func = get_apple_track else: continue if "name" in include_list: item["name"] = name_func(index) if "meta" in include_list: item["meta"] = meta_func(index) return {"items": result} @pytest.mark.parametrize( "params,status,call_count,tracks_count", ( ({}, HTTPStatus.BAD_REQUEST, (0, 0), (0, 0)), ({"start_date": "2021-08-01", "end_date": "2021-08-03"}, HTTPStatus.BAD_REQUEST, (0, 0), (0, 0)), ({"spotify_artist_id": "sp_ar_id"}, HTTPStatus.BAD_REQUEST, (0, 0), (0, 0)), ( {"spotify_artist_id": "sp_ar_id", "start_date": "2021-08-05", "end_date": "2021-08-03"}, HTTPStatus.BAD_REQUEST, (0, 0), (0, 0), ), ( {"spotify_artist_id": "sp_ar_id", "dsp": "spotify", "start_date": "2021-08-01", "end_date": "2021-08-03"}, HTTPStatus.BAD_REQUEST, (0, 0), (0, 0), ), ( { "spotify_artist_id": "sp_ar_id", "dsp": "spotify", "country_code": "us,gb,ca", "start_date": "2021-08-01", "end_date": "2021-08-03", }, HTTPStatus.OK, (2, 1), (10, 0), ), ( { "spotify_artist_id": "sp_ar_id", "dsp": "spotify,apple", "country_code": "us,gb,ca", "start_date": "2021-08-01", "end_date": "2021-08-03", "meta_country_code": "ca", "limit": 8, "include": "meta", }, HTTPStatus.OK, (2, 2), (6, 2), ), ( { "spotify_artist_id": "sp_ar_id", "dsp": "apple", "country_code": "us,gb,ca,au", "start_date": "2021-08-01", "end_date": "2021-08-03", "limit": 8, "include": "name,meta", }, HTTPStatus.OK, (2, 2), (4, 2), ), ), ) async def test_get_artists_tracks_top( params: dict, status: int, call_count: Tuple[int, int], tracks_count: Tuple[int, int], mocker: MockerFixture, auth: dict, client: TestClient, ): spotify_artist_id = params.get("spotify_artist_id") if spotify_artist_id: gras_artist_id = f"GRAS_{spotify_artist_id[3:]}" limit = int(params.get("limit", 10)) spotify_count, apple_count = tracks_count async def dsp_send(url: str, *args, **kwargs): if url == "api/delphi/search": return {"items": [{"artist_id": gras_artist_id}]} else: return get_artist_isrc_streams(params.get("country_code"), limit) mocked_dsp_send = mocker.patch.object(DspApiClient, "send_request", side_effect=dsp_send) async def vendor_send(url: str, *args, **kwargs): if url == "api/spotify/v1/tracks": return {"tracks": [get_spotify_track(index) for index in range(1, spotify_count + 1)]} else: return { "data": [get_apple_track(index) for index in range(spotify_count + 1, spotify_count + apple_count + 1)] } mocked_vendor_send = mocker.patch.object(VendorApiClient, "send_request", side_effect=vendor_send) response = await client.get("/api/artists/tracks/top/", params=params, headers=auth) assert response.status == status assert mocked_dsp_send.call_count == call_count[0] assert mocked_vendor_send.call_count == call_count[1] if status != HTTPStatus.OK: return response = await response.json() assert response == get_artist_top_tracks_result( params.get("country_code"), limit, spotify_count, apple_count, params.get("include", "name") )