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.
This commit is contained in:
GotthardG 2024-12-19 15:16:21 +01:00
parent d1bc70665f
commit da5dbb9e31
3 changed files with 121 additions and 147 deletions

View File

@ -291,7 +291,7 @@ shipments = [
pucks = [ pucks = [
Puck( Puck(
id=1, id=1,
puck_name="PUCK001", puck_name="PUCK-001",
puck_type="Unipuck", puck_type="Unipuck",
puck_location_in_dewar=1, puck_location_in_dewar=1,
dewar_id=1, dewar_id=1,

View File

@ -1,7 +1,10 @@
from datetime import datetime
from fastapi import APIRouter, HTTPException, status, Depends from fastapi import APIRouter, HTTPException, status, Depends
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy.sql import func
from typing import List from typing import List
import uuid import uuid
import re
from app.schemas import ( from app.schemas import (
Puck as PuckSchema, Puck as PuckSchema,
PuckCreate, PuckCreate,
@ -9,7 +12,6 @@ from app.schemas import (
) )
from app.models import ( from app.models import (
Puck as PuckModel, Puck as PuckModel,
Sample as SampleModel,
PuckEvent as PuckEventModel, PuckEvent as PuckEventModel,
Slot as SlotModel, Slot as SlotModel,
LogisticsEvent as LogisticsEventModel, LogisticsEvent as LogisticsEventModel,
@ -24,122 +26,84 @@ logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) 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]) @router.get("/", response_model=List[PuckSchema])
async def get_pucks(db: Session = Depends(get_db)): async def get_pucks(db: Session = Depends(get_db)):
return db.query(PuckModel).all() 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.put("/set-tell-positions", status_code=status.HTTP_200_OK) @router.put("/set-tell-positions", status_code=status.HTTP_200_OK)
async def set_tell_positions( async def set_tell_positions(
puck_name: str, pucks: List[dict], # Accept a list of puck definitions from the client
segment: str,
puck_in_segment: int,
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
Set the tell position for a puck based on the last beamline event. Set the tell positions for multiple pucks.
- Validates `puck_name`, `segment`, and `puck_in_segment`.
- Finds the most recent logistics event of type "beamline" for the associated dewar.
- Ensures the logistics event is associated with a valid slot.
- Creates a new puck event with the specified `tell_position`.
Args: Args:
puck_name (str): Name of the puck to set the tell position for. pucks (List[dict]): A list of puck definitions, where each puck contains:
segment (str): Segment label (A-F). - `puck_name` (str): The cleaned name of the puck.
puck_in_segment (int): Position in the segment (1-5). - `segment` (str): The segment in the dewar (e.g., "A-F").
- `puck_in_segment` (int): The position within the segment (1-5).
Returns: Returns:
JSON response containing puck and associated information. List[dict]: A list of responses, one for each successfully processed puck.
""" """
results = []
from datetime import datetime 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` # 1. Validate `segment` and `puck_in_segment`
if not segment or segment not in "ABCDEF": if not segment or segment not in "ABCDEF":
raise HTTPException( raise ValueError(
status_code=status.HTTP_400_BAD_REQUEST, f"Invalid segment '{segment}'. Must be A, B, C, D, E, or F."
detail="Invalid segment. Valid segments are A, B, C, D, E, F.",
) )
if not (1 <= puck_in_segment <= 5): if not (1 <= puck_in_segment <= 5):
raise HTTPException( raise ValueError(
status_code=status.HTTP_400_BAD_REQUEST, f"Invalid puck_in_segment "
detail="Invalid puck_in_segment. Valid positions are 1 to 5.", f"'{puck_in_segment}'. Must be in range 1-5."
) )
# Generate tell_position # Generate tell_position
tell_position = f"{segment}{puck_in_segment}" tell_position = f"{segment}{puck_in_segment}"
# 2. Find the puck by its name and ensure it exists # 2. Find the puck by its cleaned name
puck = db.query(PuckModel).filter(PuckModel.puck_name == puck_name).first() normalized_name = normalize_puck_name(puck_name)
if not puck:
print(f"DEBUG: Puck '{puck_name}' not found in the database.") # Use SQLAlchemy to match normalized names
raise HTTPException( puck = (
status_code=status.HTTP_404_NOT_FOUND, db.query(PuckModel)
# This should match the error returned .filter(
detail=f"Puck with name '{puck_name}' not found.", 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."
) )
print(f"DEBUG: Found puck: {puck}")
# 3. Find the dewar associated with the puck # 3. Find the dewar associated with the puck
dewar = db.query(DewarModel).filter(DewarModel.id == puck.dewar_id).first() dewar = db.query(DewarModel).filter(DewarModel.id == puck.dewar_id).first()
if not dewar: if not dewar:
raise HTTPException( raise ValueError(f"Dewar associated with puck '{puck_name}' not found.")
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Dewar associated with puck '{puck_name}' not found.",
)
# 4. Find the most recent logistics event for the dewar (type 'beamline') # 4. Find the most recent logistics event for the dewar (type 'beamline')
# and ensure not null
logistics_event = ( logistics_event = (
db.query(LogisticsEventModel) db.query(LogisticsEventModel)
.filter( .filter(
@ -150,27 +114,25 @@ async def set_tell_positions(
.first() .first()
) )
if not logistics_event: if not logistics_event:
raise HTTPException( raise ValueError(
status_code=status.HTTP_404_NOT_FOUND, f"No recent 'beamline' logistics event found for dewar "
detail=( f"'{dewar.dewar_name}'."
f"No recent 'beamline' logistics event found for dewar '"
f"{dewar.dewar_name}' "
f"(puck '{puck_name}')."
),
) )
# 5. Retrieve the slot from the logistics event # 5. Retrieve the slot from the logistics event
slot = db.query(SlotModel).filter(SlotModel.id == logistics_event.slot_id).first() slot = (
db.query(SlotModel)
.filter(SlotModel.id == logistics_event.slot_id)
.first()
)
if not slot: if not slot:
raise HTTPException( raise ValueError(
status_code=status.HTTP_404_NOT_FOUND, f"No slot associated with the most recent 'beamline' "
detail=( f"logistics event "
f"No slot associated with the most recent 'beamline' logistics event "
f"for dewar '{dewar.dewar_name}'." f"for dewar '{dewar.dewar_name}'."
),
) )
# 6. Set the tell position for the puck by creating a PuckEvent # 6. Create a PuckEvent to set the 'tell_position'
new_puck_event = PuckEventModel( new_puck_event = PuckEventModel(
puck_id=puck.id, puck_id=puck.id,
tell_position=tell_position, tell_position=tell_position,
@ -181,10 +143,11 @@ async def set_tell_positions(
db.commit() db.commit()
db.refresh(new_puck_event) db.refresh(new_puck_event)
# 7. Return the result # Add success result to the results list
return { results.append(
{
"puck_id": puck.id, "puck_id": puck.id,
"puck_name": puck_name, "puck_name": puck.puck_name,
"dewar_id": dewar.id, "dewar_id": dewar.id,
"dewar_name": dewar.dewar_name, "dewar_name": dewar.dewar_name,
"slot_id": slot.id, "slot_id": slot.id,
@ -192,6 +155,17 @@ async def set_tell_positions(
"tell_position": tell_position, "tell_position": tell_position,
"event_timestamp": new_puck_event.timestamp, "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) @router.get("/{puck_id}", response_model=PuckSchema)

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "aareDB" name = "aareDB"
version = "0.1.0a8" version = "0.1.0a9"
description = "Backend for next gen sample management system" description = "Backend for next gen sample management system"
authors = [{name = "Guillaume Gotthard", email = "guillaume.gotthard@psi.ch"}] authors = [{name = "Guillaume Gotthard", email = "guillaume.gotthard@psi.ch"}]
license = {text = "MIT"} license = {text = "MIT"}