"""Application handlers for Participant endpoints. 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 define 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 flask import jsonify, request from oto.adaptors.flask import flaskify from analytics import config from analytics.api import app from analytics.constants.access import ACCESS_ANALYTICS from analytics.constants.account import ACCOUNT_TO_FIN_LABEL_MAPPING from analytics.constants.distributors import DISTRIBUTORS from analytics.features import ( is_insights_line_soundcloud_collection_as_active_enabled, is_insights_published_max_available_date_enabled, is_insights_transfer_product_ownership_enabled, ) from analytics.handler_utils import ( _get_global_filters, _parse_int_list, filter_non_empty_items, ) from analytics.logic import ( demographics, participant, participant_metrics, participant_track_streams, ) from analytics.logic.permissions import get_permission_values from analytics.logic.streams import get_aggregated_streams from analytics.validation import access @app.route(config.PARTICIPANT_METRICS_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_participant_metrics(): """Return participant streams metrics. Route: GET /participant-metrics Query params: country_code (list[str]): Country codes to filter by. store_ids (list[int]): Store IDs to filter by. start_date (str): Start date YYYY-MM-DD. end_date (str): End date YYYY-MM-DD. distributors (str): Comma-separated distributor names. limit (int): Max results (default 25). offset (int): Pagination offset (default 0). order_by (str): Sort field (default "streams_7_days"). order_dir (str): Sort direction (default "DESC"). parent_company (list[str]): Parent company names. company_brand (list[str]): Company brand names. service_tier (str): Service tier filter. global_participant_ids (list[str]): Participant IDs. label_ids (list[int]): Label IDs. subaccount_ids (list[int]): Subaccount IDs. fin_label_ids (list[str]): Financial label IDs. upper_profit_centers (list[str]): Upper profit center IDs. Snowflake tables: - METRICS_BY_TRACK_PARTICIPANT_FEED_DISTRIBUTOR_ROLLUP - V_METRICS_BY_TRACK_PARTICIPANT_COUNTRY_FEED_DISTRIBUTOR_ROLLUP JOINs: - DIM_RELEASE - MAPPINGS_FINANCIAL_LABEL_ID_TO_PRODUCT_IDS - VENDOR_COMPANY_BRAND_PARENT_COMPANY_SERVICE_TIER_VIEW - MAPPINGS_UPPER_PROFIT_CENTER_TO_PRODUCT_IDS - GLOBAL_PARTICIPANT_REPRESENTS_LABEL_PARTICIPANT Returns: JSON with participant metrics. """ parent_companies = filter_non_empty_items( request.args.getlist("parent_company") ) or filter_non_empty_items(request.args.getlist("parent_companies")) company_brands = filter_non_empty_items( request.args.getlist("company_brand") ) or filter_non_empty_items(request.args.getlist("company_brands")) countries, store_ids, _, _, distributors = _get_global_filters() query_params = { "distributors": distributors, "country_ids": countries, "store_ids": store_ids, "label_ids": _parse_int_list(request, "label_ids"), "subaccount_ids": _parse_int_list(request, "subaccount_ids"), "fin_label_ids": request.args.getlist("fin_label_ids"), "upper_profit_center_ids": request.args.getlist("upper_profit_centers"), "global_participant_ids": request.args.getlist("global_participant_ids"), "parent_companies": parent_companies, "company_brands": company_brands, "service_tier": request.args.get("service_tier") or None, "order_by": request.args.get("order_by", "streams_7_days"), "order_dir": request.args.get("order_dir", "DESC"), "limit": int(request.args.get("limit", 25)), "offset": int(request.args.get("offset", 0)), "transfer_product_ownership_enabled": is_insights_transfer_product_ownership_enabled(), } permissions = get_permission_values() return flaskify( participant_metrics.get_participant_metrics(query_params, permissions) ) @app.route(config.PARTICIPANT_TRACK_STREAMS_ALL_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_participant_track_streams_all(global_participant_id): """Return participant track streams timeseries. Route: GET /participant//track-streams-all Path params: global_participant_id (str): Global participant ID. Query params: country_code (list[str]): Country codes to filter by. store_ids (list[int]): Store IDs to filter by. start_date (str): Start date YYYY-MM-DD. end_date (str): End date YYYY-MM-DD. distributors (str): Comma-separated distributor names. Snowflake tables: - V_STREAMS_BY_PARTICIPANT_TRACK_FEED_DISTRIBUTOR_DAILY - V_STREAMS_BY_PARTICIPANT_TRACK_COUNTRY_FEED_DISTRIBUTOR_DAILY JOINs: - DIM_RELEASE Returns: JSON with track streams timeseries. """ countries, store_ids, start_date, end_date, distributors = _get_global_filters() query_params = { "global_participant_id": global_participant_id, "country_ids": countries, "store_ids": store_ids, "start_date": start_date, "end_date": end_date, "distributors": distributors, "transfer_product_ownership_enabled": is_insights_transfer_product_ownership_enabled(), } permissions = get_permission_values() return flaskify( participant_track_streams.get_track_streams_all(query_params, permissions) ) @app.route(config.PARTICIPANT_TRACK_STREAMS_STORE_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_participant_track_streams_store(global_participant_id): """Return participant track streams by store timeseries. Route: GET /participant//track-streams-by-store Path params: global_participant_id (str): Global participant ID. Query params: country_code (list[str]): Country codes to filter by. store_ids (list[int]): Store IDs to filter by. start_date (str): Start date YYYY-MM-DD. end_date (str): End date YYYY-MM-DD. distributors (str): Comma-separated distributor names. Snowflake tables: - V_STREAMS_BY_PARTICIPANT_TRACK_FEED_DISTRIBUTOR_DAILY - V_STREAMS_BY_PARTICIPANT_TRACK_COUNTRY_FEED_DISTRIBUTOR_DAILY JOINs: - DIM_RELEASE Returns: JSON with track streams by store timeseries. """ countries, store_ids, start_date, end_date, distributors = _get_global_filters() query_params = { "global_participant_id": global_participant_id, "country_ids": countries, "store_ids": store_ids, "start_date": start_date, "end_date": end_date, "distributors": distributors, "transfer_product_ownership_enabled": is_insights_transfer_product_ownership_enabled(), } permissions = get_permission_values() return flaskify( participant_track_streams.get_track_streams_store(query_params, permissions) ) @app.route(config.PARTICIPANT_SUMMARY_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_participant_summary(global_participant_id): """Return participant summary. Route: GET /participant//summary Path params: global_participant_id (str): Global participant ID. Query params: type (str): Summary type (default "total"). Values: TOTAL, SOUND_RECORDING, COUNTRY, SOS, SOS_DETAILED, STORE, PRODUCT. start_date (str): Start date YYYY-MM-DD. end_date (str): End date YYYY-MM-DD. distributors (str): Comma-separated distributor names. countries (list[str]): Country codes to filter by. stores (list[str]): Store IDs to filter by. stream_sources (list[str]): SOS stream-source names to filter by (e.g. "discovery", "albumpage"). Only used with SOS_DETAILED. Values are lowercased. order_by (str): Sort field (default "streams"). order_dir (str): Sort direction (default "desc"). limit (int): Max results (default 100). offset (int): Pagination offset (default 0). Snowflake tables: - V_STREAMS_BY_PARTICIPANT_TRACK_FEED_DISTRIBUTOR_DAILY - V_STREAMS_BY_PARTICIPANT_TRACK_COUNTRY_FEED_DISTRIBUTOR_DAILY - DOWNLOADS_BY_PARTICIPANT_PRODUCT_FEED_DISTRIBUTOR_DAILY - DOWNLOADS_BY_PARTICIPANT_PRODUCT_COUNTRY_FEED_DISTRIBUTOR_DAILY - DOWNLOADS_BY_PARTICIPANT_TRACK_FEED_DISTRIBUTOR_DAILY - DOWNLOADS_BY_PARTICIPANT_TRACK_COUNTRY_FEED_DISTRIBUTOR_DAILY - METRICS_BY_PARTICIPANT_TRACK_28_DAYS_ROLLUP JOINs: - DIM_RELEASE Returns: JSON with participant summary data. """ start_date = request.args.get("start_date") end_date = request.args.get("end_date") query_params = { "global_participant_id": global_participant_id, "query_type": request.args.get("type", "total"), "start_date": start_date, "end_date": end_date, "distributors": request.args.get("distributors", DISTRIBUTORS).split(","), "country_ids": request.args.getlist("countries"), "store_ids": [int(s) for s in request.args.getlist("stores") if s], "stream_sources": [s.lower() for s in request.args.getlist("stream_sources")], "order_by": request.args.get("order_by", "streams"), "order_dir": request.args.get("order_dir", "desc"), "limit": request.args.get("limit", 100, type=int), "offset": request.args.get("offset", 0, type=int), "transfer_product_ownership_enabled": is_insights_transfer_product_ownership_enabled(), "line_soundcloud_collection_as_active_enabled": is_insights_line_soundcloud_collection_as_active_enabled(), } permissions = get_permission_values() return flaskify(participant.get_summary(query_params, permissions)) @app.route(config.PARTICIPANT_TIMESERIES_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_participant_timeseries(global_participant_id): """Return participant timeseries. Route: GET /participant//timeseries Path params: global_participant_id (str): Global participant ID. Query params: type (str): Timeseries type (default "total"). Values: TRACK_STREAMS, TRACK_STREAMS_BY_COUNTRY, TRACK_STREAMS_BY_SOS, TRACK_STREAMS_BY_SOS_DETAILED, TRACK_STREAMS_BY_SOUND_RECORDING, TRACK_STREAMS_BY_STORE, TRACK_STREAMS_BY_PRODUCT, TRACK_DOWNLOADS, TRACK_DOWNLOADS_BY_COUNTRY, TRACK_DOWNLOADS_BY_SOUND_RECORDING, TRACK_DOWNLOADS_BY_STORE, TRACK_DOWNLOADS_BY_PRODUCT, ALBUM_DOWNLOADS, ALBUM_DOWNLOADS_BY_COUNTRY, ALBUM_DOWNLOADS_BY_PRODUCT, ALBUM_DOWNLOADS_BY_STORE. start_date (str): Start date YYYY-MM-DD. end_date (str): End date YYYY-MM-DD. days_back (str): Number of days to look back. distributors (str): Comma-separated distributor names. countries (list[str]): Country codes to filter by. stores (list[str]): Store IDs to filter by. ids (list[str]): IDs to filter aggregation by. resolution (str): Time resolution (mid, low, ultralow). stream_sources (list[str]): SOS stream-source names to filter by (e.g. "discovery", "albumpage"). Only used with TRACK_STREAMS_BY_SOS_DETAILED. Values are lowercased. Snowflake tables: - V_STREAMS_BY_PARTICIPANT_TRACK_FEED_DISTRIBUTOR_DAILY - V_STREAMS_BY_PARTICIPANT_TRACK_COUNTRY_FEED_DISTRIBUTOR_DAILY - DOWNLOADS_BY_PARTICIPANT_TRACK_FEED_DISTRIBUTOR_DAILY - DOWNLOADS_BY_PARTICIPANT_TRACK_COUNTRY_FEED_DISTRIBUTOR_DAILY - DOWNLOADS_BY_PARTICIPANT_PRODUCT_FEED_DISTRIBUTOR_DAILY - DOWNLOADS_BY_PARTICIPANT_PRODUCT_COUNTRY_FEED_DISTRIBUTOR_DAILY JOINs: - DIM_RELEASE Returns: JSON with participant timeseries items. """ days_back_arg = request.args.get("days_back") query_params = { "global_participant_id": global_participant_id, "query_type": request.args.get("type", "total"), "ids": request.args.getlist("ids"), "start_date": request.args.get("start_date"), "end_date": request.args.get("end_date"), "days_back": int(days_back_arg) if days_back_arg else None, "distributors": request.args.get("distributors", DISTRIBUTORS).split(","), "country_ids": request.args.getlist("countries"), "store_ids": [int(s) for s in request.args.getlist("stores") if s], "stream_sources": [s.lower() for s in request.args.getlist("stream_sources")], "resolution": request.args.get("resolution"), "transfer_product_ownership_enabled": is_insights_transfer_product_ownership_enabled(), "line_soundcloud_collection_as_active_enabled": is_insights_line_soundcloud_collection_as_active_enabled(), } permissions = get_permission_values() return flaskify(participant.get_timeseries(query_params, permissions)) @app.route(config.PARTICIPANT_DEMOGRAPHICS_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_participant_demographics(global_participant_id): """Return demographics for a given participant. Route: GET /participant//demographics Path params: global_participant_id (str): Global participant ID. Query params: country_code (list[str]): Country codes to filter by. store_ids (list[int]): Store IDs to filter by. start_date (str): Start date YYYY-MM-DD. end_date (str): End date YYYY-MM-DD. distributors (str): Comma-separated distributor names. Snowflake tables: - V_STREAMS_DEMOGRAPHICS_BY_PARTICIPANT_FEED_DISTRIBUTOR_DAILY - V_STREAMS_DEMOGRAPHICS_BY_PARTICIPANT_COUNTRY_FEED_DISTRIBUTOR_DAILY JOINs: - DIM_RELEASE Returns: JSON with demographics breakdown. """ countries, store_ids, start_date, end_date, distributors = _get_global_filters() query_params = { "global_participant_id": global_participant_id, "query_type": "global_participant_id", "countries": countries, "store_ids": store_ids, "start_date": start_date, "end_date": end_date, "distributors": distributors, "transfer_product_ownership_enabled": is_insights_transfer_product_ownership_enabled(), } permissions = get_permission_values() return jsonify(demographics.get_demographics(query_params, permissions)), 200 @app.route(config.PARTICIPANT_AGGREGATED_STREAMS_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_participant_aggregated_streams(global_participant_id): """Return aggregated streams for a participant. Route: GET /participant//aggregated-streams Path params: global_participant_id (str): Global participant ID. Query params: days_back (int): Days to look back (default 28, max 28). top_size (int): Number of top items (default 5). dimension (str): Aggregation dimension (required). Values: SOS, STORE, COUNTRY. order_by (str): Sort field (default "streams7Days"). countries (list[str]): Country codes to filter by. store_ids (list[int]): Store IDs to filter by. Snowflake tables: dimension=COUNTRY or STORE: - METRICS_BY_PARTICIPANT_PRODUCT_COUNTRY_FEED_DISTRIBUTOR_ROLLUP - STREAMS_BY_PARTICIPANT_TRACK_FEED_DISTRIBUTOR_DAILY_RECENT - STREAMS_BY_PARTICIPANT_TRACK_COUNTRY_FEED_DISTRIBUTOR_DAILY_RECENT - DATA_AVAILABILITY_BY_STORE_DISTRIBUTOR_DAILY dimension=SOS: - V_STREAMS_BY_PARTICIPANT_TRACK_FEED_DISTRIBUTOR_DAILY - V_STREAMS_BY_PARTICIPANT_TRACK_COUNTRY_FEED_DISTRIBUTOR_DAILY - DATA_AVAILABILITY_BY_STORE_DISTRIBUTOR_DAILY JOINs: - DATA_AVAILABILITY_GET_MAX_AVAILABLE_STREAMING_STORES_DATE (_PUBLISHED twin when insights_published_max_available_date is on) Returns: JSON with aggregated streams by dimension. """ path_params = {"global_participant_id": global_participant_id} if int(request.args.get("days_back", 28)) > 28: raise Exception( "days_back for aggregated streams cannot be greater than 28 days" ) query_params = { "days_back": abs(int(str(request.args.get("days_back", 28)))), "top_size": request.args.get("top_size", 5), "dimension": request.args.get("dimension").upper(), "order_by": request.args.get("order_by", "streams7Days"), "countries": request.args.getlist("countries"), "store_ids": list(map(int, request.args.getlist("store_ids"))), } query_params.update(path_params) query_params[ "transfer_product_ownership_enabled" ] = is_insights_transfer_product_ownership_enabled() query_params[ "line_soundcloud_collection_as_active_enabled" ] = is_insights_line_soundcloud_collection_as_active_enabled() query_params[ "published_max_available_date_enabled" ] = is_insights_published_max_available_date_enabled() permissions = get_permission_values() aggregated_streams = get_aggregated_streams(query_params, permissions) response_body = {**aggregated_streams} return jsonify(response_body), 200