Skip to content

Commit d715f4c

Browse files
committed
refactor for complexity lint
1 parent d7bd43a commit d715f4c

File tree

5 files changed

+224
-155
lines changed

5 files changed

+224
-155
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
"""Helper to build a search query."""
2+
3+
from __future__ import annotations
4+
from typing import Final, TYPE_CHECKING
5+
6+
import sqlalchemy
7+
8+
from opentrons.protocol_engine import ModuleModel
9+
10+
from robot_server.persistence.tables import (
11+
labware_offset_table,
12+
labware_offset_location_sequence_components_table,
13+
)
14+
from .models import DoNotFilterType, DO_NOT_FILTER
15+
16+
if TYPE_CHECKING:
17+
from typing_extensions import Self
18+
19+
20+
class SearchQueryBuilder:
21+
"""Helper class to build a search query.
22+
23+
This object is stateful, and should be kept around just long enough to have the parameters
24+
of a single search injected.
25+
"""
26+
27+
def __init__(self) -> None:
28+
"""Build the object."""
29+
super().__init__()
30+
self._filter_original: Final = sqlalchemy.select(
31+
labware_offset_table.c.row_id,
32+
labware_offset_table.c.offset_id,
33+
labware_offset_table.c.definition_uri,
34+
labware_offset_table.c.vector_x,
35+
labware_offset_table.c.vector_y,
36+
labware_offset_table.c.vector_z,
37+
labware_offset_table.c.created_at,
38+
labware_offset_table.c.active,
39+
labware_offset_location_sequence_components_table.c.sequence_ordinal,
40+
labware_offset_location_sequence_components_table.c.component_kind,
41+
labware_offset_location_sequence_components_table.c.primary_component_value,
42+
).select_from(
43+
sqlalchemy.join(
44+
labware_offset_table,
45+
labware_offset_location_sequence_components_table,
46+
labware_offset_table.c.row_id
47+
== labware_offset_location_sequence_components_table.c.offset_id,
48+
)
49+
)
50+
self._offset_location_alias: Final = (
51+
labware_offset_location_sequence_components_table.alias()
52+
)
53+
self._current_base_filter_statement = self._filter_original
54+
self._current_positive_location_filter: (
55+
sqlalchemy.sql.selectable.Exists | None
56+
) = None
57+
self._current_negative_filter_subqueries: list[
58+
sqlalchemy.sql.selectable.Exists
59+
] = []
60+
61+
def _positive_query(self) -> sqlalchemy.sql.selectable.Exists:
62+
if self._current_positive_location_filter is not None:
63+
return self._current_positive_location_filter
64+
return sqlalchemy.exists().where(
65+
self._offset_location_alias.c.offset_id
66+
== labware_offset_location_sequence_components_table.c.offset_id
67+
)
68+
69+
def build_query(self) -> sqlalchemy.sql.selectable.Selectable:
70+
"""Render the query into a sqlalchemy object suitable for passing to the database."""
71+
statement = self._current_base_filter_statement
72+
if self._current_positive_location_filter is not None:
73+
statement = statement.where(self._current_positive_location_filter)
74+
for subq in self._current_negative_filter_subqueries:
75+
statement = statement.where(sqlalchemy.not_(subq))
76+
statement = statement.order_by(labware_offset_table.c.row_id).order_by(
77+
labware_offset_location_sequence_components_table.c.sequence_ordinal
78+
)
79+
return statement
80+
81+
def do_active_filter(self, active: bool) -> Self:
82+
"""Filter to only active=active rows."""
83+
self._current_base_filter_statement = self._current_base_filter_statement.where(
84+
labware_offset_table.c.active == True # noqa: E712
85+
)
86+
return self
87+
88+
def do_id_filter(self, id_filter: str | DoNotFilterType) -> Self:
89+
"""Filter to rows with only the given offset ID."""
90+
if id_filter is DO_NOT_FILTER:
91+
return self
92+
93+
self._current_base_filter_statement = self._current_base_filter_statement.where(
94+
labware_offset_table.c.offset_id == id_filter
95+
)
96+
return self
97+
98+
def do_definition_uri_filter(
99+
self, definition_uri_filter: str | DoNotFilterType
100+
) -> Self:
101+
"""Filter to rows of an offset that apply to a definition URI."""
102+
if definition_uri_filter is DO_NOT_FILTER:
103+
return self
104+
self._current_base_filter_statement = self._current_base_filter_statement.where(
105+
labware_offset_table.c.definition_uri == definition_uri_filter
106+
)
107+
return self
108+
109+
def do_on_addressable_area_filter(
110+
self,
111+
addressable_area_filter: str | DoNotFilterType,
112+
) -> Self:
113+
"""Filter to rows of an offset that applies to the given addressable area."""
114+
if addressable_area_filter is DO_NOT_FILTER:
115+
return self
116+
self._current_positive_location_filter = (
117+
self._positive_query()
118+
.where(self._offset_location_alias.c.component_kind == "onAddressableArea")
119+
.where(
120+
self._offset_location_alias.c.primary_component_value
121+
== addressable_area_filter
122+
)
123+
)
124+
return self
125+
126+
def do_on_labware_filter(
127+
self, labware_uri_filter: str | DoNotFilterType | None
128+
) -> Self:
129+
"""Filter to the rows of an offset located on the given labware (or no labware)."""
130+
if labware_uri_filter is DO_NOT_FILTER:
131+
return self
132+
if labware_uri_filter is None:
133+
self._current_negative_filter_subqueries.append(
134+
sqlalchemy.exists()
135+
.where(
136+
self._offset_location_alias.c.offset_id
137+
== labware_offset_location_sequence_components_table.c.offset_id
138+
)
139+
.where(self._offset_location_alias.c.component_kind == "onLabware")
140+
)
141+
return self
142+
self._current_positive_location_filter = (
143+
self._positive_query()
144+
.where(self._offset_location_alias.c.component_kind == "onLabware")
145+
.where(
146+
self._offset_location_alias.c.primary_component_value
147+
== labware_uri_filter
148+
)
149+
)
150+
return self
151+
152+
def do_on_module_filter(
153+
self,
154+
module_model_filter: ModuleModel | DoNotFilterType | None,
155+
) -> Self:
156+
"""Filter to the rows of an offset located on the given module (or no module)."""
157+
if module_model_filter is DO_NOT_FILTER:
158+
return self
159+
if module_model_filter is None:
160+
self._current_negative_filter_subqueries.append(
161+
sqlalchemy.exists()
162+
.where(
163+
self._offset_location_alias.c.offset_id
164+
== labware_offset_location_sequence_components_table.c.offset_id
165+
)
166+
.where(self._offset_location_alias.c.component_kind == "onModule")
167+
)
168+
return self
169+
self._current_positive_location_filter = (
170+
self._positive_query()
171+
.where(self._offset_location_alias.c.component_kind == "onModule")
172+
.where(
173+
self._offset_location_alias.c.primary_component_value
174+
== module_model_filter.value
175+
)
176+
)
177+
return self

