"""Test that Song streams are consistent across different dimensions and pages.""" from copy import deepcopy from datetime import datetime import pytest import requests from dateutil.relativedelta import relativedelta from owsrequest.constants import headers as owsrequest_headers from analytics import config def generate_date_range(start_date, end_date): start_date = datetime.strptime(start_date, "%Y-%m-%d") end_date = datetime.strptime(end_date, "%Y-%m-%d") dates = [] current_date = start_date while current_date <= end_date: dates.append(current_date.strftime("%Y-%m-%d")) current_date += relativedelta(days=1) return dates """Profile with access to everything.""" INSIGHTS_EMPLOYEE_REQUEST_HEADERS = { owsrequest_headers.ORCHARD_PROFILE_ID: "1100", owsrequest_headers.ORCHARD_PROFILE_TYPE: "InsightsProfile", } class TestStreamsConsistencyHarleyQuinn: """The goal of this test is to summarize the streams for a given track across different dimensions, accounts, artists, products, etc., and check that the figures are matching.""" isrc = "QZ9QQ2300781" # Harley Quinn https://insights.theorchard.com/song/QZ9QQ2300781/ START_DATE = "2023-10-20" END_DATE = "2023-11-08" DATE_RANGE = f"start_date={START_DATE}&end_date={END_DATE}&" SONG_TIME_SERIES_URL = f"{config.BASE_URL}/sound-recording/{isrc}/timeseries?" PRODUCT_4709407_URL = ( f"{config.BASE_URL}/product/4709407/timeseries?type=PRODUCT_STREAMS_BY_TRACK&" ) PRODUCT_4730467_URL = ( f"{config.BASE_URL}/product/4730467/timeseries?type=PRODUCT_STREAMS_BY_TRACK&" ) PRODUCT_4725671_URL = ( f"{config.BASE_URL}/product/4725671/timeseries?type=PRODUCT_STREAMS_BY_TRACK&" ) ACCOUNT_LATIN_FUERZA_REGIDA_DISTRIBUTION_URL = f"{config.BASE_URL}/account/79665/timeseries?account_type=subaccount&type=ACCOUNT_STREAMS_BY_TRACK&" ACCOUNT_SME_MULTIPLE_REP_OWNERS = f"{config.BASE_URL}/account/61168/timeseries?account_type=subaccount&type=ACCOUNT_STREAMS_BY_TRACK&" # this shouldn't change unless the data changes (backfills, adding new feeds, etc.) SONG_AGGREGATED_VALUES_FOR_DATE_RANGE = { "total_value": 64097973, "total_saves": 2748819, "total_skips": 13736838, "avg_skip_rate": round(0.178, 3), } ACCOUNT_AGGREGATED_VALUES_FOR_DATE_RANGE = { 79665: { "total_value": 63180592, "total_saves": 2708494, "total_skips": 13454528, "avg_skip_rate": round(0.177, 3), }, 61168: { "total_value": 917381, "total_saves": 40325, "total_skips": 282310, "avg_skip_rate": round(0.213, 3), }, } PRODUCT_AGGREGATED_VALUES_FOR_DATE_RANGE = { 4709407: { "total_value": 63180592, "total_saves": 2708494, "total_skips": 13454528, "avg_skip_rate": round(0.177, 3), }, 4725671: { "total_value": 917381, "total_saves": 40325, "total_skips": 282310, "avg_skip_rate": round(0.213, 3), }, } @staticmethod def aggregate_response(data): """Aggregate a response to check against.""" total_value = 0 total_saves = 0 total_skips = 0 avg_skip_rate = 0.0 for item in data: total_value += item["value"] total_saves += item["saves"] total_skips += item["skips"] avg_skip_rate += item["skip_rate"] avg_skip_rate = round(avg_skip_rate / len(data), 3) return { "total_value": total_value, "total_saves": total_saves, "total_skips": total_skips, "avg_skip_rate": avg_skip_rate, } @pytest.fixture def sound_recording_timeseries_response(self): """Return the song timeseries response.""" return requests.get( self.SONG_TIME_SERIES_URL + self.DATE_RANGE, headers=INSIGHTS_EMPLOYEE_REQUEST_HEADERS, ) @pytest.fixture( params=[ "0ea085df-f44b-4810-b997-7692307d83eb", "a52c2f26-64cb-421d-b94d-90e669212d85", ] ) def artist_url(self, request): """Return artist URLs.""" return f"{config.BASE_URL}/participant/{request.param}/timeseries?type=TRACK_STREAMS_BY_SOUND_RECORDING&" @pytest.fixture def artist_timeseries_response(self, artist_url): """Return a artist timeseries response.""" return requests.get( artist_url + self.DATE_RANGE, headers=INSIGHTS_EMPLOYEE_REQUEST_HEADERS, ) def test_artist_response(self, artist_timeseries_response): """Test that the artist response is successful.""" assert artist_timeseries_response.status_code == requests.codes.ok date_range = generate_date_range(self.START_DATE, self.END_DATE) dates_from_response = [ item["date"] for item in artist_timeseries_response.json()["items"] if item["id"] == self.isrc ] # check if all the dates are present in the response assert sorted(date_range) == sorted(dates_from_response) # check if the aggregated value is stable assert self.SONG_AGGREGATED_VALUES_FOR_DATE_RANGE == self.aggregate_response( [ item for item in artist_timeseries_response.json()["items"] if item["id"] == self.isrc ] ) @pytest.fixture def product_timeseries_4709407_response(self): """Return the product 4709407 timeseries response.""" return requests.get( self.PRODUCT_4709407_URL + self.DATE_RANGE, headers=INSIGHTS_EMPLOYEE_REQUEST_HEADERS, ) # @pytest.fixture def product_timeseries_4730467_response(self): """Return the product 4730467 timeseries response.""" return requests.get( self.PRODUCT_4730467_URL + self.DATE_RANGE, headers=INSIGHTS_EMPLOYEE_REQUEST_HEADERS, ) @pytest.fixture def product_timeseries_4725671_response(self): """Return the product 4725671 timeseries response.""" return requests.get( self.PRODUCT_4725671_URL + self.DATE_RANGE, headers=INSIGHTS_EMPLOYEE_REQUEST_HEADERS, ) def test_product_4709407_response(self, product_timeseries_4709407_response): """Test that the product 4709407 response is successful.""" assert product_timeseries_4709407_response.status_code == requests.codes.ok date_range = generate_date_range(self.START_DATE, self.END_DATE) dates_from_response = [ item["date"] for item in product_timeseries_4709407_response.json()["items"] if item["id"] == self.isrc ] # check if all the dates are present in the response assert sorted(date_range) == sorted(dates_from_response) # check if the aggregated value is stable assert self.PRODUCT_AGGREGATED_VALUES_FOR_DATE_RANGE[ 4709407 ] == self.aggregate_response( [ item for item in product_timeseries_4709407_response.json()["items"] if item["id"] == self.isrc ] ) def test_product_4730467_response(self, product_timeseries_4730467_response): """Test that the product 4730467 response is successful.""" assert product_timeseries_4730467_response.status_code == requests.codes.ok # empty response for https://insights.theorchard.com/product/196871562898/ assert product_timeseries_4730467_response.json()["items"] == [] def test_product_4725671_response(self, product_timeseries_4725671_response): """Test that the product 4725671 response is successful.""" assert product_timeseries_4725671_response.status_code == requests.codes.ok # release date is 2023-11-03, but it looks like we have some data for 2023-11-02 (probably a timezone coersion) date_range = generate_date_range("2023-11-02", self.END_DATE) dates_from_response = [ item["date"] for item in product_timeseries_4725671_response.json()["items"] if item["id"] == self.isrc ] # check if all the dates are present in the response assert sorted(date_range) == sorted(dates_from_response) # check if the aggregated value is stable assert self.PRODUCT_AGGREGATED_VALUES_FOR_DATE_RANGE[ 4725671 ] == self.aggregate_response( [ item for item in product_timeseries_4725671_response.json()["items"] if item["id"] == self.isrc ] ) @pytest.fixture def account_latinfuerza_timeseries_response(self): """Return the account 79665 timeseries response.""" return requests.get( self.ACCOUNT_LATIN_FUERZA_REGIDA_DISTRIBUTION_URL + self.DATE_RANGE, headers=INSIGHTS_EMPLOYEE_REQUEST_HEADERS, ) @pytest.fixture def account_sme_timeseries_response(self): """Return the account 61168 timeseries response.""" return requests.get( self.ACCOUNT_SME_MULTIPLE_REP_OWNERS + self.DATE_RANGE, headers=INSIGHTS_EMPLOYEE_REQUEST_HEADERS, ) def test_account_latinfuerza_timeseries_response( self, account_latinfuerza_timeseries_response ): """Test that the account 79665 response is successful and contains correct data.""" assert account_latinfuerza_timeseries_response.status_code == requests.codes.ok date_range = generate_date_range(self.START_DATE, self.END_DATE) dates_from_response = [ item["date"] for item in account_latinfuerza_timeseries_response.json()["items"] if item["id"] == self.isrc ] # check if all the dates are present in the response assert sorted(date_range) == sorted(dates_from_response) # check if the aggregated value is stable assert self.ACCOUNT_AGGREGATED_VALUES_FOR_DATE_RANGE[ 79665 ] == self.aggregate_response( [ item for item in account_latinfuerza_timeseries_response.json()["items"] if item["id"] == self.isrc ] ) assert ( self.ACCOUNT_AGGREGATED_VALUES_FOR_DATE_RANGE[79665] == self.PRODUCT_AGGREGATED_VALUES_FOR_DATE_RANGE[4709407] ) @pytest.mark.skip( reason="TODO: Investigate 504 account_sme_timeseries_response.status_code" ) def test_account_sme_timeseries_response(self, account_sme_timeseries_response): """Test that the account 61168 response is successful and contains correct data.""" assert account_sme_timeseries_response.status_code == requests.codes.ok # release date of the product belonging to this account is 2023-11-03, # but it looks like we have some data for 2023-11-02 (probably a timezone coersion) date_range = generate_date_range("2023-11-02", self.END_DATE) dates_from_response = [ item["date"] for item in account_sme_timeseries_response.json()["items"] if item["id"] == self.isrc ] # check if all the dates are present in the response assert sorted(date_range) == sorted(dates_from_response) # check if the aggregated value is stable assert self.ACCOUNT_AGGREGATED_VALUES_FOR_DATE_RANGE[ 61168 ] == self.aggregate_response( [ item for item in account_sme_timeseries_response.json()["items"] if item["id"] == self.isrc ] ) assert ( self.ACCOUNT_AGGREGATED_VALUES_FOR_DATE_RANGE[61168] == self.PRODUCT_AGGREGATED_VALUES_FOR_DATE_RANGE[4725671] ) def test_sound_recording_timeseries_response( self, sound_recording_timeseries_response ): """Test that the Song response is successful and contains correct data.""" assert sound_recording_timeseries_response.status_code == requests.codes.ok date_range = generate_date_range(self.START_DATE, self.END_DATE) dates_from_response = [ item["date"] for item in sound_recording_timeseries_response.json()["items"] ] # check if all the dates are present in the response assert sorted(date_range) == sorted(dates_from_response) # check if the aggregated value is stable assert self.SONG_AGGREGATED_VALUES_FOR_DATE_RANGE == self.aggregate_response( sound_recording_timeseries_response.json()["items"] ) def test_sum_for_products_against_song( self, sound_recording_timeseries_response, product_timeseries_4725671_response, product_timeseries_4709407_response, ): """Test that the sum of the streams of the song on the products is equal to the song.""" song_agg_values = self.aggregate_response( sound_recording_timeseries_response.json()["items"] ) product_4725671_agg_values = self.aggregate_response( [ item for item in product_timeseries_4725671_response.json()["items"] if item["id"] == self.isrc ] ) product_4709407_agg_values = self.aggregate_response( [ item for item in product_timeseries_4709407_response.json()["items"] if item["id"] == self.isrc ] ) assert ( song_agg_values["total_value"] == product_4725671_agg_values["total_value"] + product_4709407_agg_values["total_value"] ) assert ( song_agg_values["total_saves"] == product_4725671_agg_values["total_saves"] + product_4709407_agg_values["total_saves"] ) assert ( song_agg_values["total_skips"] == product_4725671_agg_values["total_skips"] + product_4709407_agg_values["total_skips"] ) assert round(song_agg_values["avg_skip_rate"], 1) == round( ( product_4725671_agg_values["avg_skip_rate"] + product_4709407_agg_values["avg_skip_rate"] ) / 2, 2, ) @pytest.fixture( params=[ "TRACK_STREAMS_BY_STORE", "TRACK_STREAMS_BY_COUNTRY", "TRACK_STREAMS_BY_PRODUCT", ] ) def song_dimensions_response(self, request): """Return a song dimensions response.""" return requests.get( self.SONG_TIME_SERIES_URL + self.DATE_RANGE + f"type={request.param}&", headers=INSIGHTS_EMPLOYEE_REQUEST_HEADERS, ) def test_song_dimension_response(self, song_dimensions_response): """Test that the figures are consistent across Song dimensions.""" assert song_dimensions_response.status_code == requests.codes.ok date_range = generate_date_range(self.START_DATE, self.END_DATE) dates_from_response = list( set([item["date"] for item in song_dimensions_response.json()["items"]]) ) # check if all the dates are present in the response assert sorted(date_range) == sorted(dates_from_response) # check if the aggregated value is stable, but skip rate is not applicable for all dimensions agg_values = deepcopy(self.SONG_AGGREGATED_VALUES_FOR_DATE_RANGE) agg_response = deepcopy( self.aggregate_response( [item for item in song_dimensions_response.json()["items"]] ) ) del agg_values["avg_skip_rate"] del agg_response["avg_skip_rate"] assert agg_values == agg_response @pytest.mark.skip( reason="Soundcloud data landed into some tables? Not critical. " "Disabling for now, we have a project to ensure consistency between components." ) class TestArtistStreamsAllTimeConsistencyLilPeep: """The goal of this test is to check if the All Time Streams hero metric is consistent with the results of the all time summary TOTAL streams regardless of the show_sme_data FF. https://theorchard.atlassian.net/browse/IN-11725 """ artist_id = "d5065b8d-733f-44d9-84dd-d98a0f661e38" # Lil Peep https://insights.theorchard.com/artist/d5065b8d-733f-44d9-84dd-d98a0f661e38/ ARTIST_SUMMARY_URL = f"{config.BASE_URL}/participant/{artist_id}/summary?type=TOTAL&start_date=1900-01-01&end_date={datetime.today().date()}&" ARTIST_METRICS_URL = ( f"{config.BASE_URL}/participant-metrics?global_participant_ids={artist_id}&" ) @pytest.fixture( params=[ "awal,theorchard", "awal,theorchard,sme", ] ) def distributors(self, request): """Return distributors.""" return request.param def test_all_time_streams_consistency(self, distributors): """Test that the figures are consistent.""" artist_summary = requests.get( self.ARTIST_SUMMARY_URL + f"distributors={distributors}", headers=INSIGHTS_EMPLOYEE_REQUEST_HEADERS, ).json() artist_all_time_hero_metric = requests.get( self.ARTIST_METRICS_URL + f"distributors={distributors}", headers=INSIGHTS_EMPLOYEE_REQUEST_HEADERS, ).json() assert ( artist_summary["items"][0]["streams"] == artist_all_time_hero_metric["metrics"][0]["streams_all_time"] )