Skip to content

Commit 443a8df

Browse files
committed
fixup: do not crash when encountering an bad loc
This will allow us to add new kinds of location without doing schema changes and migrations. If you add a location and downgrade to a version that doesn't understand it, you'll get an "UnknownLocationSequenceComponent", just like a bad run.
1 parent 7df44a2 commit 443a8df

File tree

4 files changed

+167
-34
lines changed

4 files changed

+167
-34
lines changed

robot-server/robot_server/labware_offsets/models.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from datetime import datetime
44
import enum
5-
from typing import Literal, Annotated, Final, TypeAlias
5+
from typing import Literal, Annotated, Final, TypeAlias, Sequence
66

77
from pydantic import BaseModel, Field
88

@@ -35,18 +35,33 @@ class _DoNotFilter(enum.Enum):
3535
"""
3636

3737

38+
class UnknownLabwareOffsetLocationSequenceComponent(BaseModel):
39+
"""A labware offset location sequence component from the future."""
40+
41+
kind: Literal["unknown"] = "unknown"
42+
storedKind: str
43+
primaryValue: str
44+
45+
3846
# This is redefined here so we can add stuff to it easily
3947
StoredLabwareOffsetLocationSequenceComponents = Annotated[
4048
LabwareOffsetLocationSequenceComponentsUnion, Field(discriminator="kind")
4149
]
4250

4351

52+
ReturnedLabwareOffsetLocationSequenceComponents = Annotated[
53+
LabwareOffsetLocationSequenceComponentsUnion
54+
| UnknownLabwareOffsetLocationSequenceComponent,
55+
Field(discriminator="kind"),
56+
]
57+
58+
4459
class StoredLabwareOffsetCreate(BaseModel):
4560
"""Create an offset for storage."""
4661

4762
definitionUri: str = Field(..., description="The URI for the labware's definition.")
4863

49-
locationSequence: list[StoredLabwareOffsetLocationSequenceComponents] = Field(
64+
locationSequence: Sequence[StoredLabwareOffsetLocationSequenceComponents] = Field(
5065
...,
5166
description="Where the labware is located on the robot. Can represent all locations, but may not be present for older runs.",
5267
min_length=1,
@@ -67,7 +82,7 @@ class StoredLabwareOffset(BaseModel):
6782
createdAt: datetime = Field(..., description="When this labware offset was added.")
6883
definitionUri: str = Field(..., description="The URI for the labware's definition.")
6984

70-
locationSequence: list[StoredLabwareOffsetLocationSequenceComponents] = Field(
85+
locationSequence: Sequence[ReturnedLabwareOffsetLocationSequenceComponents] = Field(
7186
...,
7287
description="Where the labware is located on the robot. Can represent all locations, but may not be present for older runs.",
7388
min_length=1,

robot-server/robot_server/labware_offsets/router.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from .store import (
2626
LabwareOffsetNotFoundError,
2727
LabwareOffsetStore,
28+
IncomingStoredLabwareOffset,
2829
)
2930
from .fastapi_dependencies import get_labware_offset_store
3031
from .models import (
@@ -58,7 +59,7 @@ async def post_labware_offset( # noqa: D103
5859
new_offset_created_at: Annotated[datetime, fastapi.Depends(get_current_time)],
5960
request_body: Annotated[RequestModel[StoredLabwareOffsetCreate], fastapi.Body()],
6061
) -> PydanticResponse[SimpleBody[StoredLabwareOffset]]:
61-
new_offset = StoredLabwareOffset.model_construct(
62+
new_offset = IncomingStoredLabwareOffset(
6263
id=new_offset_id,
6364
createdAt=new_offset_created_at,
6465
definitionUri=request_body.data.definitionUri,
@@ -67,7 +68,15 @@ async def post_labware_offset( # noqa: D103
6768
)
6869
store.add(new_offset)
6970
return await PydanticResponse.create(
70-
content=SimpleBody.model_construct(data=new_offset),
71+
content=SimpleBody.model_construct(
72+
data=StoredLabwareOffset(
73+
id=new_offset_id,
74+
createdAt=new_offset_created_at,
75+
definitionUri=request_body.data.definitionUri,
76+
locationSequence=request_body.data.locationSequence,
77+
vector=request_body.data.vector,
78+
)
79+
),
7180
status_code=201,
7281
)
7382

robot-server/robot_server/labware_offsets/store.py

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# noqa: D100
22

3-
from typing import Iterator
3+
from datetime import datetime
4+
from dataclasses import dataclass
5+
from typing import Iterator, Sequence
46
from typing_extensions import assert_never
57

68
from opentrons.protocol_engine.types import (
@@ -9,21 +11,41 @@
911
OnAddressableAreaOffsetLocationSequenceComponent,
1012
OnModuleOffsetLocationSequenceComponent,
1113
OnLabwareOffsetLocationSequenceComponent,
12-
LabwareOffsetLocationSequenceComponents,
13-
LabwareOffsetLocationSequence,
1414
)
1515

1616
from robot_server.persistence.tables import (
1717
labware_offset_table,
1818
labware_offset_location_sequence_components_table,
1919
)
20-
from .models import StoredLabwareOffset, DoNotFilterType, DO_NOT_FILTER
20+
from .models import (
21+
StoredLabwareOffset,
22+
DoNotFilterType,
23+
DO_NOT_FILTER,
24+
StoredLabwareOffsetLocationSequenceComponents,
25+
ReturnedLabwareOffsetLocationSequenceComponents,
26+
UnknownLabwareOffsetLocationSequenceComponent,
27+
)
2128

2229
import sqlalchemy
2330
import sqlalchemy.exc
2431

2532
from ._search_query_builder import SearchQueryBuilder
2633

34+
ReturnedLabwareOffsetLocationSequence = Sequence[
35+
ReturnedLabwareOffsetLocationSequenceComponents
36+
]
37+
38+
39+
@dataclass
40+
class IncomingStoredLabwareOffset:
41+
"""Internal class for representing valid incoming offsets."""
42+
43+
id: str
44+
createdAt: datetime
45+
definitionUri: str
46+
locationSequence: Sequence[StoredLabwareOffsetLocationSequenceComponents]
47+
vector: LabwareOffsetVector
48+
2749

2850
class LabwareOffsetStore:
2951
"""A persistent store for labware offsets, to support the `/labwareOffsets` endpoints."""
@@ -37,7 +59,10 @@ def __init__(self, sql_engine: sqlalchemy.engine.Engine) -> None:
3759
"""
3860
self._sql_engine = sql_engine
3961

