"""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 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. """ import datetime from flask import g, jsonify, request from oto import response from oto.adaptors.flask import flaskify from owsrequest import context from sound_recordings import config from sound_recordings.api import app from sound_recordings.constants import owner_category from sound_recordings.constants.access import ACCESS_ANALYTICS from sound_recordings.constants.pagination import DEFAULT_LIMIT, DEFAULT_OFFSET from sound_recordings.constants.placements import DEFAULT_MIN_FOLLOWERS from sound_recordings.logic import ( data_availability, demographics, downloads, performance_by_country, placements, playlists, sound_recording_metadata, source_of_streams, streams, streams_breakdown, streams_by_product, streams_by_sos, streams_by_store, top_countries, top_markets, top_sound_recordings, ) from sound_recordings.validation import access @app.route(config.HEALTH_CHECK_PATH) 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." ) g.log.exception(error) return flaskify(response.create_fatal_response(message)) @app.route(config.STREAMS_BREAKDOWN_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_streams_breakdown(isrc): """Return sound-recording source of streams data for a given isrc. Path parameters: isrc (str): ISRC to return sound recording source of streams for Returns: flask.Response with sound-recording source of streams data. """ countries, store_ids, start_date, end_date, distributors = _get_global_filters() request_context = context.get_request_context_from_headers( request.headers, label_profile=True ) return flaskify( streams_breakdown.get_streams_breakdown( request_context, isrc, countries, store_ids, start_date, end_date, distributors, ) ) @app.route(config.PERFORMANCE_BY_COUNTRY_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_performance_by_country(isrc): """Return sound-recording performance by country for a given isrc. Path parameters: isrc (str): ISRC to return sound-recording performance by country for. Returns: flask.Response with sound-recording performance by country timeseries. """ countries, store_ids, start_date, end_date, distributors = _get_global_filters() request_context = context.get_request_context_from_headers( request.headers, label_profile=True ) return flaskify( performance_by_country.get_performance_by_country( request_context, isrc, distributors, countries, store_ids, start_date, end_date, ) ) @app.route(config.STREAMS_BY_PRODUCT_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_streams_by_product(isrc): """Return sound-recording streams by product timeseries for a given isrc. Path parameters: isrc (str): ISRC to return sound-recording streams by product for. Returns: flask.Response with sound-recording streams by product timeseries. """ countries, store_ids, start_date, end_date, distributors = _get_global_filters() request_context = context.get_request_context_from_headers( request.headers, label_profile=True ) return flaskify( streams_by_product.get_streams_by_product( request_context, isrc, distributors, countries, store_ids, start_date, end_date, ) ) @app.route(config.STREAMS_BY_SOS_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_streams_by_source_of_streams(isrc): """Return sound-recording streams by source of streams for a given isrc. Path parameters: isrc (str): ISRC to return sound-recording streams by SoS for. Returns: flask.Response with sound-recording streams by source of streams. """ countries, store_ids, start_date, end_date, distributors = _get_global_filters() request_context = context.get_request_context_from_headers( request.headers, label_profile=True ) return flaskify( streams_by_sos.get_streams_by_source_of_streams( request_context, isrc, distributors, countries, store_ids, start_date, end_date, ) ) @app.route(config.STREAMS_BY_STORE_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_streams_by_store(isrc): """Return sound-recording streams by store for a given isrc. Path parameters: isrc (str): ISRC to return sound-recording streams by store for. Returns: flask.Response with sound-recording streams by store timeseries. """ countries, store_ids, start_date, end_date, distributors = _get_global_filters() request_context = context.get_request_context_from_headers( request.headers, label_profile=True ) return flaskify( streams_by_store.get_streams_by_store( request_context, isrc, distributors, countries, store_ids, start_date, end_date, ) ) @app.route(config.STREAMS_ALL_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_streams_all(isrc): """Return sound-recording aggregate streams time series for a given isrc. Path parameters: isrc (str): ISRC to return sound-recording aggregate streams. Returns: flask.Response with sound-recording aggregate streams timeseries. """ countries, store_ids, start_date, end_date, distributors = _get_global_filters() request_context = context.get_request_context_from_headers( request.headers, label_profile=True ) return flaskify( streams.get_streams_all( request_context, isrc, distributors, countries, store_ids, start_date, end_date, ) ) def _get_global_filters(): """Return global filters from request args. Returns: start_date (datetime.date): Start date end_date (datetime.date): End date countries (list): List of countries distributors (list): List of distributors names """ start_date = None end_date = None countries = [] store_ids = [] distributors = request.args.get("distributors", "theorchard") distributors = distributors.split(",") if request.args.get("country_code"): countries = request.args.getlist("country_code") if request.args.get("store_ids"): store_ids = [int(store_id) for store_id in request.args.getlist("store_ids")] if request.args.get("start_date") and ( request.args.get("end_date") or (request.args.get("days")) ): if request.args.get("end_date"): start_date = datetime.datetime.strptime( request.args.get("start_date"), "%Y-%m-%d" ).date() end_date = datetime.datetime.strptime( request.args.get("end_date"), "%Y-%m-%d" ).date() else: start_date, end_date = data_availability.get_date_range( request.args.get("start_date"), int(request.args.get("days")) ) return countries, store_ids, start_date, end_date, distributors @app.route(config.TOP_SOUND_RECORDINGS_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_top_sound_recordings(): """Return top sound recordings. Returns: flask.Response with top sound recordings payload. """ limit = int(request.args.get("limit", 25)) offset = int(request.args.get("offset", 0)) order_by = request.args.get("order_by", "streams_7_days") countries = [] if request.args.get("country_code"): countries = request.args.getlist("country_code") distributors = request.args.get("distributors", "theorchard") distributors = distributors.split(",") request_context = context.get_request_context_from_headers( request.headers, label_profile=True ) return flaskify( top_sound_recordings.get_top_sound_recordings( request_context, distributors, limit, offset, countries, order_by ) ) @app.route(config.SOUND_RECORDING_METADATA_PATH) def get_sound_recording_metadata(isrc): """Return sound-recording metadata for a given isrc.""" # Set force_profile=True when all tracks materialized in Dynamo. include_deleted = request.args.get("include_deleted", "false") == "true" request_context = context.get_request_context_from_headers( request.headers, label_profile=True ) return flaskify( sound_recording_metadata.get_sound_recording_metadata( request_context, isrc, include_deleted ) ) @app.route(config.DOWNLOADS_BY_STORE_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_sound_recording_downloads_by_store(isrc): """Return sound-recording downloads by store for a given isrc. Path parameters: isrc (str): isrc to return sound recording downloads for Returns: flask.Response with sound recording downloads payload """ countries, store_ids, start_date, end_date, distributors = _get_global_filters() request_context = context.get_request_context_from_headers( request.headers, label_profile=True ) return flaskify( downloads.get_downloads_by_store( request_context, isrc, distributors, countries, store_ids, start_date, end_date, ) ) @app.route(config.DOWNLOADS_BY_PRODUCT_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_sound_recording_downloads_by_product(isrc): """Return sound-recording downloads by product for a given isrc. Path parameters: isrc (str): ISRC to return sound-recording downloads for Returns: flask.Response with sound-recording downloads by product timeseries. """ countries, store_ids, start_date, end_date, distributors = _get_global_filters() request_context = context.get_request_context_from_headers( request.headers, label_profile=True ) return flaskify( downloads.get_downloads_by_product( request_context, isrc, distributors, countries, store_ids, start_date, end_date, ) ) @app.route(config.AGGREGATE_STREAMS_PATH, methods=["POST"]) @access.verify_profile(access=ACCESS_ANALYTICS) def get_aggregate_streams(): """Return sound-recording aggregate streams for a list of isrcs. Body parameters: isrcs ([str]): ISRCs to return sound recording aggregate streams for Returns: flask.Response with sound-recording aggregate streams data. """ data = request.get_json() countries, store_ids, _, _, distributors = _get_global_filters() request_context = context.get_request_context_from_headers( request.headers, label_profile=True ) isrcs = data.get("isrcs", []) if len(isrcs) == 0: return flaskify(response.Response({})) return flaskify( streams.get_aggregate_streams( request_context, isrcs, distributors, countries, store_ids ) ) @app.route(config.STREAMS_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_streams(isrc): """Return sound-recording streams for a given isrc. Path parameters: isrc (str): ISRC to return sound recording streams for Returns: flask.Response with sound-recording streams data. """ countries, store_ids, start_date, end_date, distributors = _get_global_filters() request_context = context.get_request_context_from_headers( request.headers, label_profile=True ) result = streams.get_streams_bulk( request_context, [isrc], distributors, countries, store_ids, start_date, end_date, ) if not result: return flaskify(result) return flaskify(response.Response(result.message[isrc])) @app.route(config.STREAMS_BULK_PATH, methods=["POST"]) @access.verify_profile(access=ACCESS_ANALYTICS) def get_streams_bulk(): """Return sound-recording streams for a list of isrcs. Path parameters: isrcs ([str]): ISRCs to return sound recording streams for Returns: flask.Response with sound-recording streams data. """ data = request.get_json() countries, store_ids, start_date, end_date, distributors = _get_global_filters() request_context = context.get_request_context_from_headers( request.headers, label_profile=True ) isrcs = data.get("isrcs", []) if len(isrcs) == 0: return flaskify(response.Response({})) return flaskify( streams.get_streams_bulk( request_context, isrcs, distributors, countries, store_ids, start_date, end_date, ) ) @app.route(config.DOWNLOADS_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_downloads(isrc): """Return sound-recording streams for a given isrc. Path parameters: isrc (str): ISRC to return sound recording streams for Returns: flask.Response with sound-recording streams data. """ countries, store_ids, start_date, end_date, distributors = _get_global_filters() request_context = context.get_request_context_from_headers( request.headers, label_profile=True ) return flaskify( downloads.get_downloads( request_context, isrc, distributors, countries, store_ids, start_date, end_date, ) ) @app.route(config.DOWNLOADS_ALL_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_sound_recording_downloads_all(isrc): """Return sound-recording downloads all for a given isrc. Path parameters: isrc (str): isrc to return sound recording downloads for Returns: flask.Response with sound recording downloads all payload """ countries, store_ids, start_date, end_date, distributors = _get_global_filters() request_context = context.get_request_context_from_headers( request.headers, label_profile=True ) return flaskify( downloads.get_downloads_all( request_context, isrc, distributors, countries, store_ids, start_date, end_date, ) ) @app.route(config.DOWNLOADS_BY_COUNTRY_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_sound_recording_downloads_by_country(isrc): """Return sound-recording downloads by country for a given isrc. Path parameters: isrc (str): isrc to return sound recording downloads for Returns: flask.Response with sound recording downloads by country payload """ countries, store_ids, start_date, end_date, distributors = _get_global_filters() request_context = context.get_request_context_from_headers( request.headers, label_profile=True ) return flaskify( downloads.get_downloads_by_country( request_context, isrc, distributors, countries, store_ids, start_date, end_date, ) ) @app.route(config.SOURCE_OF_STREAMS_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_source_of_streams(isrc): """Return sound-recording source of streams data for a given isrc. Path parameters: isrc (str): ISRC to return sound recording source of streams for Returns: flask.Response with sound-recording source of streams data. """ countries, store_ids, start_date, end_date, distributors = _get_global_filters() request_context = context.get_request_context_from_headers( request.headers, label_profile=True ) return flaskify( source_of_streams.get_source_breakdown( request_context, isrc, countries, store_ids, start_date, end_date, distributors, ) ) @app.route(config.TOP_MARKETS_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_top_markets(isrc): """Return sound-recording top markets for a given isrc. Path parameters: isrc (str): ISRC to return sound recording top markets for Returns: flask.Response with sound-recording top markets. """ countries, store_ids, _, _, distributors = _get_global_filters() request_context = context.get_request_context_from_headers( request.headers, label_profile=True ) return flaskify( top_markets.get_top_markets( request_context, isrc, distributors, store_ids, countries ) ) @app.route(config.PLACEMENTS_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_placements(isrc): """Return sound-recording placements data for a given isrc. Path parameters: isrc (str): ISRC to return sound recording placements for Returns: flask.Response with sound-recording placements data. """ request_context = context.get_request_context_from_headers( request.headers, label_profile=True ) # TODO: Assign default parameters to limit and offset # once the changes in the client dependent on this endpoint # are done to handle pagination. Until then, return all the # placements for the isrc as expected. # Standard limits for pagination are - # limit = 25 # offset = 0 limit = request.args.get("limit", None) offset = request.args.get("offset", None) order_dir = request.args.get("order_dir", "DESC") order_by = request.args.get("order_by", "added") limit = int(limit) if limit else None offset = int(offset) if offset else None store_ids = request.args.getlist("store_id", lambda x: int(x)) or list( map(lambda source: source["id"], placements.SOURCES) ) owner_categories = request.args.getlist("owner_category") or owner_category.DEFAULTS min_followers = request.args.get("min_followers", DEFAULT_MIN_FOLLOWERS) return flaskify( placements.get_placements( request_context, limit, offset, order_dir, order_by, isrc, store_ids, owner_categories, min_followers, ) ) @app.route(config.PLACEMENT_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_placement(isrc, playlist_id): """Return a playlist placement for a given ISRC and playlist_id. Path parameters: isrc (str): The song's ISRC for this placement. playlist_id (str): The playlist the song is located. Returns: flask.Response with placement and playlist data. """ request_context = context.get_request_context_from_headers( request.headers, label_profile=True ) return flaskify(placements.get_placement(request_context, isrc, playlist_id)) @app.route(config.PLAYLISTS_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_playlists(isrc): """Return playlists with associated streams for a given isrc. Path parameters: isrc (str): ISRC to return sound recording streams for Returns: flask.Response with sound-recording streams data. """ countries, store_ids, start_date, end_date, distributors = _get_global_filters() offset = int(request.args.get("offset", DEFAULT_OFFSET)) limit = ( int(request.args.get("limit")) if request.args.get("limit") else DEFAULT_LIMIT ) request_context = context.get_request_context_from_headers( request.headers, label_profile=True ) return flaskify( playlists.get_playlists( request_context, isrc, distributors, countries, store_ids, start_date, end_date, offset, limit, ) ) @app.route(config.DEMOGRAPHICS_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_demographics(isrc): """Return demographics for given isrc. Path parameters: isrc (str): ISRC to return demographics for Returns: flask.Response with sound-recording demographics data. """ countries, store_id, start_date, end_date, distributors = _get_global_filters() request_context = context.get_request_context_from_headers( request.headers, label_profile=True ) try: result = demographics.get_demographics( request_context, isrc, countries, store_id, start_date, end_date, distributors, ) except Exception as e: g.log.exception(e) message = "internal error. Unable to complete request" return flaskify(response.create_fatal_response(message)) return flaskify(result) @app.route(config.TOP_COUNTRIES_STREAMS_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_top_countries_streams(isrc): """Return top coutries with associated streams for a given isrc. Path parameters: isrc (str): ISRC to return sound recording streams for Returns: flask.Response with sound-recording streams data. """ countries, store_ids, start_date, end_date, distributors = _get_global_filters() request_context = context.get_request_context_from_headers( request.headers, label_profile=True ) return flaskify( top_countries.get_top_countries_streams( request_context, isrc, distributors, store_ids, start_date, end_date ) ) @app.route(config.TOP_COUNTRIES_DOWNLOADS_PATH) @access.verify_profile(access=ACCESS_ANALYTICS) def get_top_countries_downloads(isrc): """Return top coutries with associated downloads for a given isrc. Path parameters: isrc (str): ISRC to return sound recording downloads for Returns: flask.Response with sound-recording downloads data. """ countries, store_ids, start_date, end_date, distributors = _get_global_filters() request_context = context.get_request_context_from_headers( request.headers, label_profile=True ) return flaskify( top_countries.get_top_countries_downloads( request_context, isrc, distributors, store_ids, start_date, end_date ) )