aaredb/backend/app/routers/logistics.py
2024-12-16 22:50:04 +01:00

348 lines
12 KiB
Python

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}"
f"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"
f"associated with slot {original_slot.qr_code}"
)
raise HTTPException(
status_code=400,
detail=f"Dewar {data.dewar_qr_code} is associated"
f"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
# Set the `at_beamline` attribute to False
dewar.at_beamline = False
# Log the event
log_event(db, dewar.id, slot.id, "returned")
db.commit()
logger.info(
f"Dewar {data.dewar_qr_code} successfully"
f"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
# Validate Dewar QR Code
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"
)
# Retrieve the Dewar
dewar = db.query(DewarModel).filter(DewarModel.unique_id == dewar_qr_code).first()
if not dewar:
logger.error("Dewar not found")
raise HTTPException(status_code=404, detail="Dewar not found")
# Check for Outgoing QR Codes and set transaction type
if location_qr_code in ["Outgoing X10-SA", "Outgoing X06-SA"]:
transaction_type = "outgoing"
# Retrieve the Slot associated with the Dewar (for outgoing)
slot = None
if transaction_type == "outgoing":
slot = (
db.query(SlotModel)
.filter(SlotModel.dewar_unique_id == dewar.unique_id)
.first()
)
if not slot:
logger.error(f"No slot associated with dewar for outgoing: {dewar_qr_code}")
raise HTTPException(
status_code=404, detail="No slot associated with dewar for outgoing"
)
# Incoming Logic
if transaction_type == "incoming":
slot = db.query(SlotModel).filter(SlotModel.qr_code == location_qr_code).first()
if not slot or slot.occupied:
logger.error(f"Slot not found or already occupied: {location_qr_code}")
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.occupied or slot.dewar_unique_id != dewar.unique_id:
logger.error(f"Slot not valid for outgoing: {location_qr_code}")
raise HTTPException(
status_code=400,
detail="Dewar not associated with the slot for outgoing",
)
slot.dewar_unique_id = None
slot.occupied = False
elif transaction_type == "beamline":
slot = db.query(SlotModel).filter(SlotModel.qr_code == location_qr_code).first()
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 the event
log_event(db, dewar.id, slot.id if slot else None, transaction_type)
db.commit()
logger.info(
f"Transaction completed: {transaction_type}"
f"for dewar {dewar_qr_code} in slot {slot.qr_code if slot else 'N/A'}"
)
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:
# Initialize variables for slot-related data
time_until_refill = None
retrievedTimestamp = None
beamlineLocation = None
at_beamline = 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
# Fetch the latest event for the dewar
last_event = (
db.query(LogisticsEventModel)
.join(DewarModel, DewarModel.id == LogisticsEventModel.dewar_id)
.filter(DewarModel.unique_id == slot.dewar.unique_id)
.order_by(LogisticsEventModel.timestamp.desc())
.first()
)
# Determine if the dewar is at the beamline
if last_event:
if last_event.event_type == "beamline":
at_beamline = True
# Optionally set retrievedTimestamp and beamlineLocation for
# beamline events
retrievedTimestamp = last_event.timestamp.isoformat()
associated_slot = (
db.query(SlotModel)
.filter(SlotModel.id == last_event.slot_id)
.first()
)
beamlineLocation = (
associated_slot.label if associated_slot else None
)
elif last_event.event_type == "returned":
at_beamline = False
# Correct the contact_person assignment
contact_person = None
if slot.dewar and slot.dewar.contact_person:
first_name = slot.dewar.contact_person.firstname
last_name = slot.dewar.contact_person.lastname
contact_person = f"{first_name} {last_name}"
# Prepare the slot data for the response
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,
retrievedTimestamp=retrievedTimestamp,
beamlineLocation=beamlineLocation,
shipment_name=(
slot.dewar.shipment.shipment_name
if slot.dewar and slot.dewar.shipment
else None
),
contact_person=contact_person,
local_contact="local contact placeholder",
)
# Add updated slot data to the response list
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"
f"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} "
f"in slot: {slot_id if slot_id else 'N/A'}"
)