Skip to content
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

chore: Migrate Python projects from Pydantic v1 to v2 #14871

Draft
wants to merge 75 commits into
base: edge
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
c2d17fb
update pydantic & fix all mypy errors
ahiuchingau Apr 4, 2024
cae6028
hardware: update pydantic & fix all mypy errors
ahiuchingau Apr 4, 2024
8779c87
hardware: fix Pipfile
ahiuchingau Apr 4, 2024
19f7110
api: update pydantic & fix mypy errors
ahiuchingau Apr 4, 2024
b5c8eab
shared-data: add Vec3f
ahiuchingau Apr 5, 2024
ac77352
shared-data: rename labware_definition to models
ahiuchingau Apr 5, 2024
ef889d4
fix api mypy test errors
ahiuchingau Apr 9, 2024
b3ea918
update api
ahiuchingau Apr 9, 2024
ea9e17e
fix protocol_engine tests
ahiuchingau Apr 9, 2024
90b7fab
fix(api): tests
ahiuchingau Apr 9, 2024
16b69eb
update command schema 8.json
ahiuchingau Apr 9, 2024
528a825
shared-data: deprecate json_encoder and use v2 serializer
ahiuchingau Apr 10, 2024
ff83a57
fix api tests
ahiuchingau Apr 10, 2024
80a7000
api(src): update deprecated code
ahiuchingau Apr 10, 2024
1291aa1
api: linter
ahiuchingau Apr 11, 2024
27bbbec
shared-data: remove deprecated code
ahiuchingau Apr 11, 2024
b646339
shared-data: mypy.ini removed commented out override
ahiuchingau Apr 11, 2024
7588a3f
robot-server & server-utils: update pipenv
ahiuchingau Apr 11, 2024
0b391da
robot-server: add pydantic_settings
ahiuchingau Apr 11, 2024
8114936
robot-server: bump-pydantic to v2
ahiuchingau Apr 11, 2024
52b5899
robot-server: fix lint errors
ahiuchingau Apr 12, 2024
9b02831
robot-server: fix one test
ahiuchingau Apr 12, 2024
2728cca
make lint
ahiuchingau Apr 12, 2024
c47cb10
add model serializer
ahiuchingau Apr 19, 2024
82d5a51
Merge remote-tracking branch 'origin/edge' into chore_update-pydantic-v2
SyntaxColoring May 22, 2024
96b4591
Post-merge code fixups.
SyntaxColoring May 22, 2024
68fa893
Re-lock performance-metrics.
SyntaxColoring May 22, 2024
86623ea
Use == version spec for robot-server, like we had before.
SyntaxColoring May 23, 2024
eac19cb
Remove outdated comment.
SyntaxColoring May 23, 2024
7e1da8d
Update g-code-testing Pipfile.
SyntaxColoring May 23, 2024
8ed4c1e
Update system-server Pipfile.
SyntaxColoring May 23, 2024
913019f
Move performance-metrics from api [dev-packages] to [packages].
SyntaxColoring May 23, 2024
2938afb
Re-lock api.
SyntaxColoring May 23, 2024
2b7d068
_TestCommand.result and ReloadLabware.result need to default to None.
SyntaxColoring May 23, 2024
ac00d02
Revert SyncClient model_validate() changes.
SyntaxColoring May 23, 2024
10aa798
Remove now-unneeded type-ignores.
SyntaxColoring May 23, 2024
fef7108
Don't instantiate BaseModel directly.
SyntaxColoring May 23, 2024
a7a69d7
Merge branch 'chore_update-pydantic-v2' of github.com:Opentrons/opent…
SyntaxColoring May 23, 2024
46b024b
Regenerate command schema.
SyntaxColoring May 23, 2024
7d73026
Delete empty log.txt.
SyntaxColoring May 23, 2024
14bc816
Update .json() calls in cli.analyze.
SyntaxColoring May 23, 2024
8dbe3d8
Run bump-pydantic on api.
SyntaxColoring May 24, 2024
d975c0e
Give up on test_command_executor.py.
SyntaxColoring May 23, 2024
e677a7e
Pin fastapi with == like we had before.
SyntaxColoring May 24, 2024
3771b91
Ensure all fastapi pins consistently use 0.100.0.
SyntaxColoring May 24, 2024
ec14dd7
Move robot-server's dependency on performance-metrics to [packages].
SyntaxColoring May 24, 2024
d3c6ce2
Install pydantic-settings in system-server.
SyntaxColoring May 24, 2024
34cae8c
Add pydantic-settings to robot-server/setup.py.
SyntaxColoring May 24, 2024
7ee2b47
Re-lock everything.
SyntaxColoring May 24, 2024
1b3b099
robot-server: Run bump-pydantic.
SyntaxColoring May 24, 2024
06e87fd
robot-server: Remove now-unneeded type-ignores.
SyntaxColoring May 24, 2024
9cdeb04
robot-server: Add a unit test for robot_server.persistence.pydantic.
SyntaxColoring May 24, 2024
5f60e97
robot-server: Post-merge fixups to robot_server.persistence.pydantic.
SyntaxColoring May 24, 2024
dd461e9
system-server: Run bump-pydantic.
SyntaxColoring May 24, 2024
3b1da95
system-server: Manual fixups.
SyntaxColoring May 24, 2024
ff3db4e
Merge remote-tracking branch 'origin/edge' into chore_update-pydantic-v2
SyntaxColoring May 28, 2024
14f5c10
g-code-testing: Run bump-pydantic [WIP]
SyntaxColoring May 24, 2024
01f0ed9
WIP: Fix some slow stuff with TypeAdapter?
SyntaxColoring May 24, 2024
de2b400
robot-server: Update error messages in tests.
SyntaxColoring May 25, 2024
d674c66
Revert BaseModel -> BaseResponseBody changes.
SyntaxColoring Jun 7, 2024
1d95e40
Merge branch 'edge' into chore_update-pydantic-v2
sfoster1 Sep 3, 2024
18a3b22
chore: format and fix lints
sfoster1 Sep 4, 2024
9417cd0
s-d: fix tests
sfoster1 Sep 4, 2024
315a370
api: fix testst
sfoster1 Sep 4, 2024
fea1fad
schema format
sfoster1 Sep 5, 2024
7fecd09
make this installable per https://github.com/pypa/setuptools/issues/4483
sfoster1 Sep 5, 2024
b0c849f
Merge branch 'edge' into chore_update-pydantic-v2
sfoster1 Sep 5, 2024
8082363
chore: pydantic 2.9.0
sfoster1 Sep 5, 2024
b3f7e04
chore: mypy 1.11.0
sfoster1 Sep 5, 2024
5a781f1
chore: py lint fixes
sfoster1 Sep 5, 2024
bab482b
chore: fix hardware-testing
sfoster1 Sep 5, 2024
aaf8f58
chore: fix api runtime type dependencies
sfoster1 Sep 6, 2024
9159f4a
fixup command schema
sfoster1 Sep 6, 2024
666b3ff
fixup some server tests
sfoster1 Sep 9, 2024
cdbf235
Merge branch 'edge' into chore_update-pydantic-v2
sfoster1 Nov 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion api/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ name = "pypi"

