"""Application Handlers. Requests are redirected to handlers, which are responsible for getting information from the URL and passing it down to the logic layer. The way each layer talks to each other is through Response objects which defines the type status of the data and the data itself. Please note: the Orchard uses the term handlers over views as convention for clarity See: oto.response for more details. """ from ddtrace import tracer from flask import request from flask.json import jsonify from flask_apispec import use_kwargs, marshal_with from oto.adaptors.flask import flaskify from owsrequest import context from owsresponse import response from charts import config from charts.api import app from charts.features import get_controlled_chart_feature_flags from charts.features import get_show_sme_data_flag from charts.features import get_ff_charts_cache from charts.logic import artist, digest from charts.logic import cache_prime from charts.logic import chart from charts.logic import charts from charts.logic import track from charts.logic.dataloaders.chart_groups.chart_ids_dataloader import chart_groups_chart_ids_dataloader from charts.logic.dataloaders.charts.dates_dataloader import chart_dates_dataloader from charts.logic.dataloaders.charts import aggregated_values_dataloader from charts.logic.dataloaders.charts import metadata_dataloader from charts.logic.dataloaders.charts.chart_date_entry_dataloader import chart_date_entry_dataloader from charts.logic.dataloaders.charts.chart_date_entry_summary_dataloader import chart_date_entry_summary_dataloader from charts.logic.dataloaders.charts.dates import placements_dataloader from charts.logic.dataloaders.charts.dates.utils import get_chart_date_epochs from charts.logic.dataloaders.charts.dates.placements import private_dataloader from charts.logic.dataloaders.charts.dates.placements import public_dataloader from charts.logic.dataloaders.charts.dates.placements.utils import get_chart_date_placements_epochs from charts.logic.permissions import get_request_permissions from charts.schemas.chart.digest import ChartDigestTracks from charts.schemas.charts.by_platforms import ChartsByPlatforms from charts.schemas.charts.by_definition import ChartsByDefinition from charts.schemas.chart_placement.dataloader_private import ChartsPlacementDataloaderPrivate from charts.schemas.track.charts import TrackCharts from charts.schemas.chart.rankings_for_chart_ids import ChartRankingsForChartIds from charts.schemas.chart.chart_data_for_chart_ids import ChartDataForChartIds from charts.schemas.chart.chart_rankings_dataloaded import ChartRankingsDataloaded from charts.schemas.chart.chart_rankings_aggregated_values_dataloaded import ChartRankingsAggregatedValuesDataloaded from charts.schemas.chart.chart_rankings_aggregated_values_on_date_dataloaded import ChartRankingsAggregatedValuesOnDateDataloaded from charts.schemas.chart.dates_for_chart_ids import ChartDatesForChartIds from charts.schemas.chart.metadata_for_chart_ids import MetadataForChartIds from charts.schemas.chart.cache_prime import CachePrime from charts.schemas.chart.global_sound_recordings_for_charts_date import GlobalSoundRecordingsForChartsDate from charts.schemas.chart.chart_date_entry_for_isrcs import ChartDateEntryForIsrcsDataloaded from charts.schemas.chart.chart_date_entry_summary_for_isrcs import ChartDateEntryForIsrcsSummaryDataloaded from charts.schemas.chart_groups.chart_ids import ChartGroupsChartIds @app.route(config.HEALTH_CHECK, methods=["GET"]) @tracer.wrap(name=config.HEALTH_CHECK) def health(): """Check the health of the application.""" return jsonify({"status": "ok"}) @app.errorhandler(500) def exception_handler(error): """Handle error when uncaught exception is raised. Default exception handler. Note: Exception will also be sent to Sentry if config.SENTRY is set. Returns: flask.Response: A 500 response with JSON 'code' & 'message' payload. """ message = ( "The server encountered an internal error " "and was unable to complete your request." ) app.log_exception(error) return flaskify(response.create_fatal_response(message)) @app.route('/charts/') def get_charts(): """ graphql query: ChartsQuery :return: list of dicts of chart data """ data = chart.get_charts() return jsonify(data) @app.route('/charts//') def get_chart(chart_id): """ graphql query: - :param chart_id: :return: dict of chart data """ data = chart.get_chart(chart_id) return jsonify(data) @app.route('/charts//dates/') def get_chart_available_dates(chart_id): """ graphql query: ChartDatesQuery :param chart_id: :return: list of str """ data = chart.get_chart_available_dates(chart_id) return jsonify(data) @app.route('/charts//dates/') def get_chart_rankings(chart_id, chart_date): """ graphql query: ChartRankingsQuery :param chart_id: dim_chart.chartid :param chart_date: date in format YYYY-MM-DD :return: list of chart rankings ordered by position """ limit = request.args.get('limit', type=int) or \ config.CHART_RANKINGS_DEFAULT_LIMIT_ROWS offset = request.args.get('offset', type=int) or 0 data = chart.get_chart_rankings(chart_id, chart_date, limit, offset) return jsonify(data) @app.route('/charts//dates//in_catalog') def get_chart_rankings_in_catalog(chart_id, chart_date): """ graphql query: ChartRankingsQuery Args chart_id (str): dim_chart.chartid chart_date (str): date in format YYYY-MM-DD Returns: list of chart rankings in catalog ordered by position """ limit = request.args.get('limit', type=int) or \ config.CHART_RANKINGS_DEFAULT_LIMIT_ROWS offset = request.args.get('offset', type=int) or 0 request_context = context.get_request_context_from_headers(request.headers) data = chart.get_chart_rankings_in_catalog(request_context, chart_id, chart_date, limit, offset) return jsonify(data) @app.route('/charts//song-history/') def get_song_rankings(chart_id, public_sound_recording_id): """ graphql query: ChartRankingsQuery :param chart_id: dim_chart.chartid :param public_sound_recording_id: id of PublicSoundRecording node in Neo4j :return: list of chart rankings ordered by date """ data = chart.get_chart_song_history(chart_id, public_sound_recording_id) return jsonify(data) @app.route('/charts//product-history/') def get_product_rankings(chart_id, public_product_id): """ graphql query: ChartRankingsQuery :param chart_id: dim_chart.chartid :param public_product_id: id of PublicProduct node in Neo4j :return: list of chart rankings ordered by date """ data = chart.get_chart_product_history(chart_id, public_product_id) return jsonify(data) @app.route('/public-sound-recordings/') def get_track_aggregated_rankings(public_sound_recording_id): """ Args: track_id (str): chartmetric track_id """ appeared_on_date = request.args.get('appeared_on_date', type=str) data = track.get_track_aggregated_rankings( public_sound_recording_id=public_sound_recording_id, appeared_on_date=appeared_on_date) return jsonify(data) @app.route('/artists//aggregated') def get_artist_aggregated_rankings(artist_id): """ Args: artist_id (int): chartmetric artist_id """ limit = request.args.get('limit', type=int) or \ config.CHART_RANKINGS_DEFAULT_LIMIT_ROWS offset = request.args.get('offset', type=int) or 0 public_sound_recording_id = request.args.get('public_sound_recording_id') isrc = request.args.get('isrc') order_by = request.args.get('order_by', 'POSITION') order_direction = request.args.get('order_dir', 'asc') only_current = request.args.get('only_current', False) chart_filter = {} for p in config.CHART_FILTER_PROPERTY_TO_COLUMN: value = request.args.get(f'chart_{p}') if not value: continue chart_filter[p] = value data = artist.get_artist_aggregated_rankings( artist_id=artist_id, chart_filter=chart_filter, limit=limit, offset=offset, order_by=order_by, order_direction=order_direction, isrc=isrc, public_sound_recording_id=public_sound_recording_id, only_current=only_current, ) return jsonify(data) @app.route('/artists//songs') def get_artist_charted_songs(artist_id): """ graphql query: GlobalParticipantChartedSongs Args: artist_id (int): chartmetric artist_id """ public_sound_recording_id = request.args.get('psr_id') isrc = request.args.get('isrc') only_current = request.args.get('only_current', False) chart_filter = {} for p in config.CHART_FILTER_PROPERTY_TO_COLUMN: value = request.args.get(f'chart_{p}') if not value: continue chart_filter[p] = value data = artist.artist_songs( artist_id=artist_id, chart_filter=chart_filter, isrc=isrc, public_sound_recording_id=public_sound_recording_id, only_current=only_current) return jsonify(data) @app.route('/artists//charts') def get_artist_charts(artist_id): """ Args: artist_id (int): chartmetric artist_id """ public_sound_recording_id = request.args.get('psr_id') isrc = request.args.get('isrc') only_current = request.args.get('only_current', False) chart_filter = {} for p in config.CHART_FILTER_PROPERTY_TO_COLUMN: value = request.args.get(f'chart_{p}') if not value: continue chart_filter[p] = value data = artist.artist_charts( artist_id=artist_id, chart_filter=chart_filter, isrc=isrc, public_sound_recording_id=public_sound_recording_id, only_current=only_current) return jsonify(data) @app.route('/artists//countries') def get_artist_countries(artist_id): """ Args: artist_id (int): chartmetric artist_id """ public_sound_recording_id = request.args.get('psr_id') isrc = request.args.get('isrc') only_current = request.args.get('only_current', False) chart_filter = {} for p in config.CHART_FILTER_PROPERTY_TO_COLUMN: value = request.args.get(f'chart_{p}') if not value: continue chart_filter[p] = value data = artist.artist_countries( artist_id=artist_id, chart_filter=chart_filter, isrc=isrc, public_sound_recording_id=public_sound_recording_id, only_current=only_current) return jsonify(data) @app.route('/artists//current-appearances') def get_artist_current_appearances(artist_id): """ Args: artist_id (int): chartmetric artist_id """ public_sound_recording_id = request.args.get('psr_id') isrc = request.args.get('isrc') only_current = request.args.get('only_current', False) chart_filter = {} for p in config.CHART_FILTER_PROPERTY_TO_COLUMN: value = request.args.get(f'chart_{p}') if not value: continue chart_filter[p] = value data = artist.artist_current_appearances( artist_id=artist_id, chart_date=request.args.get('chart_date'), chart_id=request.args.get('chart_id'), chart_filter=chart_filter, isrc=isrc, public_sound_recording_id=public_sound_recording_id, only_current=only_current) return jsonify(data) @app.route('/artists//chart-highlights') def get_artist_chart_highlights(artist_id): """ Args: artist_id (int): chartmetric artist_id """ limit = request.args.get('limit') offset = request.args.get('offset') data = artist.artist_chart_highlights( artist_id=artist_id, limit=limit, offset=offset) return jsonify(data) @app.route("/track/charts/") @use_kwargs(TrackCharts.Request, location="query") @marshal_with(TrackCharts.Response(many=True), apply=True) def get_track_charts(**data): isrc = data["isrc"] platforms = data["platforms"] current_appearances = data["current_appearances"] order_by = data["order_by"] return track.get_track_charts( isrc=isrc, platforms=platforms, current_appearances=current_appearances, order_by=order_by ) @app.route("/charts/by-platforms/") @use_kwargs(ChartsByPlatforms.Request, location="query") @marshal_with(ChartsByPlatforms.Response(many=True), apply=True) def get_charts_by_platforms(**data): platforms = data["platforms"] return charts.get_charts_by_platforms_with_latest_dates(platforms) @app.route("/charts/placement-dataloader-private", methods=['POST']) @use_kwargs(ChartsPlacementDataloaderPrivate.Request(many=True), location="json") @marshal_with(ChartsPlacementDataloaderPrivate.Response(many=True), apply=True) def chart_placement_private_dataloader(*data): show_sme_data = get_show_sme_data_flag() permissions = get_request_permissions() chart_placement_with_epochs = get_chart_date_placements_epochs(data) return private_dataloader.chart_placement_private_data_dataloader.load_many( chart_placement_with_epochs, show_sme_data=show_sme_data, permissions=permissions, ) @app.route("/chart/digest/tracks/") @use_kwargs(ChartDigestTracks.Request, location="query") @marshal_with(ChartDigestTracks.Response(many=True), apply=False) def get_chart_digest_tracks(**data): filter_condition = data.pop("filter") result, entries_isrc_list = digest.get_chart_digest_tracks_with_entries(**data) return digest.filter_and_sort( all_items=ChartDigestTracks.Response(many=True, context={"entries": entries_isrc_list}).dump(result), filter_condition=filter_condition ) @app.route("/charts/by-definition/") @use_kwargs(ChartsByDefinition.Request, location="query") @marshal_with(ChartsByDefinition.Response(many=True), apply=True) def get_charts_by_definition(**data): return charts.get_charts_by_definition(**data) @app.route("/charts/rankings-for-chart-ids/", methods=["POST"]) @use_kwargs(ChartRankingsForChartIds.Request, location="json") @marshal_with(ChartRankingsForChartIds.Response(many=True), apply=True) def get_chart_rankings_for_chart_ids(**data): ids = data['chart_ids_with_date'] filter = data.get('filter') order = data.get('order') chart_dates_with_epochs = get_chart_date_epochs(ids) show_sme_data = None if placements_dataloader.is_show_sme_data_required(filter=filter): show_sme_data = get_show_sme_data_flag() permissions = None if placements_dataloader.is_permissions_required(filter=filter): permissions = get_request_permissions() if get_ff_charts_cache(): return placements_dataloader.chart_placements_for_chart_date_dataloader.load_many( chart_dates_with_epochs, filter=filter, order=order, show_sme_data=show_sme_data, permissions=permissions, ) return placements_dataloader._get_chart_placements_for_chart_dates( chart_dates_with_epochs, filter=filter, order=order, show_sme_data=show_sme_data, permissions=permissions, ) @app.route("/chart-rankings/dataloaded/", methods=["POST"]) @use_kwargs(ChartRankingsDataloaded.Request, location="json") @marshal_with(ChartRankingsDataloaded.Response(many=True), apply=True) def get_chart_rankings_dataloaded(**data): ids = data.pop('chart_rankings') chart_date_placements_with_epochs = get_chart_date_placements_epochs(ids) if get_ff_charts_cache(): return public_dataloader.chart_placement_public_data_dataloader.load_many(chart_date_placements_with_epochs) return public_dataloader._chart_placement_dataloader(chart_date_placements_with_epochs) @app.route("/chart-rankings/aggregated-values-dataloaded/", methods=["POST"]) @use_kwargs(ChartRankingsAggregatedValuesDataloaded.Request, location="json") @marshal_with(ChartRankingsAggregatedValuesDataloaded.Response(many=True), apply=True) def get_chart_rankings_aggregated_values_dataloaded(**data): ids = data.pop('chart_rankings_aggregated') if False and get_ff_charts_cache(): # This cache is actively causing issues, so is permanently disabled # IN-15263, IN-15236 return aggregated_values_dataloader.aggregated_values_dataloader.load_many(ids) return aggregated_values_dataloader._get_chart_rankings_aggregated_values_dataloaded(ids) @app.route("/chart-rankings/aggregated-values-on-date-dataloaded/", methods=["POST"]) @use_kwargs(ChartRankingsAggregatedValuesOnDateDataloaded.Request, location="json") @marshal_with(ChartRankingsAggregatedValuesOnDateDataloaded.Response(many=True), apply=True) def get_chart_rankings_aggregated_values_on_date_dataloaded(**data): ids = data.pop('chart_rankings_aggregated_on_date') return charts.get_chart_rankings_aggregated_values_on_date_dataloaded(ids) @app.route("/charts/dates-for-chart-ids/", methods=["POST"]) @use_kwargs(ChartDatesForChartIds.Request, location="json") @marshal_with(ChartDatesForChartIds.Response(many=True), apply=True) def get_chart_dates_for_chart_ids(**data): ids = data.pop('chart_ids') return chart_dates_dataloader.load_many(ids) @app.route("/charts/metadata/", methods=["POST"]) @use_kwargs(MetadataForChartIds.Request, location="json") @marshal_with(MetadataForChartIds.Response(many=True), apply=True) def get_chart_metadata_for_chart_ids(**data): ids = data.pop('chart_ids') if get_ff_charts_cache(): return metadata_dataloader.chart_metadata_dataloader.load_many(ids) return metadata_dataloader._get_chart_metadata_for_chart_ids(ids) @app.route("/charts/prime", methods=["POST"]) @use_kwargs(CachePrime.Request, location="json") @marshal_with(CachePrime.Response, apply=True) def prime_charts_cache(chart_dates): return cache_prime.prime_charts_cache(chart_dates) @app.route("/charts/gsrs-for-charts-date/", methods=["POST"]) @use_kwargs(GlobalSoundRecordingsForChartsDate.Request, location="json") @marshal_with(GlobalSoundRecordingsForChartsDate.Response, apply=True) def get_global_sound_recordings_for_charts_date(**data): ids = data['chart_ids'] date = data['date'] filter = data['filter'] order_by = data['order_by'] order_dir = data['order_dir'] permissions = None if 'brand_uuid_in' in filter: permissions = get_request_permissions() return charts.get_global_sound_recordings_for_charts_date(ids, date, filter, order_by, order_dir, permissions) @app.route("/charts/chart-date-entry/", methods=["POST"]) @use_kwargs(ChartDateEntryForIsrcsDataloaded.Request, location="json") @marshal_with(ChartDateEntryForIsrcsDataloaded.Response(many=True), apply=True) def get_chart_date_entry_for_isrcs(**data): chart_entries = data.pop('chart_entries') data = chart_date_entry_dataloader.load_many(chart_entries) return [{"data": None} if chart_entry["data"] is None or chart_entry["data"]["position"] is None else chart_entry for chart_entry in data] @app.route("/charts/chart-date-entry-summary/", methods=["POST"]) @use_kwargs(ChartDateEntryForIsrcsSummaryDataloaded.Request, location="json") @marshal_with(ChartDateEntryForIsrcsSummaryDataloaded.Response(many=True), apply=True) def get_chart_date_entries_summary_for_isrcs(**data): chart_entries = data.pop('chart_entries') chart_ids = data.pop('chart_ids') data = chart_date_entry_summary_dataloader.load_many(chart_entries, chart_ids) return [{"data": None} if chart_entry["data"] is None or chart_entry["data"]["feature"] is None else chart_entry for chart_entry in data] def filter_charts_data(filter, item): # is_part_of_catalog is_part_of_catalog = filter.get('is_part_of_catalog') if is_part_of_catalog is True: if item['isPartOfCatalog'] is not True: return False elif is_part_of_catalog is False: if item['isPartOfCatalog'] is not False: return False # brands brand_uuids = filter.get('brand_uuid_in', []) if brand_uuids: items_brand_ids = item['brand_ids'] if item['brand_ids'] else ['👾'] if not (set(brand_uuids) & set(items_brand_ids)): return False return True @app.route("/charts/data-for-chart-ids/", methods=["POST"]) @use_kwargs(ChartDataForChartIds.Request, location="json") @marshal_with(ChartDataForChartIds.Response(many=True), apply=True) def get_chart_data_for_chart_ids(**data): return _get_chart_data_for_chart_ids(data) def _get_chart_data_for_chart_ids(data): ids = data['chart_ids_with_date'] filter = data.get('filter') order = data.get('order') chart_dates_with_epochs = get_chart_date_epochs(ids) show_sme_data = False if placements_dataloader.is_show_sme_data_required(filter=filter): show_sme_data = get_show_sme_data_flag() permissions = get_request_permissions() if get_ff_charts_cache(): results = placements_dataloader.chart_placements_data_for_chart_dates_dataloader.load_many( chart_dates_with_epochs, filter=filter, order=order, show_sme_data=show_sme_data, permissions=permissions, ) else: results = placements_dataloader._get_chart_placements_data_for_chart_dates( chart_dates_with_epochs, filter=filter, order=order, show_sme_data=show_sme_data, permissions=permissions, ) # filters by brands and isMyCatalog uuidsData = results[0]['data']['uuidsData'] uuidsData = [item for item in uuidsData if filter_charts_data(filter, item)] results[0]['data']['uuidsData'] = uuidsData print(len(results[0]['data']['uuidsData'])) return results @app.route("/chart-groups/chart-ids", methods=["POST"]) @use_kwargs(ChartGroupsChartIds.Request, location="json") @marshal_with(ChartGroupsChartIds.Response(many=True), apply=True) def post_chart_groups_chart_ids_dataloader(**data): ids = data.pop('slugs') return chart_groups_chart_ids_dataloader.load_many( ids, controlled_charts_ffs=get_controlled_chart_feature_flags(), )