"""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: artist.response for more details. """ from flask import g from flask import request from owsrequest import flask_request from artist import config from artist import response from artist.api import app from artist.constants import errors from artist.constants import headers from artist.encoders import artist as artist_encoder from artist.logic import artists from artist.logic import apple_artists from artist.logic import spotify_artists from artist.logic import hello from artist.logic.permissions import ( get_and_verify_vendor_id_from_grass_headers, get_and_verify_vendor_id_from_profile_headers ) from artist.models import account from artist.utils import handler_util from artist.validation import json_schema @app.route('/hello', methods=['GET']) def legacy_health(): """Check the health of the application.""" return response.flaskify(hello.health_check()) @app.route(config.HEALTH_CHECK, methods=['GET']) def health(): """Check the health of the application.""" return response.flaskify(hello.health_check()) @app.route('/artists', methods=['GET']) @json_schema.wrap_request_validation( request, '/artists', 'get', json_schema.validate_request_args) @json_schema.wrap_request_validation( request, '/artists', 'get', json_schema.validate_request_headers) def get_artists(): """Get a paginated list of artists.""" account_type = request.headers.get(headers.GRASS_ACCOUNT_TYPE) account_id = request.headers.get(headers.GRASS_ACCOUNT_ID) page_offset = request.args.get('page_offset') page_limit = request.args.get('page_limit') artist_type = request.args.get('artist_type') return response.flaskify( response=artists.fetch_artists( account_type, account_id, page_offset, page_limit, artist_type), encoder=artist_encoder.ArtistJSONEncoder) @app.route('///artists', methods=['GET']) def fetch_artists(account_type, account_id): """Get a paginated list of artists. Args: account_type (str): The account type vendor or subaccount account_id (int): The account id Returns: flask.Response: with the paginated artists """ if ((account_type not in headers.GRASS_ACCOUNT_TYPES) or not account_id): return response.flaskify(response.create_error_response( code=errors.VALIDATION_ERROR, message=errors.ERROR_MESSAGE_BAD_REQUEST)) expected_grass_account_type = request.headers.get( headers.GRASS_ACCOUNT_TYPE, account_type) expected_grass_account_id = request.headers.get( headers.GRASS_ACCOUNT_ID, account_id) if (expected_grass_account_type == 'vendor' and account_type == 'subaccount'): expected_grass_account_id = ( account.get_subaccount(account_id).message['vendor_id']) access_validation = flask_request.verify_grass_access( request, **{expected_grass_account_type: expected_grass_account_id}) if not access_validation: return response.flaskify(access_validation) account_type = account_type account_id = account_id page_offset = request.args.get('page_offset') page_limit = request.args.get('page_limit') artist_type = request.args.get('artist_type') return response.flaskify( response=artists.fetch_artists( account_type, account_id, page_offset, page_limit, artist_type), encoder=artist_encoder.ArtistJSONEncoder) @app.route('///full-artists', methods=['GET']) def fetch_full_artists(account_type, account_id): """Get a paginated list of artists with additional metadata. Args: account_type (str): The account type vendor or subaccount account_id (int): The account id Returns: flask.Response: with the paginated artists """ if ((account_type not in headers.GRASS_ACCOUNT_TYPES) or not account_id): return response.flaskify(response.create_error_response( code=errors.VALIDATION_ERROR, message=errors.ERROR_MESSAGE_BAD_REQUEST)) access_validation = flask_request.verify_grass_access( request, **{account_type: account_id}) if not access_validation: return response.flaskify(access_validation) account_type = account_type account_id = account_id page_offset = request.args.get('page_offset') page_limit = request.args.get('page_limit') updated_since = request.args.get('updated_since') return response.flaskify( response=artists.fetch_full_artists( account_type, account_id, page_offset, page_limit, updated_since)) @app.route( '///full-artists/bulk', methods=['POST']) def fetch_full_artists_bulk(account_type, account_id): """Get a list of artists with additional metadata. Args: account_type (str): The account type vendor or subaccount account_id (int): The account id Returns: flask.Response: with the artists """ if ((account_type not in headers.GRASS_ACCOUNT_TYPES) or not account_id): return response.flaskify(response.create_error_response( code=errors.VALIDATION_ERROR, message=errors.ERROR_MESSAGE_BAD_REQUEST)) access_validation = flask_request.verify_grass_access( request, **{account_type: account_id}) if not access_validation: return response.flaskify(access_validation) account_type = account_type account_id = account_id data = request.get_json(silent=True, force=True) or {} artist_ids = data.get('artist_ids') if not artist_ids: return response.flaskify(response.create_error_response( code=errors.VALIDATION_ERROR, message=errors.ERROR_MESSAGE_BAD_REQUEST, status=400)) return response.flaskify( response=artists.fetch_full_artists_bulk( account_type, account_id, artist_ids)) @app.route( '///artist/', methods=['GET']) def fetch_full_artist_by_id(account_type, account_id, artist_id): """Get an artist with additional metadata. Args: account_type (str): The account type vendor or subaccount account_id (int): The account id artist_id (int): The artist id Returns: flask.Response: with the paginated artists """ if ((account_type not in headers.GRASS_ACCOUNT_TYPES) or not account_id): return response.flaskify(response.create_error_response( code=errors.VALIDATION_ERROR, message=errors.ERROR_MESSAGE_BAD_REQUEST)) access_validation = flask_request.verify_grass_access( request, **{account_type: account_id}) if not access_validation: return response.flaskify(access_validation) return response.flaskify( response=artists.fetch_full_artist_by_id(artist_id)) @app.route('/artist', methods=['POST']) @json_schema.wrap_request_validation( request, '/artist', 'post', json_schema.validate_request_headers) @json_schema.wrap_request_validation( request, '/artist', 'post', json_schema.validate_request_body) def create_artist(): """Create a new artist. Returns: flask.Response: On successful creation of the artist a 201 with a JSON representation of the newly created artist. """ artist_data = request.get_json() # Try to read the vendor id from the grass headers. If those cannot # be found, fall back to the profile headers. vendor_id = get_and_verify_vendor_id_from_grass_headers(request) or \ get_and_verify_vendor_id_from_profile_headers(g.request_context) if not vendor_id: return response.flaskify( response.create_error_response( code=errors.OWNERSHIP_ERROR, message=errors.VENDOR_DENIED_MESSAGE, status=403 ) ) g.ows.log.info( f"Vendor ID received for create-artist '{vendor_id}'" ) return response.flaskify( artists.create_artist(artist_data=artist_data, vendor_id=vendor_id) ) @app.route('/artist/', methods=['GET']) @json_schema.wrap_request_validation( request, '/artist/{artist_id}', 'get', json_schema.validate_request_headers) def get_artist_by_id(artist_id): """Retrieve an artist record for a given id. Returns: flask.Response: JSON representation of a single artist record. """ account_type = request.headers.get(headers.GRASS_ACCOUNT_TYPE) account_id = request.headers.get(headers.GRASS_ACCOUNT_ID) if account_id: account_id = int(account_id) access_response = flask_request.verify_grass_access( request, required=False, vendor=account_id, subaccount=account_id) if not access_response: return response.flaskify(access_response) artist_response = artists.fetch_artist_by_id( artist_id, account_id, account_type) return response.flaskify(artist_response) @app.route('/artist//document', methods=['GET']) def get_artist_doc_by_id(artist_id): """Retrieve an artist record for a given artist_id. Args: artist_id (int): Unique identifier of artist_info Returns: flask.Response: JSON representation of a single artist record. """ account_type = request.headers.get(headers.GRASS_ACCOUNT_TYPE) account_id = request.headers.get(headers.GRASS_ACCOUNT_ID) access_response = flask_request.verify_grass_access( request, required=False, vendor=account_id, subaccount=account_id) if not access_response: return response.flaskify(access_response) artist_response = artists.get_artist_document( artist_id, account_id, account_type) return response.flaskify(artist_response) @app.route('/artists/filter', methods=['GET']) def filter_artist(): """Retrieve a list of artist objects by name for a given vendor id. Returns: flask.Response: JSON representation of a list of artist objects. """ account_id = request.headers.get(headers.GRASS_ACCOUNT_ID) if account_id: account_id = int(account_id) access_response = flask_request.verify_grass_access( request, required=False, vendor=account_id, subaccount=account_id) if not access_response: return response.flaskify(access_response) if not request.args: return response.flaskify(response.Response({'items': []})) filters = request.args artist_response = artists.filter_artists(**filters) return response.flaskify(artist_response) @app.route('/spotify/search', methods=['GET']) def search_spotify_artist(): """Search for artists via the Spotify API. Returns: flask.Response: JSON representation of a list of Spotify objects. """ if not request.args: return response.flaskify(response.Response({'items': []})) query = request.args.get('q') search_response = spotify_artists.search(query=query) return response.flaskify(search_response) @app.route('/apple/search', methods=['GET']) def search_apple_artist(): """Search for artists via the Apple Music API. Returns: flask.Response: JSON representation of a list of Apple Music objects. """ account_id = request.headers.get(headers.GRASS_ACCOUNT_ID) if account_id: account_id = int(account_id) if not request.args: return response.flaskify(response.Response({'items': []})) query = request.args.get('q') search_response = apple_artists.search(query=query, account_id=account_id) return response.flaskify(search_response) @app.route('/products//artists', methods=['GET']) @json_schema.wrap_request_validation( request, '/products/{product_id}/artists', 'get', json_schema.validate_request_headers) def get_artist_identifiers(product_id): """Retrieve all artist identifiers associated with a product. Returns: flask.Response: JSON representation of artist identifiers for each artist associated with a product. """ account_id = request.headers.get(headers.GRASS_ACCOUNT_ID) if account_id: account_id = int(account_id) access_response = flask_request.verify_grass_access( request, required=False, vendor=account_id, subaccount=account_id ) if not access_response: return response.flaskify(access_response) artist_response = artists.fetch_artist_identifiers(product_id) return response.flaskify(artist_response) @app.route('/artist/', methods=['PUT']) def update_artist(artist_id): """Update an artist details by artist_id. Returns: flask.Response: On successful updation of the artist return a 200 with a JSON representation of the updated artist. """ artist_data = request.get_json() headers_response = flask_request.verify_grass_headers(request) if not headers_response: return response.flaskify(headers_response) ownership_response = flask_request.verify_grass_ownership( request, handler_util.check_artist_ownership, artist_id=artist_id) if not ownership_response: return response.flaskify(ownership_response) update_response = artists.update_artist( artist_id, artist_data) return response.flaskify(update_response) @app.route('/artist/bulk-ensure', methods=['POST']) def bulk_ensure_artists(): """Ensure source artists exist under the destination vendor; create if missing.""" headers_response = flask_request.verify_grass_headers(request) if not headers_response: return response.flaskify(headers_response) data = request.get_json(silent=True, force=True) or {} source_artist_ids = data.get('source_artist_ids') destination_vendor_id = data.get('destination_vendor_id') if not source_artist_ids or not isinstance(source_artist_ids, list): return response.flaskify(response.create_error_response( code=errors.VALIDATION_ERROR, message=errors.ERROR_MESSAGE_BAD_REQUEST, status=400)) if not destination_vendor_id or not isinstance(destination_vendor_id, int): return response.flaskify(response.create_error_response( code=errors.VALIDATION_ERROR, message=errors.ERROR_MESSAGE_BAD_REQUEST, status=400)) return response.flaskify( artists.bulk_ensure_artists( source_artist_ids=source_artist_ids, destination_vendor_id=destination_vendor_id)) @app.errorhandler(500) def exception_handler(error): """Default handler when uncaught exception is raised. 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.ows.log.exception(error) return response.flaskify(response.create_fatal_response(message))