[packages]
jsonschema = "==4.17.3"
pydantic = "==1.10.12"
pydantic = "==2.6.4"
pydantic-settings = "==2.2.1"
anyio = "==3.7.1"
opentrons-shared-data = { editable = true, path = "../shared-data/python" }
opentrons = { editable = true, path = "." }
Expand Down
931 changes: 513 additions & 418 deletions api/Pipfile.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions api/mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@ disallow_incomplete_defs = False
disallow_untyped_defs = False
disallow_untyped_calls = False
disallow_incomplete_defs = False

# TODO: fixed in Pydantic 2.7, see https://github.com/pydantic/pydantic/pull/9008
[mypy-tests.opentrons.protocol_engine.execution.test_command_executor.*]
no_strict_optional=True
4 changes: 2 additions & 2 deletions api/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ def get_version():
f"opentrons-shared-data=={VERSION}",
"aionotify==0.2.0",
"anyio>=3.6.1,<4.0.0",
"jsonschema>=3.0.1,<4.18.0",
"jsonschema>=4.0.0,<5",
"numpy>=1.20.0,<2",
"pydantic>=1.10.9,<2.0.0",
"pydantic>=2.0.0,<3",
"pyserial>=3.5",
"typing-extensions>=4.0.0,<5",
"click>=8.0.0,<9",
Expand Down
4 changes: 2 additions & 2 deletions api/src/opentrons/calibration_storage/deck_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ def serialize_deck_configuration(
cutout_fixture_placements: List[CutoutFixturePlacement], last_modified: datetime
) -> bytes:
"""Serialize a deck configuration for storing on the filesystem."""
data = _DeckConfigurationModel.construct(
data = _DeckConfigurationModel.model_construct(
cutoutFixtures=[
_CutoutFixturePlacementModel.construct(
_CutoutFixturePlacementModel.model_construct(
cutoutId=e.cutout_id, cutoutFixtureId=e.cutout_fixture_id
)
for e in cutout_fixture_placements
Expand Down
4 changes: 2 additions & 2 deletions api/src/opentrons/calibration_storage/file_operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def save_to_file(

def serialize_pydantic_model(data: pydantic.BaseModel) -> bytes:
"""Safely serialize data from a Pydantic model into a form suitable for storing on disk."""
return data.json(by_alias=True).encode("utf-8")
return data.model_dump_json(by_alias=True).encode("utf-8")


_ModelT = typing.TypeVar("_ModelT", bound=pydantic.BaseModel)
Expand All @@ -133,7 +133,7 @@ def deserialize_pydantic_model(
Returns `None` if the file is missing or corrupt.
"""
try:
return model.parse_raw(serialized)
return model.model_validate_json(serialized)
except json.JSONDecodeError:
_log.warning("Data is not valid JSON.", exc_info=True)
return None
Expand Down
45 changes: 16 additions & 29 deletions api/src/opentrons/calibration_storage/ot2/models/v1.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import typing

from typing_extensions import Literal
from pydantic import BaseModel, Field, validator
from pydantic import field_validator, BaseModel, Field, PlainSerializer
from datetime import datetime

from opentrons_shared_data.pipette.dev_types import LabwareUri

from opentrons.types import Point
from opentrons.calibration_storage import types

DatetimeType = typing.Annotated[
datetime,
PlainSerializer(lambda x: x.isoformat(), when_used="json"),
]

Comment on lines +12 to +16
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm inferring that this is replacing the old json_encoders config. Does something need to replace the old json_decoders config, to match?


class CalibrationStatus(BaseModel):
markedBad: bool = False
source: typing.Optional[types.SourceType] = None
markedAt: typing.Optional[datetime] = None
markedAt: typing.Optional[DatetimeType] = None


# Schemas used to store the data types
Expand All @@ -22,7 +27,7 @@ class CalibrationStatus(BaseModel):
# they are currently saved in on the OT-2 to avoid a large migration.
class TipLengthModel(BaseModel):
tipLength: float = Field(..., description="Tip length data found from calibration.")
lastModified: datetime = Field(
lastModified: DatetimeType = Field(
..., description="The last time this tip length was calibrated."
)
source: types.SourceType = Field(
Expand All @@ -40,22 +45,19 @@ class TipLengthModel(BaseModel):
..., description="The tiprack hash associated with the tip length data."
)

@validator("tipLength")
@field_validator("tipLength")
@classmethod
def ensure_tip_length_positive(cls, tipLength: float) -> float:
if tipLength < 0.0:
raise ValueError("Tip Length must be a positive number")
return tipLength

class Config:
json_encoders = {datetime: lambda obj: obj.isoformat()}
json_decoders = {datetime: lambda obj: datetime.fromisoformat(obj)}


class DeckCalibrationModel(BaseModel):
attitude: types.AttitudeMatrix = Field(
..., description="Attitude matrix for deck found from calibration."
)
last_modified: typing.Optional[datetime] = Field(
last_modified: typing.Optional[DatetimeType] = Field(
default=None, description="The last time this deck was calibrated."
)
source: types.SourceType = Field(
Expand All @@ -72,18 +74,14 @@ class DeckCalibrationModel(BaseModel):
description="The status of the calibration data.",
)

class Config:
json_encoders = {datetime: lambda obj: obj.isoformat()}
json_decoders = {datetime: lambda obj: datetime.fromisoformat(obj)}


class InstrumentOffsetModel(BaseModel):
offset: Point = Field(..., description="Instrument offset found from calibration.")
tiprack: str = Field(..., description="Tiprack used to calibrate this offset")
uri: str = Field(
..., description="The URI of the labware used for instrument offset"
)
last_modified: datetime = Field(
last_modified: DatetimeType = Field(
..., description="The last time this instrument was calibrated."
)
source: types.SourceType = Field(
Expand All @@ -94,10 +92,6 @@ class InstrumentOffsetModel(BaseModel):
description="The status of the calibration data.",
)

class Config:
json_encoders = {datetime: lambda obj: obj.isoformat()}
json_decoders = {datetime: lambda obj: datetime.fromisoformat(obj)}


# TODO(lc 09-19-2022) We need to refactor the calibration endpoints
# so that we only need to use one data model schema. This model is a
Expand All @@ -112,7 +106,7 @@ class PipetteOffsetCalibration(BaseModel):
uri: str = Field(
..., description="The URI of the labware used for instrument offset"
)
last_modified: datetime = Field(
last_modified: DatetimeType = Field(
..., description="The last time this instrument was calibrated."
)
source: types.SourceType = Field(
Expand All @@ -123,10 +117,6 @@ class PipetteOffsetCalibration(BaseModel):
description="The status of the calibration data.",
)

class Config:
json_encoders = {datetime: lambda obj: obj.isoformat()}
json_decoders = {datetime: lambda obj: datetime.fromisoformat(obj)}


# TODO(lc 09-19-2022) We need to refactor the calibration endpoints
# so that we only need to use one data model schema. This model is a
Expand All @@ -137,7 +127,7 @@ class TipLengthCalibration(BaseModel):
..., description="The tiprack hash associated with this tip length data."
)
tipLength: float = Field(..., description="Tip length data found from calibration.")
lastModified: datetime = Field(
lastModified: DatetimeType = Field(
..., description="The last time this tip length was calibrated."
)
source: types.SourceType = Field(
Expand All @@ -151,12 +141,9 @@ class TipLengthCalibration(BaseModel):
..., description="The tiprack URI associated with the tip length data."
)

@validator("tipLength")
@field_validator("tipLength")
@classmethod
def ensure_tip_length_positive(cls, tipLength: float) -> float:
if tipLength < 0.0:
raise ValueError("Tip Length must be a positive number")
return tipLength

class Config:
json_encoders = {datetime: lambda obj: obj.isoformat()}
json_decoders = {datetime: lambda obj: datetime.fromisoformat(obj)}
1 change: 1 addition & 0 deletions api/src/opentrons/calibration_storage/ot2/tip_length.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
def _convert_tip_length_model_to_dict(
to_dict: typing.Dict[LabwareUri, v1.TipLengthModel]
) -> typing.Dict[LabwareUri, typing.Any]:
# TODO[pydantic]: supported in Pydantic V2
# This is a workaround since pydantic doesn't have a nice way to
# add encoders when converting to a dict.
dict_of_tip_lengths = {}
Expand Down
37 changes: 14 additions & 23 deletions api/src/opentrons/calibration_storage/ot3/models/v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing_extensions import Literal
from opentrons.hardware_control.modules.types import ModuleType
from opentrons.hardware_control.types import OT3Mount
from pydantic import BaseModel, Field, validator
from pydantic import field_validator, BaseModel, Field, PlainSerializer
from datetime import datetime

from opentrons_shared_data.pipette.dev_types import LabwareUri
Expand All @@ -12,15 +12,21 @@
from opentrons.calibration_storage import types


DatetimeType = typing.Annotated[
datetime,
PlainSerializer(lambda x: x.isoformat(), when_used="json"),
]
Comment on lines +15 to +18
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.



class CalibrationStatus(BaseModel):
markedBad: bool = False
source: typing.Optional[types.SourceType] = None
markedAt: typing.Optional[datetime] = None
markedAt: typing.Optional[DatetimeType] = None


class TipLengthModel(BaseModel):
tipLength: float = Field(..., description="Tip length data found from calibration.")
lastModified: datetime = Field(
lastModified: DatetimeType = Field(
..., description="The last time this tip length was calibrated."
)
uri: typing.Union[LabwareUri, Literal[""]] = Field(
Expand All @@ -34,22 +40,19 @@ class TipLengthModel(BaseModel):
description="The status of the calibration data.",
)

@validator("tipLength")
@field_validator("tipLength")
@classmethod
def ensure_tip_length_positive(cls, tipLength: float) -> float:
if tipLength < 0.0:
raise ValueError("Tip Length must be a positive number")
return tipLength

class Config:
json_encoders = {datetime: lambda obj: obj.isoformat()}
json_decoders = {datetime: lambda obj: datetime.fromisoformat(obj)}


class BeltCalibrationModel(BaseModel):
attitude: types.AttitudeMatrix = Field(
..., description="Attitude matrix for belts found from calibration."
)
lastModified: datetime = Field(
lastModified: DatetimeType = Field(
..., description="The last time this deck was calibrated."
)
source: types.SourceType = Field(
Expand All @@ -63,14 +66,10 @@ class BeltCalibrationModel(BaseModel):
description="The status of the calibration data.",
)

class Config:
json_encoders = {datetime: lambda obj: obj.isoformat()}
json_decoders = {datetime: lambda obj: datetime.fromisoformat(obj)}


class InstrumentOffsetModel(BaseModel):
offset: Point = Field(..., description="Instrument offset found from calibration.")
lastModified: datetime = Field(
lastModified: DatetimeType = Field(
..., description="The last time this instrument was calibrated."
)
source: types.SourceType = Field(
Expand All @@ -81,10 +80,6 @@ class InstrumentOffsetModel(BaseModel):
description="The status of the calibration data.",
)

class Config:
json_encoders = {datetime: lambda obj: obj.isoformat()}
json_decoders = {datetime: lambda obj: datetime.fromisoformat(obj)}


class ModuleOffsetModel(BaseModel):
offset: Point = Field(..., description="Module offset found from calibration.")
Expand All @@ -99,7 +94,7 @@ class ModuleOffsetModel(BaseModel):
...,
description="The unique id of the instrument used to calibrate this module.",
)
lastModified: datetime = Field(
lastModified: DatetimeType = Field(
..., description="The last time this module was calibrated."
)
source: types.SourceType = Field(
Expand All @@ -109,7 +104,3 @@ class ModuleOffsetModel(BaseModel):
default_factory=CalibrationStatus,
description="The status of the calibration data.",
)

class Config:
json_encoders = {datetime: lambda obj: obj.isoformat()}
json_decoders = {datetime: lambda obj: datetime.fromisoformat(obj)}
11 changes: 5 additions & 6 deletions api/src/opentrons/cli/analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,20 +81,19 @@ async def _analyze(
robot_type=protocol_source.robot_type, protocol_config=protocol_source.config
)
analysis = await runner.run(deck_configuration=[], protocol_source=protocol_source)

if json_output:
results = AnalyzeResults.construct(
results = AnalyzeResults.model_construct(
createdAt=datetime.now(tz=timezone.utc),
files=[
ProtocolFile.construct(name=f.path.name, role=f.role)
ProtocolFile.model_construct(name=f.path.name, role=f.role)
for f in protocol_source.files
],
config=(
JsonConfig.construct(
JsonConfig.model_construct(
schemaVersion=protocol_source.config.schema_version
)
if isinstance(protocol_source.config, JsonProtocolConfig)
else PythonConfig.construct(
else PythonConfig.model_construct(
apiVersion=protocol_source.config.api_version
)
),
Expand All @@ -110,7 +109,7 @@ async def _analyze(
)

await json_output.write_text(
results.json(exclude_none=True),
results.model_dump_json(exclude_none=True),
encoding="utf-8",
)

Expand Down
6 changes: 4 additions & 2 deletions api/src/opentrons/execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
Union,
)

from opentrons_shared_data.labware.labware_definition import LabwareDefinition
from opentrons_shared_data.labware.models import LabwareDefinition
from opentrons_shared_data.robot.dev_types import RobotType

from opentrons import protocol_api, __version__, should_use_ot3
Expand Down Expand Up @@ -550,7 +550,9 @@ def _create_live_context_pe(
# Non-async would use call_soon_threadsafe(), which makes the waiting harder.
async def add_all_extra_labware() -> None:
for labware_definition_dict in extra_labware.values():
labware_definition = LabwareDefinition.parse_obj(labware_definition_dict)
labware_definition = LabwareDefinition.model_validate(
labware_definition_dict
)
pe.add_labware_definition(labware_definition)

# Add extra_labware to ProtocolEngine, being careful not to modify ProtocolEngine from this
Expand Down
7 changes: 3 additions & 4 deletions api/src/opentrons/hardware_control/emulation/settings.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import List
from opentrons.hardware_control.emulation.types import ModuleType
from opentrons.hardware_control.emulation.util import TEMPERATURE_ROOM
from pydantic import BaseSettings, BaseModel
from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict


class PipetteSettings(BaseModel):
Expand Down Expand Up @@ -113,8 +114,6 @@ class Settings(BaseSettings):
emulator_port=9003, driver_port=9998
)
magdeck_proxy: ProxySettings = ProxySettings(emulator_port=9004, driver_port=9999)

class Config:
env_prefix = "OT_EMULATOR_"
model_config = SettingsConfigDict(env_prefix="OT_EMULATOR_")

module_server: ModuleServerSettings = ModuleServerSettings()
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from opentrons.types import Mount, Point
from opentrons.hardware_control.types import OT3Mount

from opentrons_shared_data.labware.labware_definition import LabwareDefinition
from opentrons_shared_data.labware.models import LabwareDefinition

if typing.TYPE_CHECKING:
from opentrons_shared_data.pipette.dev_types import LabwareUri
Expand Down
Loading
Loading