-
Notifications
You must be signed in to change notification settings - Fork 179
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(robot-server): handle new offset locations in /labwareOffsets (#…
…17388) Adds the new format for labware offset locations from #17363 to the HTTP API `/labwareOffsets` endpoint. The endpoint now accepts and returns labware offset locations as these sequences. The filter API is for now unchanged, and still works properly to filter out or in offsets based on their core features or on their locations. The types are defined separately from the protocol engine because we'll be shortly adding a concept of a "general offset" to this API, and that is best modeled as another heterogenous union entry, which means the types will start to diverge. This comes along with a sql schema migration; the offset locations are normalized out into an offset table with a foreign key into the main offset table. All content from the offset table is migrated to the new one in the standard persistence directory migration api. ## Reviews - does the sql look good? - do the tests look appropriate? some have expanded - do the migrations look good? ## Testing - [x] put this on a machine that has offset data and make sure it migrates and comes up with the data preserved Closes EXEC-1105
- Loading branch information
Showing
16 changed files
with
1,590 additions
and
221 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
177 changes: 177 additions & 0 deletions
177
robot-server/robot_server/labware_offsets/_search_query_builder.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
"""Helper to build a search query.""" | ||
|
||
from __future__ import annotations | ||
from typing import Final, TYPE_CHECKING | ||
|
||
import sqlalchemy | ||
|
||
from opentrons.protocol_engine import ModuleModel | ||
|
||
from robot_server.persistence.tables import ( | ||
labware_offset_table, | ||
labware_offset_location_sequence_components_table, | ||
) | ||
from .models import DoNotFilterType, DO_NOT_FILTER | ||
|
||
if TYPE_CHECKING: | ||
from typing_extensions import Self | ||
|
||
|
||
class SearchQueryBuilder: | ||
"""Helper class to build a search query. | ||
This object is stateful, and should be kept around just long enough to have the parameters | ||
of a single search injected. | ||
""" | ||
|
||
def __init__(self) -> None: | ||
"""Build the object.""" | ||
super().__init__() | ||
self._filter_original: Final = sqlalchemy.select( | ||
labware_offset_table.c.row_id, | ||
labware_offset_table.c.offset_id, | ||
labware_offset_table.c.definition_uri, | ||
labware_offset_table.c.vector_x, | ||
labware_offset_table.c.vector_y, | ||
labware_offset_table.c.vector_z, | ||
labware_offset_table.c.created_at, | ||
labware_offset_table.c.active, | ||
labware_offset_location_sequence_components_table.c.sequence_ordinal, | ||
labware_offset_location_sequence_components_table.c.component_kind, | ||
labware_offset_location_sequence_components_table.c.primary_component_value, | ||
).select_from( | ||
sqlalchemy.join( | ||
labware_offset_table, | ||
labware_offset_location_sequence_components_table, | ||
labware_offset_table.c.row_id | ||
== labware_offset_location_sequence_components_table.c.offset_id, | ||
) | ||
) | ||
self._offset_location_alias: Final = ( | ||
labware_offset_location_sequence_components_table.alias() | ||
) | ||
self._current_base_filter_statement = self._filter_original | ||
self._current_positive_location_filter: ( | ||
sqlalchemy.sql.selectable.Exists | None | ||
) = None | ||
self._current_negative_filter_subqueries: list[ | ||
sqlalchemy.sql.selectable.Exists | ||
] = [] | ||
|
||
def _positive_query(self) -> sqlalchemy.sql.selectable.Exists: | ||
if self._current_positive_location_filter is not None: | ||
return self._current_positive_location_filter | ||
return sqlalchemy.exists().where( | ||
self._offset_location_alias.c.offset_id | ||
== labware_offset_location_sequence_components_table.c.offset_id | ||
) | ||
|
||
def build_query(self) -> sqlalchemy.sql.selectable.Selectable: | ||
"""Render the query into a sqlalchemy object suitable for passing to the database.""" | ||
statement = self._current_base_filter_statement | ||
if self._current_positive_location_filter is not None: | ||
statement = statement.where(self._current_positive_location_filter) | ||
for subq in self._current_negative_filter_subqueries: | ||
statement = statement.where(sqlalchemy.not_(subq)) | ||
statement = statement.order_by(labware_offset_table.c.row_id).order_by( | ||
labware_offset_location_sequence_components_table.c.sequence_ordinal | ||
) | ||
return statement | ||
|
||
def do_active_filter(self, active: bool) -> Self: | ||
"""Filter to only rows that are active (active=True) or inactive (active=False).""" | ||
self._current_base_filter_statement = self._current_base_filter_statement.where( | ||
labware_offset_table.c.active == active | ||
) | ||
return self | ||
|
||
def do_id_filter(self, id_filter: str | DoNotFilterType) -> Self: | ||
"""Filter to rows with only the given offset ID.""" | ||
if id_filter is DO_NOT_FILTER: | ||
return self | ||
|
||
self._current_base_filter_statement = self._current_base_filter_statement.where( | ||
labware_offset_table.c.offset_id == id_filter | ||
) | ||
return self | ||
|
||
def do_definition_uri_filter( | ||
self, definition_uri_filter: str | DoNotFilterType | ||
) -> Self: | ||
"""Filter to rows of an offset that apply to a definition URI.""" | ||
if definition_uri_filter is DO_NOT_FILTER: | ||
return self | ||
self._current_base_filter_statement = self._current_base_filter_statement.where( | ||
labware_offset_table.c.definition_uri == definition_uri_filter | ||
) | ||
return self | ||
|
||
def do_on_addressable_area_filter( | ||
self, | ||
addressable_area_filter: str | DoNotFilterType, | ||
) -> Self: | ||
"""Filter to rows of an offset that applies to the given addressable area.""" | ||
if addressable_area_filter is DO_NOT_FILTER: | ||
return self | ||
self._current_positive_location_filter = ( | ||
self._positive_query() | ||
.where(self._offset_location_alias.c.component_kind == "onAddressableArea") | ||
.where( | ||
self._offset_location_alias.c.primary_component_value | ||
== addressable_area_filter | ||
) | ||
) | ||
return self | ||
|
||
def do_on_labware_filter( | ||
self, labware_uri_filter: str | DoNotFilterType | None | ||
) -> Self: | ||
"""Filter to the rows of an offset located on the given labware (or no labware).""" | ||
if labware_uri_filter is DO_NOT_FILTER: | ||
return self | ||
if labware_uri_filter is None: | ||
self._current_negative_filter_subqueries.append( | ||
sqlalchemy.exists() | ||
.where( | ||
self._offset_location_alias.c.offset_id | ||
== labware_offset_location_sequence_components_table.c.offset_id | ||
) | ||
.where(self._offset_location_alias.c.component_kind == "onLabware") | ||
) | ||
return self | ||
self._current_positive_location_filter = ( | ||
self._positive_query() | ||
.where(self._offset_location_alias.c.component_kind == "onLabware") | ||
.where( | ||
self._offset_location_alias.c.primary_component_value | ||
== labware_uri_filter | ||
) | ||
) | ||
return self | ||
|
||
def do_on_module_filter( | ||
self, | ||
module_model_filter: ModuleModel | DoNotFilterType | None, | ||
) -> Self: | ||
"""Filter to the rows of an offset located on the given module (or no module).""" | ||
if module_model_filter is DO_NOT_FILTER: | ||
return self | ||
if module_model_filter is None: | ||
self._current_negative_filter_subqueries.append( | ||
sqlalchemy.exists() | ||
.where( | ||
self._offset_location_alias.c.offset_id | ||
== labware_offset_location_sequence_components_table.c.offset_id | ||
) | ||
.where(self._offset_location_alias.c.component_kind == "onModule") | ||
) | ||
return self | ||
self._current_positive_location_filter = ( | ||
self._positive_query() | ||
.where(self._offset_location_alias.c.component_kind == "onModule") | ||
.where( | ||
self._offset_location_alias.c.primary_component_value | ||
== module_model_filter.value | ||
) | ||
) | ||
return self |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.