diff --git a/src/nsls2api/api/models/stats_model.py b/src/nsls2api/api/models/stats_model.py index 5b0b735..101cbd5 100644 --- a/src/nsls2api/api/models/stats_model.py +++ b/src/nsls2api/api/models/stats_model.py @@ -13,7 +13,8 @@ class StatsModel(pydantic.BaseModel): beamline_count: int commissioning_proposal_count: int facility_data_health: bool - nsls2_proposals_per_cycle: Optional[list[ProposalsPerCycleModel]] = [] + nsls2_proposals_per_cycle: Optional[list[ProposalsPerCycleModel]] + lbms_proposals_per_cycle: Optional[list[ProposalsPerCycleModel]] class AboutModel(pydantic.BaseModel): diff --git a/src/nsls2api/api/v1/facility_api.py b/src/nsls2api/api/v1/facility_api.py index 89002d0..0246d28 100644 --- a/src/nsls2api/api/v1/facility_api.py +++ b/src/nsls2api/api/v1/facility_api.py @@ -62,17 +62,12 @@ async def get_facility_cycles(facility: FacilityName): include_in_schema=True, ) async def get_proposals_for_cycle(facility: FacilityName, cycle: str): - if facility.name != "nsls2": - # TODO: Add other facilities - return fastapi.responses.JSONResponse( - {"message": f"Not implemented for the {facility.name} facility."}, - status_code=501, - ) - - proposal_list = await proposal_service.fetch_proposals_for_cycle(cycle) + proposal_list = await proposal_service.fetch_proposals_for_cycle(cycle, facility) if proposal_list is None: return fastapi.responses.JSONResponse( - {"error": f"No proposals were found for cycle {cycle}"}, + { + "error": f"No proposals were found for cycle {cycle} for facility {facility.name}" + }, status_code=404, ) model = CycleProposalList( diff --git a/src/nsls2api/api/v1/jobs_api.py b/src/nsls2api/api/v1/jobs_api.py index 4e02505..0a6961c 100644 --- a/src/nsls2api/api/v1/jobs_api.py +++ b/src/nsls2api/api/v1/jobs_api.py @@ -13,7 +13,8 @@ ) from nsls2api.services import background_service -SYNC_ROUTES_IN_SCHEMA = False +# TODO: This flag should be automatically set to be True for development but False for production. +SYNC_ROUTES_IN_SCHEMA = True router = fastapi.APIRouter(tags=["jobs"]) @@ -82,9 +83,29 @@ async def sync_proposal_types(facility: FacilityName = FacilityName.nsls2): dependencies=[Depends(get_current_user)], include_in_schema=SYNC_ROUTES_IN_SCHEMA, tags=["sync"], + deprecated=True, ) -async def sync_proposals_for_cycle(request: Request, cycle: str) -> BackgroundJob: - sync_params = JobSyncParameters(cycle=cycle) +async def sync_proposals_for_cycle( + request: Request, cycle: str, facility: FacilityName = FacilityName.nsls2 +) -> BackgroundJob: + sync_params = JobSyncParameters(cycle=cycle, facility=facility) + job = await background_service.create_background_job( + JobActions.synchronize_proposals_for_cycle, + sync_parameters=sync_params, + ) + return job + + +@router.get( + "/sync/facility/{facility}/cycle/{cycle}/proposals", + dependencies=[Depends(get_current_user)], + include_in_schema=SYNC_ROUTES_IN_SCHEMA, + tags=["sync"], +) +async def sync_proposals_for_facility_cycle( + request: Request, facility: FacilityName, cycle: str +) -> BackgroundJob: + sync_params = JobSyncParameters(cycle=cycle, facility=facility) job = await background_service.create_background_job( JobActions.synchronize_proposals_for_cycle, sync_parameters=sync_params, @@ -108,6 +129,7 @@ async def sync_cycles(facility: FacilityName = FacilityName.nsls2): "/sync/update-cycles/{facility}", include_in_schema=SYNC_ROUTES_IN_SCHEMA, tags=["sync"], + summary="Updates the local (nsls2core DB) cycle <-> proposal mapping", ) async def sync_update_cycles( request: fastapi.Request, diff --git a/src/nsls2api/api/v1/proposal_api.py b/src/nsls2api/api/v1/proposal_api.py index 8ffbb0a..b53f2e7 100644 --- a/src/nsls2api/api/v1/proposal_api.py +++ b/src/nsls2api/api/v1/proposal_api.py @@ -1,4 +1,3 @@ -from http.client import HTTPException from typing import Annotated import fastapi diff --git a/src/nsls2api/api/v1/stats_api.py b/src/nsls2api/api/v1/stats_api.py index 490e9c8..8a62f1b 100644 --- a/src/nsls2api/api/v1/stats_api.py +++ b/src/nsls2api/api/v1/stats_api.py @@ -24,8 +24,8 @@ async def stats(): facility_data_health = await facility_service.is_healthy("nsls2") + # Get the NSLS-II proposals per cycle nsls2_proposals_per_cycle: list[ProposalsPerCycleModel] = [] - nsls2_cycle_list = await facility_service.facility_cycles("nsls2") for cycle in nsls2_cycle_list: proposal_list = await proposal_service.fetch_proposals_for_cycle(cycle) @@ -35,6 +35,17 @@ async def stats(): ) nsls2_proposals_per_cycle.append(model) + # Get the LBMS proposals per cycle + lbms_proposals_per_cycle: list[ProposalsPerCycleModel] = [] + lbms_cycle_list = await facility_service.facility_cycles("lbms") + for cycle in lbms_cycle_list: + proposal_list = await proposal_service.fetch_proposals_for_cycle(cycle) + if proposal_list is not None: + model = ProposalsPerCycleModel( + cycle=cycle, proposal_count=len(proposal_list) + ) + lbms_proposals_per_cycle.append(model) + model = StatsModel( facility_count=facilities, beamline_count=beamlines, @@ -42,6 +53,7 @@ async def stats(): commissioning_proposal_count=commissioning, facility_data_health=facility_data_health, nsls2_proposals_per_cycle=nsls2_proposals_per_cycle, + lbms_proposals_per_cycle=lbms_proposals_per_cycle, ) return model diff --git a/src/nsls2api/services/background_service.py b/src/nsls2api/services/background_service.py index 267c3a1..bb2f8b8 100644 --- a/src/nsls2api/services/background_service.py +++ b/src/nsls2api/services/background_service.py @@ -130,10 +130,10 @@ async def worker_function(): ) case JobActions.synchronize_proposals_for_cycle: logger.info( - f"Processing job {job.id} to synchronize proposals for cycle {job.sync_parameters.cycle} (from {job.sync_parameters.sync_source})." + f"Processing job {job.id} to synchronize proposals for the {job.sync_parameters.facility} facility's cycle {job.sync_parameters.cycle} (from {job.sync_parameters.sync_source})." ) await sync_service.worker_synchronize_proposals_for_cycle_from_pass( - job.sync_parameters.cycle + job.sync_parameters.cycle, job.sync_parameters.facility ) case JobActions.synchronize_proposal_types: logger.info( diff --git a/src/nsls2api/services/pass_service.py b/src/nsls2api/services/pass_service.py index 541bf75..4bc55a9 100644 --- a/src/nsls2api/services/pass_service.py +++ b/src/nsls2api/services/pass_service.py @@ -122,11 +122,11 @@ async def get_saf_from_proposal( async def get_commissioning_proposals_by_year( - year: str, facility: FacilityName = FacilityName.nsls2 + year: str, facility_name: FacilityName = FacilityName.nsls2 ) -> Optional[list[PassProposal]]: - pass_facility = await facility_service.pass_id_for_facility(facility) + pass_facility = await facility_service.pass_id_for_facility(facility_name) if not pass_facility: - error_message: str = f"Facility {facility} does not have a PASS ID." + error_message: str = f"Facility {facility_name} does not have a PASS ID." logger.error(error_message) raise PassException(error_message) @@ -142,11 +142,11 @@ async def get_commissioning_proposals_by_year( PassProposal(**commissioning_proposal) ) except ValidationError as error: - error_message = f"Error validating commissioning proposal data received from PASS for year {str(year)} at {facility} facility." + error_message = f"Error validating commissioning proposal data received from PASS for year {str(year)} at {facility_name} facility." logger.error(error_message) raise PassException(error_message) from error except Exception as error: - error_message = f"Error retrieving commissioning proposal information from PASS for year {str(year)} at {facility} facility." + error_message = f"Error retrieving commissioning proposal information from PASS for year {str(year)} at {facility_name} facility." logger.exception(error_message) raise PassException(error_message) from error diff --git a/src/nsls2api/services/proposal_service.py b/src/nsls2api/services/proposal_service.py index e6b9eb3..88e7f77 100644 --- a/src/nsls2api/services/proposal_service.py +++ b/src/nsls2api/services/proposal_service.py @@ -74,8 +74,12 @@ async def recently_updated(count=5, beamline: str | None = None): # return result -async def fetch_proposals_for_cycle(cycle_name: str) -> list[str]: - cycle = await Cycle.find_one(Cycle.name == cycle_name) +async def fetch_proposals_for_cycle( + cycle_name: str, facility_name: FacilityName = FacilityName.nsls2 +) -> list[str]: + cycle = await Cycle.find_one( + Cycle.name == cycle_name, Cycle.facility == facility_name + ) if cycle is None: raise LookupError(f"Cycle {cycle} not found in local database.") return cycle.proposals diff --git a/src/nsls2api/services/sync_service.py b/src/nsls2api/services/sync_service.py index 48f2b12..0818012 100644 --- a/src/nsls2api/services/sync_service.py +++ b/src/nsls2api/services/sync_service.py @@ -317,7 +317,9 @@ async def synchronize_proposal_from_pass(proposal_id: str) -> None: logger.debug(f"Response: {response}") -async def update_proposals_with_cycle(cycle_name: str) -> None: +async def update_proposals_with_cycle( + cycle_name: str, facility_name: FacilityName = FacilityName.nsls2 +) -> None: """ Update the cycle <-> proposals mapping for the given cycle. @@ -325,9 +327,13 @@ async def update_proposals_with_cycle(cycle_name: str) -> None: :type cycle_name: str """ - proposal_list = await proposal_service.fetch_proposals_for_cycle(cycle_name) + proposal_list = await proposal_service.fetch_proposals_for_cycle( + cycle_name, facility_name=facility_name + ) - logger.info(f"Found {len(proposal_list)} proposals for cycle {cycle_name}.") + logger.info( + f"Found {len(proposal_list)} proposals for {facility_name} cycle {cycle_name}." + ) for proposal_id in proposal_list: # Add the cycle to the Proposal object @@ -352,13 +358,19 @@ async def worker_synchronize_proposal_from_pass(proposal_id: str) -> None: ) -async def worker_synchronize_proposals_for_cycle_from_pass(cycle: str) -> None: +async def worker_synchronize_proposals_for_cycle_from_pass( + cycle: str, facility_name: FacilityName = FacilityName.nsls2 +) -> None: start_time = datetime.datetime.now() - cycle_year = await facility_service.cycle_year(cycle) + cycle_year = await facility_service.cycle_year(cycle, facility_name=facility_name) - proposals = await proposal_service.fetch_proposals_for_cycle(cycle) - logger.info(f"Synchronizing {len(proposals)} proposals for {cycle} cycle.") + proposals = await proposal_service.fetch_proposals_for_cycle( + cycle, facility_name=facility_name + ) + logger.info( + f"Synchronizing {len(proposals)} proposals for facility {facility_name} in {cycle} cycle." + ) for proposal_id in proposals: logger.info(f"Synchronizing proposal {proposal_id}.") @@ -366,7 +378,9 @@ async def worker_synchronize_proposals_for_cycle_from_pass(cycle: str) -> None: commissioning_proposals: list[ PassProposal - ] = await pass_service.get_commissioning_proposals_by_year(cycle_year) + ] = await pass_service.get_commissioning_proposals_by_year( + cycle_year, facility_name=facility_name + ) logger.info( f"Synchronizing {len(proposals)} commissioning proposals for the year {cycle_year}." ) @@ -375,7 +389,7 @@ async def worker_synchronize_proposals_for_cycle_from_pass(cycle: str) -> None: await synchronize_proposal_from_pass(str(proposal.Proposal_ID)) # Now update the cycle information for each proposal - await update_proposals_with_cycle(cycle) + await update_proposals_with_cycle(cycle, facility_name=facility_name) time_taken = datetime.datetime.now() - start_time logger.info(