Refactor set_tell_positions logic with updated rules.

Revised the set_tell_positions endpoint to handle updated business rules for puck positioning. Improved event handling to ensure proper nullification, updates, and removal of tell_positions based on the provided payload. Enhanced query performance and normalized puck name processing for consistency.
This commit is contained in:
GotthardG 2025-01-08 15:18:08 +01:00
parent 4f73f41717
commit 6c91bc78da

View File

@ -45,25 +45,34 @@ async def get_pucks(db: Session = Depends(get_db)):
@router.put("/set-tell-positions", status_code=status.HTTP_200_OK)
async def set_tell_positions(
pucks: List[SetTellPosition], # Accept a list of SetTellPosition Pydantic models
pucks: List[SetTellPosition], # Accept a validated Pydantic model
db: Session = Depends(get_db),
):
"""
Set the tell positions for multiple pucks based on the business rules.
Set tell positions for multiple pucks with updated rules.
Args:
pucks (List[SetTellPosition]): A list of puck definitions
with potential new positions:
- `puck_name` (Optional[str]): The name of the puck to update.
- `segment` (Optional[str]): The segment (A-F) in the dewar.
- `puck_in_segment` (Optional[int]): The position within the segment (1-5).
pucks (List[SetTellPosition]): A list including puck_name,
segment, and puck_in_segment.
Rules:
1. If a puck already has a tell_position matching the payload,
it is ignored (no new timestamps).
2. If a puck is assigned a different position,
set the new position while nullifying the previous.
3. Pucks that have a last `tell_position_set` event with
a non-null `tell_position` but
are not in the payload will get
a `"puck_removed"` event nullifying their position.
4. If the last event for a puck is already null
or it has no events, nothing is added or updated.
Returns:
List[dict]: A list of results indicating the status of each puck update.
List[dict]: Status information for processed and ignored pucks.
"""
results = []
# Helper function: Validate and normalize puck names
# Helper function to normalize puck names for database querying
def normalize_puck_name(name: str) -> str:
return str(name).strip().replace(" ", "_").upper()
@ -73,33 +82,37 @@ async def set_tell_positions(
detail="Payload cannot be empty. Provide at least one puck.",
)
# Normalize payload puck names
input_puck_names = {normalize_puck_name(p.puck_name): p for p in pucks}
# Retrieve all pucks in the database
all_pucks_with_last_event = (
db.query(PuckModel, PuckEventModel)
.outerjoin(PuckEventModel, PuckEventModel.puck_id == PuckModel.id)
.filter(PuckEventModel.event_type == "tell_position_set")
.order_by(PuckEventModel.puck_id, PuckEventModel.timestamp.desc())
.all()
)
# Dictionary mapping each puck's ID to its latest event
last_events = {}
for puck, last_event in all_pucks_with_last_event:
if puck.id not in last_events:
last_events[puck.id] = last_event
# Process pucks provided in the payload
for puck_data in pucks:
try:
# Step 1: Extract data from the Pydantic model
puckname = puck_data.puckname
tell_position = (
# Extract data from input
puck_name = puck_data.puck_name
new_position = (
puck_data.tell_position
) # Combines `segment` + `puck_in_segment`
) # Combined from segment + puck_in_segment
if not puckname:
# Step 3: If `puckname` is missing, clear tell positions for ALL pucks
db.query(PuckEventModel).filter(
PuckEventModel.tell_position.isnot(None)
).update({"tell_position": None})
db.commit()
# Add the result for the clean-up case
results.append(
{
"action": "clear",
"message": "Tell positions cleared for all pucks.",
}
)
break # No need to process further since all tell positions are cleared
# Normalize puck name
normalized_name = normalize_puck_name(puck_name)
# Step 2: Normalize puck name for database lookups
normalized_name = normalize_puck_name(puckname)
# Query the puck from the database
# Find puck in the database
puck = (
db.query(PuckModel)
.filter(
@ -110,86 +123,99 @@ async def set_tell_positions(
)
if not puck:
raise ValueError(f"Puck with name '{puckname}' not found.")
raise ValueError(f"Puck with name '{puck_name}' not found.")
# Query the most recent tell_position for this puck
last_tell_position_event = (
db.query(PuckEventModel)
.filter(
PuckEventModel.puck_id == puck.id,
PuckEventModel.tell_position.isnot(None),
# Query the last event for this puck
last_event = last_events.get(puck.id)
# Rule 1: Skip if the last event's tell_position matches the new position
if last_event and last_event.tell_position == new_position:
results.append(
{
"puck_name": puck.puck_name,
"current_position": new_position,
"status": "unchanged",
"message": "No change in tell_position. No event created.",
}
)
.order_by(PuckEventModel.timestamp.desc())
.first()
)
continue
if tell_position:
# Step 4: Compare `tell_position` in the payload with the current one
if (
last_tell_position_event
and last_tell_position_event.tell_position == tell_position
):
# Step 4.1: If positions match, do nothing
results.append(
{
"puck_name": puck.puck_name,
"current_position": tell_position,
"status": "unchanged",
"message": "The tell_position remains the same.",
}
)
continue # Skip to the next puck
# Rule 2: If the last tell_position is not None, nullify it with a
# puck_removed event
if last_event and last_event.tell_position is not None:
remove_event = PuckEventModel(
puck_id=puck.id,
tell_position=None,
event_type="puck_removed", # Set event_type to "puck_removed"
timestamp=datetime.utcnow(),
)
db.add(remove_event)
# Step 4.2: If the position is different, update it
if last_tell_position_event:
# Clear the previous tell_position (set it to null)
last_tell_position_event.tell_position = None
db.add(last_tell_position_event)
# Add a new event with the updated tell_position
# Add a new event with the updated tell_position
if new_position:
new_event = PuckEventModel(
puck_id=puck.id,
tell_position=tell_position,
tell_position=new_position,
event_type="tell_position_set",
timestamp=datetime.utcnow(),
)
db.add(new_event)
results.append(
{
"puck_name": puck.puck_name,
"previous_position": last_tell_position_event.tell_position
if last_tell_position_event
"new_position": new_position,
"previous_position": last_event.tell_position
if last_event
else None,
"new_position": tell_position,
"status": "updated",
"message": "The tell_position was updated successfully.",
}
)
else:
# Step 5: If the new tell_position is None, clear the current one
if last_tell_position_event:
last_tell_position_event.tell_position = None
db.add(last_tell_position_event)
results.append(
{
"puck_name": puck.puck_name,
"previous_position": last_tell_position_event.tell_position
if last_tell_position_event
else None,
"new_position": None,
"status": "cleared",
"message": "The tell_position was cleared.",
}
)
# Commit transaction for updating tell_position
# Commit changes after processing each puck
db.commit()
except Exception as e:
# Handle errors for individual pucks and continue processing others
results.append({"puck_name": puckname, "error": str(e)})
# Handle individual puck errors
results.append({"puck_name": puck_data.puck_name, "error": str(e)})
# Process pucks not included in the payload but present in the DB
for puck_id, last_event in last_events.items():
puck = db.query(PuckModel).filter(PuckModel.id == puck_id).first()
normalized_name = normalize_puck_name(puck.puck_name)
# Check if the puck is missing from the payload
if (
normalized_name not in input_puck_names
and last_event
and last_event.tell_position is not None
):
try:
# Add a puck_removed event
remove_event = PuckEventModel(
puck_id=puck.id,
tell_position=None,
event_type="puck_removed", # Set event_type to "puck_removed"
timestamp=datetime.utcnow(),
)
db.add(remove_event)
# Append to results
results.append(
{
"puck_name": puck.puck_name,
"removed_position": last_event.tell_position,
"status": "removed",
"message": "Puck is not in payload"
" and has been marked as removed from tell_position.",
}
)
db.commit()
except Exception as e:
# Handle errors for individual puck removal
results.append({"puck_name": puck.puck_name, "error": str(e)})
return results