40-
def add(self, offset: StoredLabwareOffset) -> None:
62+
def add(
63+
self,
64+
offset: IncomingStoredLabwareOffset,
65+
) -> None:
4166
"""Store a new labware offset."""
4267
with self._sql_engine.begin() as transaction:
4368
offset_row_id = transaction.execute(
@@ -131,7 +156,7 @@ def __init__(self, bad_offset_id: str) -> None:
131156

132157
def _sql_sequence_component_to_pydantic_sequence_component(
133158
component_row: sqlalchemy.engine.Row,
134-
) -> LabwareOffsetLocationSequenceComponents:
159+
) -> ReturnedLabwareOffsetLocationSequenceComponents:
135160
if component_row.component_kind == "onLabware":
136161
return OnLabwareOffsetLocationSequenceComponent(
137162
labwareUri=component_row.primary_component_value
@@ -145,14 +170,19 @@ def _sql_sequence_component_to_pydantic_sequence_component(
145170
addressableAreaName=component_row.primary_component_value
146171
)
147172
else:
148-
raise KeyError(component_row.component_kind)
173+
return UnknownLabwareOffsetLocationSequenceComponent(
174+
storedKind=component_row.component_kind,
175+
primaryValue=component_row.primary_component_value,
176+
)
149177

150178

151179
def _collate_sql_locations(
152180
first_row: sqlalchemy.engine.Row, rest_rows: Iterator[sqlalchemy.engine.Row]
153-
) -> tuple[LabwareOffsetLocationSequence, sqlalchemy.engine.Row | None]:
181+
) -> tuple[
182+
list[ReturnedLabwareOffsetLocationSequenceComponents], sqlalchemy.engine.Row | None
183+
]:
154184
offset_id = first_row.offset_id
155-
location_sequence: list[LabwareOffsetLocationSequenceComponents] = [
185+
location_sequence: list[ReturnedLabwareOffsetLocationSequenceComponents] = [
156186
_sql_sequence_component_to_pydantic_sequence_component(first_row)
157187
]
158188
while True:
@@ -197,7 +227,9 @@ def _collate_sql_to_pydantic(
197227
yield result
198228

199229

200-
def _pydantic_to_sql_offset(labware_offset: StoredLabwareOffset) -> dict[str, object]:
230+
def _pydantic_to_sql_offset(
231+
labware_offset: IncomingStoredLabwareOffset,
232+
) -> dict[str, object]:
201233
return dict(
202234
offset_id=labware_offset.id,
203235
definition_uri=labware_offset.definitionUri,
@@ -210,7 +242,7 @@ def _pydantic_to_sql_offset(labware_offset: StoredLabwareOffset) -> dict[str, ob
210242

211243

212244
def _pydantic_to_sql_location_sequence_iterator(
213-
labware_offset: StoredLabwareOffset, offset_row_id: int
245+
labware_offset: IncomingStoredLabwareOffset, offset_row_id: int
214246
) -> Iterator[dict[str, object]]:
215247
for index, component in enumerate(labware_offset.locationSequence):
216248
if isinstance(component, OnLabwareOffsetLocationSequenceComponent):

0 commit comments

Comments
 (0)