"""Contributor identity endpoints.""" from uuid import UUID from fastapi import APIRouter, Depends, HTTPException from neo4j import Driver from pydantic import UUID4 from contributor.api.auth import ( identity_uuid_from_scope, is_oa_user, profiles_from_scope, ) from contributor.api.datasources import get_neo4j_driver from contributor.api.schemas.contributors import ( Contributor, ContributorDataloaderRequest, CreateContributorRequest, MergeContributorsRequest, RenameContributorRequest, SoundRecordingParticipationsResponse, UpdateContributorRequest, ) from contributor.api.schemas.products import ProductParticipationsResponse from contributor.api.schemas.projects import ProjectsResponse from contributor.api.schemas.tracks import TrackParticipationsResponse from contributor.connectors.neo4j import neo4j_session from contributor.logic import contributors as contributor_logic from contributor.logic import products as product_logic from contributor.logic import projects as project_logic from contributor.logic import tracks as track_logic router = APIRouter(prefix="/contributors", tags=["contributors"]) @router.get( "/{contributor_uuid}", operation_id="get_contributor", response_model=Contributor, summary="Get contributor by UUID", description="Returns a contributor by UUID, scoped to the given label.", ) def get_contributor( contributor_uuid: UUID4, identity_uuid: UUID = Depends(identity_uuid_from_scope), profiles: list[dict] = Depends(profiles_from_scope), oa_user: bool = Depends(is_oa_user), driver: Driver = Depends(get_neo4j_driver), ) -> Contributor: with neo4j_session(driver) as session: contributor = contributor_logic.get_contributor( session, contributor_uuid, profiles, identity_uuid, oa_user=oa_user ) if contributor is None: raise HTTPException(status_code=404, detail="Contributor not found") contributor_logic.assert_authorization(contributor=contributor) return contributor @router.post( "/dataloader", operation_id="load_contributors", response_model=list[Contributor], summary="Bulk-load contributors by UUID", description="Returns a list of contributors for the given contributor UUIDs in a single query.", ) def load_contributors( body: ContributorDataloaderRequest, identity_uuid: UUID = Depends(identity_uuid_from_scope), profiles: list[dict] = Depends(profiles_from_scope), oa_user: bool = Depends(is_oa_user), driver: Driver = Depends(get_neo4j_driver), ) -> list[Contributor]: with neo4j_session(driver) as session: contributors = contributor_logic.get_contributors( session, identity_uuid=identity_uuid, contributor_uuids=[key.uuid for key in body.contributors], profiles=profiles, oa_user=oa_user, ) [ contributor_logic.assert_authorization(contributor=contributor) for contributor in contributors ] return contributors @router.post( "", operation_id="create_contributor", response_model=Contributor, status_code=201, summary="Create a contributor", description="Creates a new contributor associated with the given label.", ) def create_contributor( body: CreateContributorRequest, profiles: list[dict] = Depends(profiles_from_scope), oa_user: bool = Depends(is_oa_user), identity_uuid: UUID = Depends(identity_uuid_from_scope), driver: Driver = Depends(get_neo4j_driver), ) -> Contributor: created_by = f"ows-contributor/{identity_uuid}" with neo4j_session(driver, access_mode="WRITE") as session: if not contributor_logic.check_vendor_access(session, profiles, body.vendor_id): raise HTTPException(status_code=403, detail="Forbidden") result = contributor_logic.create_contributor( session, vendor_id=body.vendor_id, subaccount_id=body.subaccount_id, name=body.input.name, spotify_id=body.input.spotify_id, apple_music_id=body.input.apple_music_id, spotify_artist_key=body.input.spotify_artist_key, isni=body.input.isni, global_participant_uuid=body.input.global_participant_uuid, oa_user=oa_user, created_by=created_by, ) return Contributor.model_validate(result) @router.patch( "/{contributor_uuid}", operation_id="update_contributor", response_model=Contributor, summary="Update a contributor", description="Updates a contributor's metadata. Name changes are not supported; use the rename endpoint. The existing spotifyId is preserved if not provided.", ) def update_contributor( contributor_uuid: UUID4, body: UpdateContributorRequest, profiles: list[dict] = Depends(profiles_from_scope), oa_user: bool = Depends(is_oa_user), identity_uuid: UUID = Depends(identity_uuid_from_scope), driver: Driver = Depends(get_neo4j_driver), ) -> Contributor: updated_by = f"ows-contributor/{identity_uuid}" with neo4j_session(driver, access_mode="WRITE") as session: contributor = contributor_logic.get_contributor( session, contributor_uuid, profiles, identity_uuid ) if contributor is None: raise HTTPException(status_code=404, detail="Contributor not found") contributor_logic.assert_authorization(contributor=contributor) result = contributor_logic.update_contributor( session, contributor_uuid=contributor_uuid, profiles=profiles, spotify_id=body.spotify_id, apple_music_id=body.apple_music_id, spotify_artist_key=body.spotify_artist_key, isni=body.isni, global_participant_uuid=body.global_participant_uuid, oa_user=oa_user, updated_by=updated_by, ) if result is None: raise HTTPException(status_code=404, detail="Contributor not found") return Contributor.model_validate(result) @router.patch( "/{contributor_uuid}/rename", operation_id="rename_contributor", response_model=Contributor, summary="Rename a contributor", description="Renames a contributor's display name.", ) def rename_contributor( contributor_uuid: UUID4, body: RenameContributorRequest, profiles: list[dict] = Depends(profiles_from_scope), oa_user: bool = Depends(is_oa_user), identity_uuid: UUID = Depends(identity_uuid_from_scope), driver: Driver = Depends(get_neo4j_driver), ) -> Contributor: updated_by = f"ows-contributor/{identity_uuid}" with neo4j_session(driver, access_mode="WRITE") as session: contributor = contributor_logic.get_contributor( session, contributor_uuid, profiles, identity_uuid ) if contributor is None: raise HTTPException(status_code=404, detail="Contributor not found") contributor_logic.assert_authorization(contributor=contributor) result = contributor_logic.rename_contributor( session, contributor_uuid=contributor_uuid, name=body.name, updated_by=updated_by, profiles=profiles, oa_user=oa_user, ) return Contributor.model_validate(result) @router.post( "/{contributor_uuid}/merge", operation_id="merge_contributor", response_model=Contributor, summary="Merge contributors", description="Merges a duplicate contributor into another contributor.", ) def merge_contributors( contributor_uuid: UUID4, body: MergeContributorsRequest, profiles: list[dict] = Depends(profiles_from_scope), oa_user: bool = Depends(is_oa_user), identity_uuid: UUID = Depends(identity_uuid_from_scope), driver: Driver = Depends(get_neo4j_driver), ) -> Contributor: with neo4j_session(driver, access_mode="WRITE") as session: contributors = contributor_logic.get_contributors( session, contributor_uuids=[contributor_uuid, body.duplicate_contributor.uuid], profiles=profiles, identity_uuid=identity_uuid, ) [ contributor_logic.assert_authorization(contributor=contributor) for contributor in contributors ] return contributor_logic.merge_contributors( neo4j_session=session, contributor_uuid=contributor_uuid, duplicate_uuid=body.duplicate_contributor.uuid, updated_by=f"ows-contributor/{identity_uuid}", profiles=profiles, oa_user=oa_user, ) @router.get( "/{contributor_uuid}/track-participations", operation_id="get_track_participations", response_model=TrackParticipationsResponse, summary="Get track participations for a contributor", description="Returns all track participations for a contributor by UUID.", ) def get_track_participations( contributor_uuid: UUID4, identity_uuid: UUID = Depends(identity_uuid_from_scope), profiles: list[dict] = Depends(profiles_from_scope), driver: Driver = Depends(get_neo4j_driver), ) -> TrackParticipationsResponse: with neo4j_session(driver) as session: contributor = contributor_logic.get_contributor( session, contributor_uuid, profiles, identity_uuid=identity_uuid ) if contributor is None: raise HTTPException(status_code=404, detail="Contributor not found") contributor_logic.assert_authorization(contributor=contributor) result = track_logic.get_track_participations( session, contributor_uuid, profiles ) if result is None: raise HTTPException(status_code=404, detail="Contributor not found") return TrackParticipationsResponse.model_validate(result) @router.get( "/{contributor_uuid}/product-participations", operation_id="get_product_participations", response_model=ProductParticipationsResponse, summary="Get product participations for a contributor", description="Returns all product participations for a contributor by UUID.", ) def get_product_participations( contributor_uuid: UUID4, identity_uuid: UUID = Depends(identity_uuid_from_scope), profiles: list[dict] = Depends(profiles_from_scope), driver: Driver = Depends(get_neo4j_driver), ) -> ProductParticipationsResponse: with neo4j_session(driver) as session: contributor = contributor_logic.get_contributor( session, contributor_uuid, profiles, identity_uuid=identity_uuid ) if contributor is None: raise HTTPException(status_code=404, detail="Contributor not found") contributor_logic.assert_authorization(contributor=contributor) result = product_logic.get_product_participations( session, contributor_uuid, profiles ) if result is None: raise HTTPException(status_code=404, detail="Contributor not found") return ProductParticipationsResponse.model_validate(result) @router.get( "/{contributor_uuid}/sound-recording-participations", operation_id="get_sound_recording_participations", response_model=SoundRecordingParticipationsResponse, summary="Get sound recording participations for a contributor", description="Returns all sound recording participations for a contributor by UUID.", ) def get_sound_recording_participations( contributor_uuid: UUID4, identity_uuid: UUID = Depends(identity_uuid_from_scope), profiles: list[dict] = Depends(profiles_from_scope), driver: Driver = Depends(get_neo4j_driver), ) -> SoundRecordingParticipationsResponse: with neo4j_session(driver) as session: contributor = contributor_logic.get_contributor( session, contributor_uuid, profiles, identity_uuid=identity_uuid ) if contributor is None: raise HTTPException(status_code=404, detail="Contributor not found") contributor_logic.assert_authorization(contributor=contributor) result = contributor_logic.get_sound_recording_participations( session, contributor_uuid, profiles=profiles ) if result is None: raise HTTPException(status_code=404, detail="Contributor not found") return SoundRecordingParticipationsResponse.model_validate(result) @router.get( "/{contributor_uuid}/projects", operation_id="get_projects", response_model=ProjectsResponse, summary="Get projects for a contributor", description="Returns all projects for a contributor by UUID.", ) def get_projects( contributor_uuid: UUID4, identity_uuid: UUID = Depends(identity_uuid_from_scope), profiles: list[dict] = Depends(profiles_from_scope), driver: Driver = Depends(get_neo4j_driver), ) -> ProjectsResponse: with neo4j_session(driver) as session: contributor = contributor_logic.get_contributor( session, contributor_uuid, profiles, identity_uuid=identity_uuid ) if contributor is None: raise HTTPException(status_code=404, detail="Contributor not found") contributor_logic.assert_authorization(contributor=contributor) result = project_logic.get_projects(session, contributor_uuid, profiles) if result is None: raise HTTPException(status_code=404, detail="Contributor not found") return ProjectsResponse.model_validate(result)