robot-server/robot_server/labware_offsets/models.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""Request/response models for the `/labwareOffsets` endpoints."""
22

33
from datetime import datetime
4-
from typing import Literal, Annotated
4+
import enum
5+
from typing import Literal, Annotated, Final, TypeAlias
56

67
from pydantic import BaseModel, Field
78

@@ -14,6 +15,26 @@
1415

1516
from robot_server.errors.error_responses import ErrorDetails
1617

18+
19+
class _DoNotFilter(enum.Enum):
20+
DO_NOT_FILTER = enum.auto()
21+
22+
23+
DO_NOT_FILTER: Final = _DoNotFilter.DO_NOT_FILTER
24+
"""A sentinel value for when a filter should not be applied.
25+
26+
This is different from filtering on `None`, which returns only entries where the
27+
value is equal to `None`.
28+
"""
29+
30+
31+
DoNotFilterType: TypeAlias = Literal[_DoNotFilter.DO_NOT_FILTER]
32+
"""The type of `DO_NOT_FILTER`, as `NoneType` is to `None`.
33+
34+
Unfortunately, mypy doesn't let us write `Literal[DO_NOT_FILTER]`. Use this instead.
35+
"""
36+
37+
1738
# This is redefined here so we can add stuff to it easily
1839
StoredLabwareOffsetLocationSequenceComponents = Annotated[
1940
LabwareOffsetLocationSequenceComponentsUnion, Field(discriminator="kind")

robot-server/robot_server/labware_offsets/router.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,16 @@
2323
)
2424

2525
from .store import (
26-
DO_NOT_FILTER,
27-
DoNotFilterType,
2826
LabwareOffsetNotFoundError,
2927
LabwareOffsetStore,
3028
)
3129
from .fastapi_dependencies import get_labware_offset_store
32-
from .models import StoredLabwareOffset, StoredLabwareOffsetCreate
30+
from .models import (
31+
StoredLabwareOffset,
32+
StoredLabwareOffsetCreate,
33+
DO_NOT_FILTER,
34+
DoNotFilterType,
35+
)
3336

3437

3538
router = LightRouter()

0 commit comments

Comments
 (0)