GotthardG da5dbb9e31 Refactor puck handling and update tell position setting
Simplify and standardize puck name normalization and validation. Refactor `set_tell_positions` to handle batch operations, improve error handling, and provide detailed responses for each puck. Removed unnecessary `with-tell-position` endpoint for better clarity and maintainability.
2024-12-19 15:16:21 +01:00

359 lines
12 KiB
Python

from datetime import datetime
from fastapi import APIRouter, HTTPException, status, Depends
from sqlalchemy.orm import Session
from sqlalchemy.sql import func
from typing import List
import uuid
import re
from app.schemas import (
Puck as PuckSchema,
PuckCreate,
PuckUpdate,
)
from app.models import (
Puck as PuckModel,
PuckEvent as PuckEventModel,
Slot as SlotModel,
LogisticsEvent as LogisticsEventModel,
Dewar as DewarModel,
)
from app.dependencies import get_db
import logging
router = APIRouter()
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def normalize_puck_name(name: str) -> str:
"""
Normalize a puck_name to remove special characters and ensure consistent formatting.
"""
name = str(name).strip().replace(" ", "_").upper()
name = re.sub(r"[^A-Z0-9]", "", name) # Remove special characters
return name
@router.get("/", response_model=List[PuckSchema])
async def get_pucks(db: Session = Depends(get_db)):
return db.query(PuckModel).all()
@router.put("/set-tell-positions", status_code=status.HTTP_200_OK)
async def set_tell_positions(
pucks: List[dict], # Accept a list of puck definitions from the client
db: Session = Depends(get_db),
):
"""
Set the tell positions for multiple pucks.
Args:
pucks (List[dict]): A list of puck definitions, where each puck contains:
- `puck_name` (str): The cleaned name of the puck.
- `segment` (str): The segment in the dewar (e.g., "A-F").
- `puck_in_segment` (int): The position within the segment (1-5).
Returns:
List[dict]: A list of responses, one for each successfully processed puck.
"""
results = []
for puck_data in pucks:
try:
# Extract data from input
puck_name = puck_data.get("puckname")
segment = puck_data.get("segment")
puck_in_segment = puck_data.get("puck_in_segment")
# 1. Validate `segment` and `puck_in_segment`
if not segment or segment not in "ABCDEF":
raise ValueError(
f"Invalid segment '{segment}'. Must be A, B, C, D, E, or F."
)
if not (1 <= puck_in_segment <= 5):
raise ValueError(
f"Invalid puck_in_segment "
f"'{puck_in_segment}'. Must be in range 1-5."
)
# Generate tell_position
tell_position = f"{segment}{puck_in_segment}"
# 2. Find the puck by its cleaned name
normalized_name = normalize_puck_name(puck_name)
# Use SQLAlchemy to match normalized names
puck = (
db.query(PuckModel)
.filter(
func.replace(func.upper(PuckModel.puck_name), "-", "")
== normalized_name
)
.first()
)
if not puck:
raise ValueError(
f"Puck with cleaned name '{puck_name}' not found in the database."
)
# 3. Find the dewar associated with the puck
dewar = db.query(DewarModel).filter(DewarModel.id == puck.dewar_id).first()
if not dewar:
raise ValueError(f"Dewar associated with puck '{puck_name}' not found.")
# 4. Find the most recent logistics event for the dewar (type 'beamline')
logistics_event = (
db.query(LogisticsEventModel)
.filter(
LogisticsEventModel.dewar_id == dewar.id,
LogisticsEventModel.event_type == "beamline",
)
.order_by(LogisticsEventModel.timestamp.desc())
.first()
)
if not logistics_event:
raise ValueError(
f"No recent 'beamline' logistics event found for dewar "
f"'{dewar.dewar_name}'."
)
# 5. Retrieve the slot from the logistics event
slot = (
db.query(SlotModel)
.filter(SlotModel.id == logistics_event.slot_id)
.first()
)
if not slot:
raise ValueError(
f"No slot associated with the most recent 'beamline' "
f"logistics event "
f"for dewar '{dewar.dewar_name}'."
)
# 6. Create a PuckEvent to set the 'tell_position'
new_puck_event = PuckEventModel(
puck_id=puck.id,
tell_position=tell_position,
event_type="tell_position_set",
timestamp=datetime.utcnow(),
)
db.add(new_puck_event)
db.commit()
db.refresh(new_puck_event)
# Add success result to the results list
results.append(
{
"puck_id": puck.id,
"puck_name": puck.puck_name,
"dewar_id": dewar.id,
"dewar_name": dewar.dewar_name,
"slot_id": slot.id,
"slot_label": slot.label,
"tell_position": tell_position,
"event_timestamp": new_puck_event.timestamp,
}
)
except Exception as e:
# Handle errors for individual pucks and continue processing others
results.append(
{
"puck_name": puck_name,
"error": str(e),
}
)
return results
@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.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