-
Notifications
You must be signed in to change notification settings - Fork 176
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
refactor(calibration-storage): Use pydantic models for data validation, add OT-3 pydantic models #11520
Conversation
Codecov Report
@@ Coverage Diff @@
## edge #11520 +/- ##
===========================================
- Coverage 74.58% 64.49% -10.10%
===========================================
Files 2074 1416 -658
Lines 57827 24790 -33037
Branches 6104 5990 -114
===========================================
- Hits 43133 15988 -27145
+ Misses 13265 7381 -5884
+ Partials 1429 1421 -8
Flags with carried forward coverage won't be shown. Click here to find out more. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some smaller things in code review, but one bigger one: I don't really like the use of "Schema" in pydantic model names. They're schemas, but they're also instantiated with data and passed around as data container objects. It's weird to have a function that modifies the data of a schema, it's a mismatch of kind: if you have a function that operates on a schema, i expect the schema to be different afterwards - but here, it's just the data in an instance. We should either use these objects only to verify data but not store it, or call them Model
s or something
api/src/opentrons/calibration_storage/ot2/mark_bad_calibration.py
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Many of the added tests assert on implementation details (private methods / fields) of the code under test, which is inappropriate and does not actually test that the public API works. Code and/or tests should structured such that behavior can be validated against public, used-in-production methods
@@ -52,7 +72,7 @@ def read_cal_file( | |||
|
|||
def save_to_file( | |||
filepath: StrPath, | |||
data: typing.Mapping[str, typing.Any], | |||
data: typing.Union[BaseModel, typing.Dict[str, typing.Any], typing.Any], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That last typing.Any
effectively removes any type constraints on this argument. Why is it needed?
data: typing.Union[BaseModel, typing.Dict[str, typing.Any], typing.Any], | |
data: typing.Union[BaseModel, typing.Dict[str, typing.Any]], |
if file.name == "deck_calibration.json": | ||
try: | ||
return v1.DeckCalibrationSchema(**io.read_cal_file(file.path)) | ||
except (json.JSONDecodeError, ValidationError): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does this mean? Should it be logged?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure I'll throw in a log. It effectively means the data is bad.
I want to get this merged ASAP because it's blocking some other calibration work. Any other clean-ups requested (aside from anything found during testing) will be documented here and addressed after our 'feature freeze'. |
…chemas for all calibration data We are now using pydantic models to store and modify calibration data. Most OT-3 calibration data no longer requires tips to calibrate with, so the schemas reflect this fact. re #RLIQ-118
…are stored We should really keep the dataclasses in hardware controller -- as that's the only place they are used. While here, also separated out the concept of 'robot' calibration and 'instrument' calibration.
…ok-up tip length calibration
…ns to calibration storage
… storage module g
… calibration storage
…ble names 'get', 'delete' etc can be rather confusing when using the calibration_storage module. Instead, we should name each file based on the calibration data they handle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me! Would love to remove conditional imports at some point but not this PR.
) | ||
from .ot2.pipette_offset import get_all_pipette_offset_calibrations | ||
|
||
if config.feature_flags.enable_ot3_hardware_controller(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is fine for now but can we add a todo to make this something that is not based on a feature flag and not conditionally imported at some point? I'd love for fewer decisions to get made during import, and having our code support both machines at runtime is an important thing for offline analysis and general sanity
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. If we do need to check a feature flag, it should be done at function run time, not import time. This global config
singleton that needs to be initialized in order for imports to work is an antipattern in our codebase
If it's too much for this PR, that's fine, but I was imagining a lightweight facade in my earlier review:
def save_robot_deck_attitude(...) -> ...:
if is_ot3:
from .ot3 import deck_attitude as ot3_deck_attitude
ot3_deck_attitude.save_robot_deck_attitude(...)
else:
...
ffe4bef
to
0a1c8f2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm ok to move forward with this PR and address additional items in a follow up PR for speed, but in order of importance, I'm still concerned about these issues:
- Conditional imports/exports at the top level of the
calibration_storage
module should be avoided at all costs - Tests for
calibration_storage
are written withif/else
, which seems to suggest that a test suite run will only check OT-2 or OT-3, rather than OT-2 and OT-3 - File/directory reading/writing concerns still leak out of
file_operators.py
- Since
file_operators.py
has test coverage, tests for othercalibration_storage
components could mock outfile_operators
for speed and useful design feedback, but they do not- Tests will be slower because there will be a lot of hitting the filesystem
- We have redundant test coverage of file operations
) | ||
from .ot2.pipette_offset import get_all_pipette_offset_calibrations | ||
|
||
if config.feature_flags.enable_ot3_hardware_controller(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. If we do need to check a feature flag, it should be done at function run time, not import time. This global config
singleton that needs to be initialized in order for imports to work is an antipattern in our codebase
If it's too much for this PR, that's fine, but I was imagining a lightweight facade in my earlier review:
def save_robot_deck_attitude(...) -> ...:
if is_ot3:
from .ot3 import deck_attitude as ot3_deck_attitude
ot3_deck_attitude.save_robot_deck_attitude(...)
else:
...
""" | ||
pipette_calibration_dir = Path(config.get_opentrons_path("pipette_calibration_dir")) | ||
pipette_calibration_list = [] | ||
for filepath in pipette_calibration_dir.glob("**/*.json"): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would recommend moving to a io.get_json_files_from_directory
or something similar for testability
with open(custom_tiprack_path, "rb") as f: | ||
return typing.cast( | ||
"LabwareDefinition", | ||
json.loads(f.read().decode("utf-8")), | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be going through io.read_json_file
or similar?
@pytest.fixture(autouse=True) | ||
def reload_module(robot_model: "RobotModel") -> None: | ||
importlib.reload(opentrons.calibration_storage) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a warning sign, is it due to all the conditional importing and re-exporting? An explicit facade would avoid this problem.
importlib.reload
, in my experience, is not terribly reliable or intuitive
) | ||
|
||
assert get_robot_deck_attitude() is None | ||
if robot_model == "OT-3 Standard": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if/else
in tests should almost always be avoided. Can this be parametrized, instead?
|
||
|
||
@pytest.fixture | ||
def starting_calibration_data( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we write these tests as isolated unit tests and mock out file_operators
instead, given that file_operators
has unit tests?
Will hopefully get to all of this next week! |
Overview
Closes RLIQ-118. The objective of this PR is to add pydantic models to support better validation on calibration data in general. Please check out this confluence doc for more information on the pydantic schemas and this confluence doc for more information on what the file system looks like in general. There are a lot of loose ends around calibration data that need to be tied up in separate PRs of which I've documented in this epic.
In-memory caching for calibration data will be in a follow-up PR. Mike's comments from here have been addressed in this new branch aside from file access asynchrony which will be addressed in a separate ticket. Apologies for the commit wonky-ness. I couldn't get the branches to rebase properly.
Changelog
Review requests
Please test this on a robot, let me know if you have any changes you want to the actual data shape as well.
Test Plan
Please view this confluence document for a more detailed test plan and check list.
calibration/pipette_offset
andcalibation/tip_length
) still work as expectedRisk assessment
High. This touches a lot of flakey code and needs a lot of QA eyes. See test plan above.