"""Business logic for contributor endpoints.""" from typing import Any from uuid import UUID from ddtrace import tracer from fastapi import HTTPException from neo4j import Session as Neo4jSession from pydantic import UUID4 from sqlalchemy.orm import Session as MySQLSession from contributor.api import datasources from contributor.api.schemas.contributors import Contributor from contributor.connectors import ows_permissions from contributor.connectors.features import BooleanFeature from contributor.connectors.mysql import db_session_wrap from contributor.constants.features import FEATURE_FLAG_CCM_CONTRIBUTOR_ART_RELATIONS from contributor.queries.mysql import ( artist_info as artist_info_queries, ) from contributor.queries.mysql import contributor as mysql_queries from contributor.queries.mysql import product_display_artist as display_artist_queries from contributor.queries.mysql import ( reassign_artist_info, reassign_contributor_content, remove_artist_info_references, rename_art_relations_contributor_records, ) from contributor.queries.neo4j import contributors as neo4j_queries from contributor.queries.neo4j import product_participations as pp_queries from contributor.queries.neo4j import projects as project_queries from contributor.queries.neo4j import sound_recording_participations as srp_queries from contributor.queries.neo4j import track_participations as tp_queries def label_profile_ids(profiles: list[dict]) -> list[int]: return [ p["profile_id"] for p in profiles if p.get("profile_type") == "LabelProfile" and "profile_id" in p ] @tracer.wrap() def check_vendor_access( session: Neo4jSession, profiles: list[dict], vendor_id: int ) -> bool: if neo4j_queries.has_full_catalog_access(session, profiles): return True return neo4j_queries.check_vendor_access( session, profile_ids=label_profile_ids(profiles), vendor_id=vendor_id ) @tracer.wrap() def assert_authorization(*, contributor: Contributor) -> None: """ Assert that a user is authorized to access a Contributor. Raises: HTTPException(404) if the user does not have access to the contributor's label. """ if not ows_permissions.can_access_vendor(vendor_id=contributor.label.vendor_id): raise HTTPException( status_code=404, detail=f"Contributor {contributor.uuid} not found" ) @tracer.wrap() def get_contributor( neo4j_session: Neo4jSession, contributor_uuid: UUID4, profiles: list[dict], identity_uuid: UUID, *, oa_user: bool = False, ) -> Contributor | None: results = get_contributors( neo4j_session, [contributor_uuid], profiles, identity_uuid=identity_uuid, oa_user=oa_user, ) return results[0] if results else None @tracer.wrap() def get_contributors( neo4j_session: Neo4jSession, contributor_uuids: list[UUID4], profiles: list[dict], identity_uuid: UUID, *, oa_user: bool = False, ) -> list[Contributor]: art_relations_feature = BooleanFeature( client=datasources.get_splitio_client(), feature_name=FEATURE_FLAG_CCM_CONTRIBUTOR_ART_RELATIONS, ) if art_relations_feature.is_on_for_identity(str(identity_uuid)): contributors = mysql_queries.get_by_uuids( contributor_uuids=contributor_uuids, ) results = [ Contributor.model_validate(contributor) for contributor in contributors ] if not oa_user: for contributor in results: contributor.spotify_artist_key = None return results is_admin = neo4j_queries.has_full_catalog_access(neo4j_session, profiles) contributors = neo4j_queries.get_by_uuids( neo4j_session, uuids=[str(u) for u in contributor_uuids], profile_ids=label_profile_ids(profiles), is_admin=is_admin, ) results = [Contributor.model_validate(contributor) for contributor in contributors] if not oa_user: for contributor in results: contributor.spotify_artist_key = None return results @tracer.wrap() def create_contributor( session: Neo4jSession, vendor_id: int, subaccount_id: int | None, name: str, spotify_id: str | None, apple_music_id: str | None, spotify_artist_key: str | None, isni: str | None, global_participant_uuid: str | None, created_by: str, oa_user: bool = False, ) -> dict: result = neo4j_queries.create( session, vendor_id=vendor_id, subaccount_id=subaccount_id, name=name, spotify_id=spotify_id, apple_music_id=apple_music_id, spotify_artist_key=spotify_artist_key, isni=isni, global_participant_uuid=global_participant_uuid, created_by=created_by, ) contributor = Contributor.model_validate(result) if not oa_user: contributor.spotify_artist_key = None return contributor.model_dump(mode="json") @tracer.wrap() def update_contributor( session: Neo4jSession, contributor_uuid: UUID4, profiles: list[dict], spotify_id: str | None, apple_music_id: str | None, spotify_artist_key: str | None, isni: str | None, global_participant_uuid: str | None, updated_by: str, oa_user: bool = False, ) -> dict | None: is_admin = neo4j_queries.has_full_catalog_access(session, profiles) result = neo4j_queries.update( session, uuid=str(contributor_uuid), profile_ids=label_profile_ids(profiles), is_admin=is_admin, spotify_id=spotify_id, apple_music_id=apple_music_id, spotify_artist_key=spotify_artist_key, isni=isni, global_participant_uuid=global_participant_uuid, updated_by=updated_by, ) if result is None: return None contributor = Contributor.model_validate(result) if not oa_user: contributor.spotify_artist_key = None return contributor.model_dump(mode="json") @tracer.wrap() def get_global_participant( session: Neo4jSession, global_participant_id: str, profiles: list[dict], ) -> dict[str, Any] | None: results = get_global_participants(session, [global_participant_id], profiles) return results[0] if results else None @tracer.wrap() def get_global_participants( session: Neo4jSession, global_participant_ids: list[str], profiles: list[dict], ) -> list[dict[str, Any]]: is_admin = neo4j_queries.has_full_catalog_access(session, profiles) return neo4j_queries.get_gp_by_ids( session, ids=global_participant_ids, profile_ids=label_profile_ids(profiles), is_admin=is_admin, ) @tracer.wrap() def get_contributors_for_global_participant( session: Neo4jSession, global_participant_id: str, profiles: list[dict], ) -> list[dict[str, Any]] | None: is_admin = neo4j_queries.has_full_catalog_access(session, profiles) return neo4j_queries.get_contributors_for_global_participant( session, global_participant_id=global_participant_id, profile_ids=label_profile_ids(profiles), is_admin=is_admin, ) @tracer.wrap() def get_track_participations( session: Neo4jSession, contributor_uuid: UUID4, profiles: list[dict], identity_uuid: UUID, ) -> dict[str, Any] | None: is_admin = neo4j_queries.has_full_catalog_access(session, profiles) result = tp_queries.get_by_label_participant_uuid( session, uuid=str(contributor_uuid), identity_uuid=identity_uuid, profile_ids=label_profile_ids(profiles), is_admin=is_admin, ) if result is None: return None result["participations"] = [ p for p in result["participations"] if p.get("participated_as") != "feature_to_primary" ] return result @tracer.wrap() def get_track_participations_by_track_ids( session: Neo4jSession, track_ids: list[int], profiles: list[dict], identity_uuid: UUID, ) -> list[dict[str, Any]]: is_admin = neo4j_queries.has_full_catalog_access(session, profiles) results = ( tp_queries.get_by_track_ids( session, track_ids=track_ids, identity_uuid=identity_uuid, profile_ids=label_profile_ids(profiles), is_admin=is_admin, ) or [] ) for result in results: result["participations"] = [ p for p in result["participations"] if p is not None and p.get("participated_as") != "feature_to_primary" ] return results @tracer.wrap() def get_track_participations_for_track( session: Neo4jSession, track_id: int, profiles: list[dict], identity_uuid: UUID, ) -> dict[str, Any] | None: results = get_track_participations_by_track_ids( session, track_ids=[track_id], identity_uuid=identity_uuid, profiles=profiles, ) return results[0] if results else None @tracer.wrap() def get_product_participations( session: Neo4jSession, contributor_uuid: UUID4, profiles: list[dict], ) -> dict[str, Any] | None: is_admin = neo4j_queries.has_full_catalog_access(session, profiles) result = pp_queries.get_by_label_participant_uuid( session, uuid=str(contributor_uuid), profile_ids=label_profile_ids(profiles), is_admin=is_admin, ) if result is None: return None result["participations"] = [ p for p in result["participations"] if p.get("participated_as") != "feature_to_primary" ] return result @tracer.wrap() def get_sound_recording_participations( session: Neo4jSession, contributor_uuid: UUID4, profiles: list[dict], ) -> dict[str, Any] | None: is_admin = neo4j_queries.has_full_catalog_access(session, profiles) return srp_queries.get_by_label_participant_uuid( session, uuid=str(contributor_uuid), profile_ids=label_profile_ids(profiles), is_admin=is_admin, ) @tracer.wrap() def get_projects( session: Neo4jSession, contributor_uuid: UUID4, profiles: list[dict], ) -> dict[str, Any] | None: is_admin = neo4j_queries.has_full_catalog_access(session, profiles) return project_queries.get_by_label_participant_uuid( session, uuid=str(contributor_uuid), profile_ids=label_profile_ids(profiles), is_admin=is_admin, ) @tracer.wrap() def get_product_contributors( session: Neo4jSession, product_ids: list[int], profiles: list[dict], ) -> list[dict[str, Any]]: is_admin = neo4j_queries.has_full_catalog_access(session, profiles) results = pp_queries.get_by_product_ids( session, product_ids=product_ids, profile_ids=label_profile_ids(profiles), is_admin=is_admin, ) display_artist_results = display_artist_queries.get_by_product_ids( product_ids=product_ids, ) display_artists_by_product_id = { result["product"]["product_id"]: result["display_artists"] for result in display_artist_results } for result in results: result["contributors"] = [ p for p in result["contributors"] if p.get("participated_as") != "feature_to_primary" ] product_id = result["product"]["product_id"] result["display_artists"] = display_artists_by_product_id.get(product_id, []) return results @tracer.wrap() def get_project_contributors( session: Neo4jSession, project_ids: list[int], profiles: list[dict], ) -> list[dict[str, Any]]: is_admin = neo4j_queries.has_full_catalog_access(session, profiles) return project_queries.get_contributors_by_project_ids( session, project_ids=project_ids, profile_ids=label_profile_ids(profiles), is_admin=is_admin, ) @tracer.wrap() @db_session_wrap def rename_contributor( neo4j_session: Neo4jSession, session: MySQLSession, *, contributor_uuid: UUID4, name: str, updated_by: str, profiles: list[dict], identity_uuid: UUID, oa_user: bool = False, ) -> Contributor: """ Rename a contributor and its associated art relations records. Raises: HTTPException(404) if the contributor is not found or not accessible to the user. HTTPException(403) if the user does not have access to the contributor's label. HTTPException(400) if the contributor is not associated with a vendor. """ contributor = get_contributor( neo4j_session=neo4j_session, contributor_uuid=contributor_uuid, identity_uuid=identity_uuid, profiles=profiles, oa_user=oa_user, ) if not contributor: raise HTTPException(status_code=404, detail="Contributor not found") current_name, vendor_id = contributor.name, contributor.label.vendor_id if not vendor_id: raise HTTPException( status_code=400, detail="Contributor is not associated with a vendor", ) if not check_vendor_access(neo4j_session, profiles, vendor_id): raise HTTPException(status_code=403, detail="Forbidden") try: rename_art_relations_contributor_records( session=session, name=name, vendor_id=vendor_id, current_name=current_name, ) result = neo4j_queries.rename( session=neo4j_session, uuid=str(contributor_uuid), name=name, updated_by=updated_by, profile_ids=label_profile_ids(profiles), is_admin=neo4j_queries.has_full_catalog_access(neo4j_session, profiles), ) assert result is not None, "Contributor rename failed in Neo4j" session.commit() return Contributor.model_validate(result) except Exception as e: session.rollback() raise HTTPException( status_code=500, detail=f"Failed to rename contributor in art relations: {str(e)}", ) @db_session_wrap @tracer.wrap() def merge_contributors( session: MySQLSession, neo4j_session: Neo4jSession, contributor_uuid: UUID4, duplicate_uuid: UUID4, profiles: list[dict], updated_by: str, identity_uuid: UUID4, oa_user: bool = False, ) -> Contributor: # Grab contributor references contributor = get_contributor( identity_uuid=identity_uuid, neo4j_session=neo4j_session, contributor_uuid=contributor_uuid, profiles=profiles, oa_user=oa_user, ) assert contributor if not contributor.label.vendor_id: raise HTTPException( status_code=400, detail="Contributor is not associated with a vendor", ) assert contributor.label.vendor_id duplicate_contributor = get_contributor( neo4j_session=neo4j_session, contributor_uuid=duplicate_uuid, identity_uuid=identity_uuid, profiles=profiles, oa_user=oa_user, ) assert duplicate_contributor if not contributor or not duplicate_contributor: raise HTTPException(status_code=404, detail="Contributor not found") artist_info: dict[str, Any] | None = artist_info_queries.get( session=session, name=contributor.name, vendor_id=contributor.label.vendor_id, ) duplicate_artist_info: dict[str, Any] | None = artist_info_queries.get( session=session, name=duplicate_contributor.name, vendor_id=duplicate_contributor.label.vendor_id, ) # Rename art relations records rename_art_relations_contributor_records( session=session, name=contributor.name, vendor_id=contributor.label.vendor_id, current_name=duplicate_contributor.name, ) if artist_info and duplicate_artist_info: # Reassign content references reassign_contributor_content( session=session, contributor_artist_info_id=artist_info["artist_id"], duplicate_artist_info_id=duplicate_artist_info["artist_id"], ) # Reassign ArtistInfo references reassign_artist_info( session=session, artist_name=contributor.name, artist_info_id=artist_info["artist_id"], duplicate_artist_info_id=duplicate_artist_info["artist_id"], ) # Remove unused ArtistInfo references remove_artist_info_references( session=session, artist_info_id=duplicate_artist_info["artist_id"] ) # Merge Neo4j Nodes neo4j_queries.merge( session=neo4j_session, contributor_uuid=contributor_uuid, duplicate_uuid=duplicate_uuid, updated_by=updated_by, profile_ids=label_profile_ids(profiles), is_admin=neo4j_queries.has_full_catalog_access(neo4j_session, profiles), ) contributor = get_contributor( neo4j_session=neo4j_session, contributor_uuid=contributor_uuid, identity_uuid=identity_uuid, profiles=profiles, oa_user=oa_user, ) if not contributor: raise HTTPException(status_code=404, detail="Contributor not found") return contributor