"""Artist Model. Model representing artist metadata. """ import datetime from ddtrace import tracer from flask import g from sqlalchemy import func from sqlalchemy import Column from sqlalchemy import Enum from sqlalchemy import Integer from sqlalchemy import String from artist import response from artist.connectors import mysql from artist.constants import errors from artist.models import account from artist.models.sql.artist import COUNT_GET_FULL_ARTISTS from artist.models.sql.artist import GET_ARTIST_GENRES from artist.models.sql.artist import GET_FULL_ARTISTS from artist.models.sql.artist import GET_FULL_ARTISTS_BULK from artist.models.sql.artist import GET_ARTIST_DOCUMENT_FIELDS ARTIST_TYPES = ['artist', 'film_collection', 'tv_artist'] DEFAULT_COUNTRY_ID = 1 DEFAULT_PAGE_LIMIT = 50 DEFAULT_ARTIST_TYPE = 'artist' class Artist(mysql.BaseModel): """Artist Model. Represents Artist metadata. """ __tablename__ = 'artist_info' artist_id = Column( Integer, primary_key=True, autoincrement=True, nullable=False) artist_type = Column(Enum(*ARTIST_TYPES)) country_id = Column('orchard_country', Integer) isni_id = Column(String) name = Column(String) vendor_id = Column(Integer) unique_artist_id = Column(Integer) def to_dict(self): """Return dictionary of artist data. Returns: dict: artist metadata. """ return { 'id': self.artist_id, 'artist_type': self.artist_type, 'country_id': self.country_id, 'name': self.name } def list_by_vendor_id( vendor_id, page_offset=None, page_limit=None, artist_type=None): """Fetch a paginated list of artists with the given vendor_id. Args: vendor_id (int): vendor id. page_offset (int): offset to use for pagination. page_limit (int): max number of items per page. artist_type (str): whether the artist is for music, film or TV Returns: response.Response: object containing paginated result set. """ page_offset = int(page_offset or 0) page_limit = int(page_limit or DEFAULT_PAGE_LIMIT) with mysql.db_session() as session: full_query = session.query(Artist).filter_by(vendor_id=vendor_id) if artist_type is not None: full_query = full_query.filter_by(artist_type=artist_type) total_records = full_query.count() # Sort results by artist name in ascending order. This can be # overridden in the future, if needed. ordered_query = full_query.order_by(Artist.name, Artist.artist_id) paginated_query = ordered_query.offset(page_offset).limit(page_limit) artists = paginated_query.all() # Detach the objects from the session so their properties can be # accessed outside of the scope of this session. for artist in artists: session.expunge(artist) result_data = { 'items': artists, 'pagination': { 'offset': page_offset, 'limit': page_limit, 'total_records': total_records}} return response.Response(message=result_data) @tracer.wrap() def upsert_artist(*, artist_type, name, vendor_id, unique_artist_id=None): """Persist a new artist to the database, or find it if it already exists. Args: artist_type (str): type of artist. name (str): name of the artist. vendor_id (int): vendor id of the artist. unique_artist_id (int): primary key of a record in unique_artist table. Returns: response.Response: object containing dictionary of artist data """ span = tracer.current_span() span.set_tag("vendor_id", vendor_id) span.set_tag("name", name) artist_type = artist_type or DEFAULT_ARTIST_TYPE new_artist = Artist( artist_type=artist_type, country_id=DEFAULT_COUNTRY_ID, name=name, vendor_id=vendor_id, unique_artist_id=unique_artist_id, ) result = None was_created = False with mysql.db_session() as session: # We want to emulate an upsert here (INSERT ... ON DUPLICATE KEY UPDATE), but we don't have a unique constraint. # so we execute a SELECT ... FOR UPDATE to produce gap locks that should prevent another process jumping in and # executing an INSERT after this SELECT but before our INSERT. old_artist = session \ .query(Artist) \ .filter(Artist.name == name, Artist.vendor_id == vendor_id) \ .order_by(Artist.artist_id.asc()) \ .with_for_update() \ .first() if old_artist: result = old_artist.to_dict() else: was_created = True session.add(new_artist) # Flush the session so the artist object will have the artist_id value session.flush() result = new_artist.to_dict() # This logging is outside the with above to reduce the time we hold the gap lock on the table. if not was_created: error_message = f'Create called for existing artist_info ({vendor_id}, "{name}"), returning row {result["id"]}.' g.ows.log.warning(error_message) span.error = 1 span.set_tag("error", error_message) return response.Response(message=result, status=201) def fetch_artist_by_id( artist_id, vendor_id=None ): """Retrieve a single artist from the database by id. Args: artist_id (int): id of an artist to retrieve. vendor_id (int): vendor id of the artist Returns: response.Response: object containing the artist information """ with mysql.db_session() as session: artist = (session.query( Artist.artist_id, Artist.artist_type, Artist.name, Artist.isni_id, Artist.vendor_id) .filter(Artist.artist_id == artist_id) .all()) if not artist: not_found_message = 'No artist exists for ID {}'.format(artist_id) return response.create_not_found_response( message=not_found_message) return _map_results_to_dict(artist) def _map_results_to_dict(results): """Map results from `artist` query to a dictionary.""" return [{ 'id': artist_id, 'artist_type': artist_type, 'name': name, 'isni_id': isni_id, 'vendor_id': vendor_id, } for artist_id, artist_type, name, isni_id, vendor_id in results] def filter_artists(**kwargs): """Retrieve a list of artists from the database by name and vendor ID. Returns: response.Response: list object containing the artist objects """ with mysql.db_session() as session: filters = { key: value for key, value in kwargs.items() # Limit the fields that be filtered on. if key in ['artist_name', 'vendor_id', 'unique_artist_id'] } if 'artist_name' in filters: # Make the artist name search case sensitive. filters['name'] = func.binary(filters['artist_name']) del filters['artist_name'] artists = session.query(Artist)\ .filter_by(**filters)\ .order_by(Artist.artist_id) artists = [artist.to_dict() for artist in artists] payload = {'items': artists} return response.Response(message=payload, status=200) def check_ownership(artist_id, account_type, account_id): """Check if artist is owned by the given account. Args: artist_id (int): the id of the artist to check account_type (str): type of account (vendor or subaccount) account_id (int): id of the account to check Returns: response.Response: status code 200 if owner, 403 if not owner """ if account_type not in ['vendor', 'subaccount']: return response.create_error_response( code=errors.ERROR_CODE_BAD_GRASS_REQUEST, message='Invalid account type', status=400) if account_type == 'subaccount' and account_id: # Look up the vendor id for this subaccount account_response = account.get_vendor_id_for_subaccount_id( account_id) if not account_response: if account_response.status == 404: return response.create_error_response( code=errors.VALIDATION_ERROR, message=errors.ERROR_SUBACCOUNT_NOT_FOUND_MESSAGE, status=400) return response.create_fatal_response( message=errors.ERROR_SUBACCOUNT_LOOKUP_FAILED_MESSAGE) account_id = account_response.message with mysql.db_session() as session: artist = session.query(Artist).get(artist_id) if not artist: not_found_message = 'No artist exists for ID {}'.format(artist_id) return response.create_not_found_response( message=not_found_message) if account_id and \ int(account_id) != artist.vendor_id: error_message = \ "The vendor ID provided does not match the artist's vendor ID." return response.create_error_response( code=errors.OWNERSHIP_ERROR, status=400, message=error_message) return response.Response('sucess') def update_artist(artist_id, artist_data): """Update artist data. Args: artist_id (int): artist_id. artist_data (dict): request data to an update artist details. Returns: response.Response: object containing dictionary of artist data """ with mysql.db_session() as session: artist = session.query(Artist).get(artist_id) if not artist: not_found_message = 'No artist exists for ID {}'.format(artist_id) return response.create_not_found_response( message=not_found_message) if 'name' in artist_data: artist.name = artist_data.get('name') if 'country_id' in artist_data: artist.country_id = artist_data.get('country_id') if 'isni_id' in artist_data: artist.isni_id = artist_data.get('isni_id') if 'unique_artist_id' in artist_data: artist.unique_artist_id = artist_data.get('unique_artist_id') session.commit() return response.Response(message=artist.to_dict()) def get_artist_genres(artist_id): """Get a list of genres associated with an artist. Args: artist_id (int): the artist id. Returns: response.Response: containing the list of genres. """ with mysql.db_session() as session: query = GET_ARTIST_GENRES.format(artist_id=artist_id) rows = session.execute(query).fetchall() genres = [row[0] for row in rows] return response.Response({'items': genres}) def fetch_full_artists( vendor_id, page_offset=None, page_limit=None, updated_since=None): """Fetch a paginated list of artists with additional metadata. Args: vendor_id (int): vendor id. page_offset (int): offset to use for pagination. page_limit (int): max number of items per page. updated_since (int): timestamp to restrict query Returns: Response: containing the paginated list of artists. """ page_offset = int(page_offset or 0) page_limit = int(page_limit or DEFAULT_PAGE_LIMIT) updated_since = int(updated_since or 0) updated_since = datetime.datetime.utcfromtimestamp( updated_since).strftime('%Y-%m-%d %H:%M:%S') with mysql.db_session() as session: count_query = COUNT_GET_FULL_ARTISTS.format( vendor_id=vendor_id, updated_since=updated_since) query = GET_FULL_ARTISTS.format( vendor_id=vendor_id, updated_since=updated_since, page_limit=page_limit, page_offset=page_offset) count_rows = session.execute(count_query).fetchall() rows = session.execute(query).fetchall() artists = [] for row in rows: artists.append({ 'artist_id': row[0], 'name': row[1], 'genres': row[2].split(',') if row[2] else [] }) return response.Response({ 'items': artists, 'pagination': { 'offset': page_offset, 'limit': page_limit, 'total_records': count_rows[0][0] } }) def fetch_full_artists_bulk(vendor_id, artist_ids): """Fetch a list of artists with additional metadata. Args: vendor_id (int): vendor id. artist_ids ([int]): the list of artist ids to fetch. Returns: Response: containing the list of artists. """ with mysql.db_session() as session: query = GET_FULL_ARTISTS_BULK.format( vendor_id=vendor_id, artist_ids=','.join(str(id) for id in artist_ids)) rows = session.execute(query).fetchall() artists = [] for row in rows: artists.append({ 'artist_id': row[0], 'name': row[1], 'genres': row[2].split(',') if row[2] else [] }) return response.Response({'items': artists}) def get_artist_document(artist_id, label_id): """Get artist details in cloudsearch friendly format. Args: artist_id (int): the primary key of artist_info table. label_id (int): the primary key of vendor table. Returns: response.Response: dict with artist details """ with mysql.db_session() as session: query = GET_ARTIST_DOCUMENT_FIELDS.format(artist_id=artist_id) artist = session.execute(query).fetchone() if not artist[0]: not_found_message = 'No artist exists for ID {}'.format(artist_id) return response.create_not_found_response( message=not_found_message) artist_id, artist_name, vendor_id, subaccount_id = artist subaccount_id = [] if not subaccount_id else subaccount_id.split(',') if vendor_id and label_id \ and vendor_id != label_id: return response.create_error_response( code=errors.OWNERSHIP_ERROR, message=errors.ERROR_MESSAGE_FORBIDDEN_USER, status=errors.FORBIDDEN_CODE) return response.Response({ 'artist_id': artist_id, 'artist_name': artist_name, 'vendor_id': vendor_id, 'subaccount_id': subaccount_id })