from fastapi import APIRouter, HTTPException, status, Depends from sqlalchemy.orm import Session from typing import List import uuid from app.schemas import ( Puck as PuckSchema, PuckCreate, PuckUpdate, SetTellPosition, PuckEvent, ) from app.models import ( Puck as PuckModel, Sample as SampleModel, PuckEvent as PuckEventModel, Slot as SlotModel, LogisticsEvent as LogisticsEventModel, Dewar as DewarModel, ) from app.dependencies import get_db from datetime import datetime import logging router = APIRouter() logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @router.get("/", response_model=List[PuckSchema]) async def get_pucks(db: Session = Depends(get_db)): return db.query(PuckModel).all() @router.get("/with-tell-position", response_model=List[dict]) async def get_pucks_with_tell_position(db: Session = Depends(get_db)): """ Retrieve all pucks with a `tell_position` set (not null) and their associated samples. """ # Query all pucks that have an event with a non-null tell_position pucks = ( db.query(PuckModel) .join(PuckEventModel, PuckModel.id == PuckEventModel.puck_id) .filter(PuckEventModel.tell_position.isnot(None)) .all() ) logger.info(f"Pucks with tell position: {pucks}") if not pucks: logger.info("No pucks with tell_position found.") # Log for debugging raise HTTPException( status_code=404, detail="No pucks with a `tell_position` found." ) result = [] for puck in pucks: # Get associated samples for the puck samples = db.query(SampleModel).filter(SampleModel.puck_id == puck.id).all() sample_data = [ { "id": sample.id, "sample_name": sample.sample_name, "position": sample.position, # Updated field based on schema } for sample in samples ] # Add puck and sample info to the result result.append( { "id": puck.id, "puck_name": puck.puck_name, "puck_type": puck.puck_type, "puck_location_in_dewar": puck.puck_location_in_dewar, "dewar_id": puck.dewar_id, "samples": sample_data, # Add associated samples } ) return result @router.get("/{puck_id}", response_model=PuckSchema) async def get_puck(puck_id: str, db: Session = Depends(get_db)): puck = db.query(PuckModel).filter(PuckModel.id == puck_id).first() if not puck: raise HTTPException(status_code=404, detail="Puck not found") return puck @router.post("/", response_model=PuckSchema, status_code=status.HTTP_201_CREATED) async def create_puck(puck: PuckCreate, db: Session = Depends(get_db)) -> PuckSchema: puck_id = f"PUCK-{uuid.uuid4().hex[:8].upper()}" db_puck = PuckModel( id=puck_id, puck_name=puck.puck_name, puck_type=puck.puck_type, puck_location_in_dewar=puck.puck_location_in_dewar, dewar_id=puck.dewar_id, ) db.add(db_puck) db.commit() db.refresh(db_puck) return db_puck @router.put("/{puck_id}", response_model=PuckSchema) async def update_puck( puck_id: str, updated_puck: PuckUpdate, db: Session = Depends(get_db) ): puck = db.query(PuckModel).filter(PuckModel.id == puck_id).first() if not puck: raise HTTPException(status_code=404, detail="Puck not found") for key, value in updated_puck.dict(exclude_unset=True).items(): setattr(puck, key, value) db.commit() db.refresh(puck) return puck @router.delete("/{puck_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_puck(puck_id: str, db: Session = Depends(get_db)): puck = db.query(PuckModel).filter(PuckModel.id == puck_id).first() if not puck: raise HTTPException(status_code=404, detail="Puck not found") db.delete(puck) db.commit() return @router.put("/{puck_id}/tell_position", status_code=status.HTTP_200_OK) async def set_tell_position( puck_id: int, request: SetTellPosition, db: Session = Depends(get_db) ): # Get the requested tell_position tell_position = request.tell_position # Define valid positions valid_positions = [ f"{letter}{num}" for letter in "ABCDEF" for num in range(1, 6) ] + ["null", None] # Validate tell_position if tell_position not in valid_positions: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid tell_position value. Must be one of {valid_positions}.", ) # Set the correct tell_position logic actual_position = None if tell_position in ["null", None] else tell_position # Create a new PuckEvent (always a new event, even with null/None) new_puck_event = PuckEventModel( puck_id=puck_id, tell_position=actual_position, # Null for disassociation, else the valid position event_type="tell_position_set", # Example event type timestamp=datetime.utcnow(), ) db.add(new_puck_event) db.commit() db.refresh(new_puck_event) # Send the response return { "message": "New tell position event created successfully", "tell_position": new_puck_event.tell_position, "timestamp": new_puck_event.timestamp, } @router.get("/{puck_id}/last-tell-position", status_code=status.HTTP_200_OK) async def get_last_tell_position(puck_id: str, db: Session = Depends(get_db)): # Query the most recent tell_position_set event for the given puck_id last_event = ( db.query(PuckEventModel) .filter( PuckEventModel.puck_id == puck_id, PuckEventModel.event_type == "tell_position_set", ) .order_by(PuckEventModel.timestamp.desc()) .first() ) # If no event is found, return a 404 error if not last_event: raise HTTPException( status_code=404, detail=f"No 'tell_position' event found for puck with ID {puck_id}", ) # Return the details of the last tell_position event return { "puck_id": puck_id, "tell_position": last_event.tell_position, "timestamp": last_event.timestamp, } @router.get("/slot/{slot_identifier}", response_model=List[dict]) async def get_pucks_by_slot(slot_identifier: str, db: Session = Depends(get_db)): """ Retrieve all pucks associated with all dewars linked to the given slot (by ID or keyword) via 'beamline' events. - Accepts slot keywords like PXI, PXII, PXIII. - Retrieves all dewars (and their names) associated with the slot. """ # Map keywords to slot IDs slot_aliases = { "PXI": 47, "PXII": 48, "PXIII": 49, "X06SA": 47, "X10SA": 48, "X06DA": 49, } # Check if the slot identifier is an alias or ID try: slot_id = int(slot_identifier) # If the user provided a numeric ID alias = next( (k for k, v in slot_aliases.items() if v == slot_id), slot_identifier ) except ValueError: slot_id = slot_aliases.get(slot_identifier.upper()) # Try mapping alias alias = slot_identifier.upper() # Keep alias as-is for error messages if not slot_id: raise HTTPException( status_code=400, detail="Invalid slot identifier. Must be an ID or one of the following: PXI, PXII, PXIII, X06SA, X10SA, X06DA.", ) # Verify that the slot exists slot = db.query(SlotModel).filter(SlotModel.id == slot_id).first() if not slot: raise HTTPException( status_code=404, detail=f"Slot not found for identifier '{alias}'." ) logger.info(f"Slot found: ID={slot.id}, Label={slot.label}") # Retrieve all beamline events associated with the slot beamline_events = ( db.query(LogisticsEventModel) .filter( LogisticsEventModel.slot_id == slot_id, LogisticsEventModel.event_type == "beamline", ) .order_by(LogisticsEventModel.timestamp.desc()) .all() ) if not beamline_events: logger.warning(f"No dewars associated to this beamline '{alias}'.") raise HTTPException( status_code=404, detail=f"No dewars found for the given beamline '{alias}'." ) logger.info(f"Found {len(beamline_events)} beamline events for slot_id={slot_id}.") # Use the beamline events to find all associated dewars dewar_ids = {event.dewar_id for event in beamline_events if event.dewar_id} dewars = db.query(DewarModel).filter(DewarModel.id.in_(dewar_ids)).all() if not dewars: logger.warning(f"No dewars found for beamline '{alias}'.") raise HTTPException( status_code=404, detail=f"No dewars found for beamline '{alias}'." ) logger.info(f"Found {len(dewars)} dewars for beamline '{alias}'.") # Create a mapping of dewar_id to dewar_name dewar_mapping = {dewar.id: dewar.dewar_name for dewar in dewars} # Retrieve all pucks associated with the dewars puck_list = ( db.query(PuckModel) .filter(PuckModel.dewar_id.in_([dewar.id for dewar in dewars])) .all() ) if not puck_list: logger.warning(f"No pucks found for dewars associated with beamline '{alias}'.") raise HTTPException( status_code=404, detail=f"No pucks found for dewars associated with beamline '{alias}'.", ) logger.info(f"Found {len(puck_list)} pucks for beamline '{alias}'.") # Add the dewar_name to the output for each puck puck_output = [ { "id": puck.id, "puck_name": puck.puck_name, "puck_type": puck.puck_type, "dewar_id": puck.dewar_id, "dewar_name": dewar_mapping.get(puck.dewar_id), # Link dewar_name } for puck in puck_list ] # Return the list of pucks with their associated dewar names return puck_output