from fastapi import APIRouter, HTTPException, Depends from pydantic import ValidationError from sqlalchemy.orm import Session, joinedload from typing import List, Optional from ..models import Dewar as DewarModel, Slot as SlotModel, LogisticsEvent as LogisticsEventModel from ..schemas import LogisticsEventCreate, SlotSchema, Dewar as DewarSchema from ..database import get_db import logging from datetime import datetime, timedelta router = APIRouter() logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def calculate_time_until_refill(last_refill: Optional[datetime], refill_interval_hours: int = 1) -> int: refill_interval = timedelta(hours=refill_interval_hours) now = datetime.now() if not last_refill: return -1 # Sentinel value indicating no refill event recorded time_until_next_refill = last_refill + refill_interval - now return max(0, int(time_until_next_refill.total_seconds())) @router.post("/dewars/return", response_model=DewarSchema) async def return_to_storage(data: LogisticsEventCreate, db: Session = Depends(get_db)): logger.info(f"Returning dewar to storage: {data.dewar_qr_code} at location {data.location_qr_code}") try: # Log the incoming payload logger.info("Received payload: %s", data.json()) dewar = db.query(DewarModel).filter(DewarModel.unique_id == data.dewar_qr_code).first() if not dewar: logger.error(f"Dewar not found for unique ID: {data.dewar_qr_code}") raise HTTPException(status_code=404, detail="Dewar not found") original_slot = db.query(SlotModel).filter(SlotModel.dewar_unique_id == data.dewar_qr_code).first() if original_slot and original_slot.qr_code != data.location_qr_code: logger.error(f"Dewar {data.dewar_qr_code} is associated with slot {original_slot.qr_code}") raise HTTPException(status_code=400, detail=f"Dewar {data.dewar_qr_code} is associated with a different slot {original_slot.qr_code}.") slot = db.query(SlotModel).filter(SlotModel.qr_code == data.location_qr_code).first() if not slot: logger.error(f"Slot not found for QR code: {data.location_qr_code}") raise HTTPException(status_code=404, detail="Slot not found") if slot.occupied and slot.dewar_unique_id != data.dewar_qr_code: logger.error(f"Slot {data.location_qr_code} is already occupied by another dewar") raise HTTPException(status_code=400, detail="Selected slot is already occupied by another dewar") # Update slot with dewar information slot.dewar_unique_id = dewar.unique_id slot.occupied = True dewar.last_retrieved_timestamp = None # Log the event log_event(db, dewar.id, slot.id, "returned") db.commit() logger.info(f"Dewar {data.dewar_qr_code} successfully returned to storage slot {slot.qr_code}.") db.refresh(dewar) return dewar except ValidationError as e: logger.error(f"Validation error: {e.json()}") raise HTTPException(status_code=400, detail="Invalid payload") except Exception as e: logger.error(f"Unexpected error: {str(e)}") raise HTTPException(status_code=500, detail="Internal server error") @router.post("/dewar/scan", response_model=dict) async def scan_dewar(event_data: LogisticsEventCreate, db: Session = Depends(get_db)): logger.info(f"Received event data: {event_data}") dewar_qr_code = event_data.dewar_qr_code location_qr_code = event_data.location_qr_code transaction_type = event_data.transaction_type if not dewar_qr_code or not dewar_qr_code.strip(): logger.error("Dewar QR Code is null or empty") raise HTTPException(status_code=422, detail="Dewar QR Code cannot be null or empty") dewar = db.query(DewarModel).filter(DewarModel.unique_id == dewar_qr_code).first() if not dewar: raise HTTPException(status_code=404, detail="Dewar not found") slot = db.query(SlotModel).filter(SlotModel.qr_code == location_qr_code).first() if transaction_type == 'incoming': if not slot or slot.occupied: logger.error(f"Slot not found or already occupied: {slot}") raise HTTPException(status_code=400, detail="Slot not found or already occupied") slot.dewar_unique_id = dewar.unique_id slot.occupied = True elif transaction_type == 'outgoing': if not slot or not slot.occupied or slot.dewar_unique_id != dewar.unique_id: logger.error(f"Slot not found or dewar not associated with slot: {slot}") raise HTTPException(status_code=400, detail="Slot not found or dewar not associated with slot") slot.dewar_unique_id = None slot.occupied = False elif transaction_type == 'beamline': if not slot: logger.error(f"Beamline location not found: {location_qr_code}") raise HTTPException(status_code=400, detail="Beamline location not found") dewar.beamline_location = location_qr_code logger.info(f"Dewar {dewar_qr_code} assigned to beamline {location_qr_code}") log_event(db, dewar.id, slot.id if slot else None, transaction_type) db.commit() return {"message": "Status updated successfully"} @router.get("/slots", response_model=List[SlotSchema]) async def get_all_slots(db: Session = Depends(get_db)): slots = db.query(SlotModel).options(joinedload(SlotModel.dewar)).all() slots_with_refill_time = [] for slot in slots: time_until_refill = None retrievedTimestamp = None beamlineLocation = None at_beamline = False retrieved = False if slot.dewar_unique_id: # Calculate time until refill last_refill_event = db.query(LogisticsEventModel) \ .join(DewarModel, DewarModel.id == LogisticsEventModel.dewar_id) \ .filter( DewarModel.unique_id == slot.dewar.unique_id, LogisticsEventModel.event_type == "refill" ) \ .order_by(LogisticsEventModel.timestamp.desc()) \ .first() if last_refill_event: last_refill = last_refill_event.timestamp time_until_refill = calculate_time_until_refill(last_refill) else: time_until_refill = -1 # Get last retrieved timestamp last_retrieved_event = db.query(LogisticsEventModel) \ .join(DewarModel, DewarModel.id == LogisticsEventModel.dewar_id) \ .filter( DewarModel.unique_id == slot.dewar.unique_id, LogisticsEventModel.event_type == "retrieved" ) \ .order_by(LogisticsEventModel.timestamp.desc()) \ .first() if last_retrieved_event: retrievedTimestamp = last_retrieved_event.timestamp.isoformat() retrieved = True # Determine the last event excluding refills last_event = db.query(LogisticsEventModel) \ .join(DewarModel, DewarModel.id == LogisticsEventModel.dewar_id) \ .filter( DewarModel.unique_id == slot.dewar.unique_id, LogisticsEventModel.event_type != "refill" ) \ .order_by(LogisticsEventModel.timestamp.desc()) \ .first() if last_event: associated_slot = db.query(SlotModel).filter(SlotModel.id == last_event.slot_id).first() beamlineLocation = associated_slot.label if associated_slot else None at_beamline = last_event.event_type == "beamline" slot_data = SlotSchema( id=slot.id, qr_code=slot.qr_code, label=slot.label, qr_base=slot.qr_base, occupied=slot.occupied, needs_refill=slot.needs_refill, dewar_unique_id=slot.dewar_unique_id, dewar_name=slot.dewar.dewar_name if slot.dewar else None, time_until_refill=time_until_refill, at_beamline=at_beamline, retrieved=retrieved, retrievedTimestamp=retrievedTimestamp, beamlineLocation=beamlineLocation, ) logger.info(f"Dewar retrieved: {retrieved}") logger.info(f"Dewar at: {beamlineLocation}") slots_with_refill_time.append(slot_data) return slots_with_refill_time @router.post("/dewar/refill", response_model=dict) async def refill_dewar(qr_code: str, db: Session = Depends(get_db)): logger.info(f"Refilling dewar with QR code: {qr_code}") dewar = db.query(DewarModel).filter(DewarModel.unique_id == qr_code.strip()).first() if not dewar: logger.error("Dewar not found") raise HTTPException(status_code=404, detail="Dewar not found") now = datetime.now() new_event = LogisticsEventModel( dewar_id=dewar.id, slot_id=None, event_type="refill", timestamp=now, ) db.add(new_event) db.commit() time_until_refill_seconds = calculate_time_until_refill(now) logger.info(f"Dewar refilled successfully with time_until_refill: {time_until_refill_seconds}") return {"message": "Dewar refilled successfully", "time_until_refill": time_until_refill_seconds} @router.get("/dewars", response_model=List[DewarSchema]) async def get_all_dewars(db: Session = Depends(get_db)): dewars = db.query(DewarModel).all() return dewars @router.get("/dewar/{unique_id}", response_model=DewarSchema) async def get_dewar_by_unique_id(unique_id: str, db: Session = Depends(get_db)): logger.info(f"Received request for dewar with unique_id: {unique_id}") dewar = db.query(DewarModel).filter(DewarModel.unique_id == unique_id.strip()).first() if not dewar: logger.warning(f"Dewar with unique_id '{unique_id}' not found.") raise HTTPException(status_code=404, detail="Dewar not found") logger.info(f"Returning dewar: {dewar}") return dewar def log_event(db: Session, dewar_id: int, slot_id: Optional[int], event_type: str): new_event = LogisticsEventModel( dewar_id=dewar_id, slot_id=slot_id, event_type=event_type, timestamp=datetime.now() ) db.add(new_event) db.commit() logger.info(f"Logged event: {event_type} for dewar: {dewar_id} in slot: {slot_id if slot_id else 'N/A'}")