Skip to content

Commit 74606e6

Browse files
committed
fix schema generation
This is in one commit so it's easy to diff vs the last sorted schema. Minimal changes from the last one. Funny little trick from https://skaaptjop.medium.com/how-i-use-pydantic-unrequired-fields-so-that-the-schema-works-0010d8758072 to make it express optional things in the same way as before.
1 parent e3a6b90 commit 74606e6

36 files changed

+1193
-1418
lines changed

api/src/opentrons/protocol_engine/commands/absorbance_reader/initialize.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"""Command models to initialize an Absorbance Reader."""
22
from __future__ import annotations
3-
from typing import List, Optional, Literal, TYPE_CHECKING
3+
from typing import List, Optional, Literal, TYPE_CHECKING, Any
44
from typing_extensions import Type
55

66
from pydantic import BaseModel, Field
7+
from pydantic.json_schema import SkipJsonSchema
78

89
from opentrons.drivers.types import ABSMeasurementMode
910
from opentrons.protocol_engine.types import ABSMeasureMode
@@ -20,6 +21,10 @@
2021
InitializeCommandType = Literal["absorbanceReader/initialize"]
2122

2223

24+
def _remove_default(s: dict[str, Any]) -> None:
25+
s.pop("default")
26+
27+
2328
class InitializeParams(BaseModel):
2429
"""Input parameters to initialize an absorbance reading."""
2530

@@ -28,8 +33,10 @@ class InitializeParams(BaseModel):
2833
..., description="Initialize single or multi measurement mode."
2934
)
3035
sampleWavelengths: List[int] = Field(..., description="Sample wavelengths in nm.")
31-
referenceWavelength: Optional[int] = Field(
32-
None, description="Optional reference wavelength in nm."
36+
referenceWavelength: int | SkipJsonSchema[None] = Field(
37+
None,
38+
description="Optional reference wavelength in nm.",
39+
json_schema_extra=_remove_default,
3340
)
3441

3542

api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"""Command models to read absorbance."""
22
from __future__ import annotations
33
from datetime import datetime
4-
from typing import Optional, Dict, TYPE_CHECKING, List
5-
from typing_extensions import Literal, Type
4+
from typing import Optional, Dict, TYPE_CHECKING, List, Any
65

6+
from typing_extensions import Literal, Type
77
from pydantic import BaseModel, Field
8+
from pydantic.json_schema import SkipJsonSchema
89

910
from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
1011
from ...errors import CannotPerformModuleAction, StorageLimitReachedError
@@ -22,16 +23,21 @@
2223
from opentrons.protocol_engine.execution import EquipmentHandler
2324

2425

26+
def _remove_default(s: dict[str, Any]) -> None:
27+
s.pop("default")
28+
29+
2530
ReadAbsorbanceCommandType = Literal["absorbanceReader/read"]
2631

2732

2833
class ReadAbsorbanceParams(BaseModel):
2934
"""Input parameters for an absorbance reading."""
3035

3136
moduleId: str = Field(..., description="Unique ID of the Absorbance Reader.")
32-
fileName: Optional[str] = Field(
37+
fileName: str | SkipJsonSchema[None] = Field(
3338
None,
3439
description="Optional file name to use when storing the results of a measurement.",
40+
json_schema_extra=_remove_default,
3541
)
3642

3743

api/src/opentrons/protocol_engine/commands/calibration/calibrate_gripper.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"""Models and implementation for the calibrateGripper command."""
22

33
from enum import Enum
4-
from typing import Optional, Type
4+
from typing import Optional, Type, Any
55
from typing_extensions import Literal
66

77
from pydantic import BaseModel, Field
8+
from pydantic.json_schema import SkipJsonSchema
89

910
from opentrons.types import Point
1011
from opentrons.hardware_control import HardwareControlAPI
@@ -22,6 +23,10 @@
2223
CalibrateGripperCommandType = Literal["calibration/calibrateGripper"]
2324

2425

26+
def _remove_default(s: dict[str, Any]) -> None:
27+
s.pop("default")
28+
29+
2530
class CalibrateGripperParamsJaw(Enum): # noqa: D101
2631
FRONT = "front"
2732
REAR = "rear"
@@ -39,7 +44,7 @@ class CalibrateGripperParams(BaseModel):
3944
),
4045
)
4146

42-
otherJawOffset: Optional[Vec3f] = Field(
47+
otherJawOffset: Vec3f | SkipJsonSchema[None] = Field(
4348
None,
4449
description=(
4550
"If an offset for the other probe is already found, then specifying it here"
@@ -48,6 +53,7 @@ class CalibrateGripperParams(BaseModel):
4853
" If this param is not specified then the command will only find and return"
4954
" the offset for the specified probe."
5055
),
56+
json_schema_extra=_remove_default,
5157
)
5258

5359

@@ -62,11 +68,12 @@ class CalibrateGripperResult(BaseModel):
6268
),
6369
)
6470

65-
savedCalibration: Optional[GripperCalibrationOffset] = Field(
71+
savedCalibration: GripperCalibrationOffset | SkipJsonSchema[None] = Field(
6672
None,
6773
description=(
6874
"Gripper calibration result data, when `otherJawOffset` is provided."
6975
),
76+
json_schema_extra=_remove_default,
7077
)
7178

7279

api/src/opentrons/protocol_engine/commands/command.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@
1414
List,
1515
Type,
1616
Union,
17+
Any,
18+
Dict,
1719
)
1820

1921
from pydantic import BaseModel, Field
22+
from pydantic.json_schema import SkipJsonSchema
2023

2124
from opentrons.hardware_control import HardwareControlAPI
2225
from opentrons.protocol_engine.state.update_types import StateUpdate
@@ -61,6 +64,10 @@ class CommandIntent(str, enum.Enum):
6164
FIXIT = "fixit"
6265

6366

67+
def _pop_default(s: Dict[str, Any]) -> None:
68+
s.pop("default")
69+
70+
6471
class BaseCommandCreate(
6572
BaseModel,
6673
# These type parameters need to be invariant because our fields are mutable.
@@ -80,7 +87,7 @@ class BaseCommandCreate(
8087
),
8188
)
8289
params: _ParamsT = Field(..., description="Command execution data payload")
83-
intent: Optional[CommandIntent] = Field(
90+
intent: CommandIntent | SkipJsonSchema[None] = Field(
8491
None,
8592
description=(
8693
"The reason the command was added. If not specified or `protocol`,"
@@ -93,14 +100,16 @@ class BaseCommandCreate(
93100
"Use setup commands for activities like pre-run calibration checks"
94101
" and module setup, like pre-heating."
95102
),
103+
json_schema_extra=_pop_default,
96104
)
97-
key: Optional[str] = Field(
105+
key: str | SkipJsonSchema[None] = Field(
98106
None,
99107
description=(
100108
"A key value, unique in this run, that can be used to track"
101109
" the same logical command across multiple runs of the same protocol."
102110
" If a value is not provided, one will be generated."
103111
),
112+
json_schema_extra=_pop_default,
104113
)
105114

106115

api/src/opentrons/protocol_engine/commands/configure_for_volume.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""Configure for volume command request, result, and implementation models."""
22
from __future__ import annotations
3+
from typing import TYPE_CHECKING, Optional, Type, Any
4+
35
from pydantic import BaseModel, Field
4-
from typing import TYPE_CHECKING, Optional, Type
6+
from pydantic.json_schema import SkipJsonSchema
57
from typing_extensions import Literal
68

79
from .pipetting_common import PipetteIdMixin
@@ -16,6 +18,10 @@
1618
ConfigureForVolumeCommandType = Literal["configureForVolume"]
1719

1820

21+
def _remove_default(s: dict[str, Any]) -> None:
22+
s.pop("default")
23+
24+
1925
class ConfigureForVolumeParams(PipetteIdMixin):
2026
"""Parameters required to configure volume for a specific pipette."""
2127

@@ -25,12 +31,13 @@ class ConfigureForVolumeParams(PipetteIdMixin):
2531
"than a pipette-specific maximum volume.",
2632
ge=0,
2733
)
28-
tipOverlapNotAfterVersion: Optional[str] = Field(
34+
tipOverlapNotAfterVersion: str | SkipJsonSchema[None] = Field(
2935
None,
3036
description="A version of tip overlap data to not exceed. The highest-versioned "
3137
"tip overlap data that does not exceed this version will be used. Versions are "
3238
"expressed as vN where N is an integer, counting up from v0. If None, the current "
3339
"highest version will be used.",
40+
json_schema_extra=_remove_default,
3441
)
3542

3643

api/src/opentrons/protocol_engine/commands/dispense.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
"""Dispense command request, result, and implementation models."""
22

33
from __future__ import annotations
4-
from typing import TYPE_CHECKING, Optional, Type, Union
4+
from typing import TYPE_CHECKING, Optional, Type, Union, Any
55
from typing_extensions import Literal
66

77

88
from pydantic import Field
9+
from pydantic.json_schema import SkipJsonSchema
910

1011
from ..state.update_types import StateUpdate, CLEAR
1112
from .pipetting_common import (
@@ -39,14 +40,19 @@
3940
DispenseCommandType = Literal["dispense"]
4041

4142

43+
def _remove_default(s: dict[str, Any]) -> None:
44+
s.pop("default")
45+
46+
4247
class DispenseParams(
4348
PipetteIdMixin, DispenseVolumeMixin, FlowRateMixin, LiquidHandlingWellLocationMixin
4449
):
4550
"""Payload required to dispense to a specific well."""
4651

47-
pushOut: Optional[float] = Field(
52+
pushOut: float | SkipJsonSchema[None] = Field(
4853
None,
4954
description="push the plunger a small amount farther than necessary for accurate low-volume dispensing",
55+
json_schema_extra=_remove_default,
5056
)
5157

5258

api/src/opentrons/protocol_engine/commands/dispense_in_place.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"""Dispense-in-place command request, result, and implementation models."""
22

33
from __future__ import annotations
4-
from typing import TYPE_CHECKING, Optional, Type, Union
4+
from typing import TYPE_CHECKING, Optional, Type, Union, Any
55
from typing_extensions import Literal
66
from pydantic import Field
7+
from pydantic.json_schema import SkipJsonSchema
78

89
from .pipetting_common import (
910
PipetteIdMixin,
@@ -32,12 +33,17 @@
3233
DispenseInPlaceCommandType = Literal["dispenseInPlace"]
3334

3435

36+
def _remove_default(s: dict[str, Any]) -> None:
37+
s.pop("default")
38+
39+
3540
class DispenseInPlaceParams(PipetteIdMixin, DispenseVolumeMixin, FlowRateMixin):
3641
"""Payload required to dispense in place."""
3742

38-
pushOut: Optional[float] = Field(
43+
pushOut: float | SkipJsonSchema[None] = Field(
3944
None,
4045
description="push the plunger a small amount farther than necessary for accurate low-volume dispensing",
46+
json_schema_extra=_remove_default,
4147
)
4248

4349

api/src/opentrons/protocol_engine/commands/drop_tip.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""Drop tip command request, result, and implementation models."""
22

33
from __future__ import annotations
4+
from typing import TYPE_CHECKING, Optional, Type, Any
45

56
from pydantic import Field
6-
from typing import TYPE_CHECKING, Optional, Type
7+
from pydantic.json_schema import SkipJsonSchema
8+
79
from typing_extensions import Literal
810

911
from opentrons.protocol_engine.errors.exceptions import TipAttachedError
@@ -37,6 +39,10 @@
3739
DropTipCommandType = Literal["dropTip"]
3840

3941

42+
def _remove_default(s: dict[str, Any]) -> None:
43+
s.pop("default")
44+
45+
4046
class DropTipParams(PipetteIdMixin):
4147
"""Payload required to drop a tip in a specific well."""
4248

@@ -46,15 +52,16 @@ class DropTipParams(PipetteIdMixin):
4652
default_factory=DropTipWellLocation,
4753
description="Relative well location at which to drop the tip.",
4854
)
49-
homeAfter: Optional[bool] = Field(
55+
homeAfter: bool | SkipJsonSchema[None] = Field(
5056
None,
5157
description=(
5258
"Whether to home this pipette's plunger after dropping the tip."
5359
" You should normally leave this unspecified to let the robot choose"
5460
" a safe default depending on its hardware."
5561
),
62+
json_schema_extra=_remove_default,
5663
)
57-
alternateDropLocation: Optional[bool] = Field(
64+
alternateDropLocation: bool | SkipJsonSchema[None] = Field(
5865
False,
5966
description=(
6067
"Whether to alternate location where tip is dropped within the labware."
@@ -63,6 +70,7 @@ class DropTipParams(PipetteIdMixin):
6370
" labware well."
6471
" If False, the tip will be dropped at the top center of the well."
6572
),
73+
json_schema_extra=_remove_default,
6674
)
6775

6876

api/src/opentrons/protocol_engine/commands/drop_tip_in_place.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
"""Drop tip in place command request, result, and implementation models."""
22
from __future__ import annotations
3+
4+
from typing import TYPE_CHECKING, Optional, Type, Any
5+
36
from pydantic import Field, BaseModel
4-
from typing import TYPE_CHECKING, Optional, Type
7+
from pydantic.json_schema import SkipJsonSchema
58
from typing_extensions import Literal
69

710
from .command import (
@@ -24,16 +27,21 @@
2427
DropTipInPlaceCommandType = Literal["dropTipInPlace"]
2528

2629

30+
def _remove_default(s: dict[str, Any]) -> None:
31+
s.pop("default")
32+
33+
2734
class DropTipInPlaceParams(PipetteIdMixin):
2835
"""Payload required to drop a tip in place."""
2936

30-
homeAfter: Optional[bool] = Field(
37+
homeAfter: bool | SkipJsonSchema[None] = Field(
3138
None,
3239
description=(
3340
"Whether to home this pipette's plunger after dropping the tip."
3441
" You should normally leave this unspecified to let the robot choose"
3542
" a safe default depending on its hardware."
3643
),
44+
json_schema_extra=_remove_default,
3745
)
3846

3947

api/src/opentrons/protocol_engine/commands/generate_command_schema.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,17 @@
11
"""Generate a JSON schema against which all create commands statically validate."""
2+
23
import json
3-
import pydantic
44
import argparse
55
import sys
6-
from opentrons.protocol_engine.commands.command_unions import CommandCreate
7-
8-
9-
class CreateCommandUnion(pydantic.RootModel[CommandCreate]):
10-
"""Model that validates a union of all CommandCreate models."""
11-
12-
root: CommandCreate
6+
from opentrons.protocol_engine.commands.command_unions import CommandCreateAdapter
137

148

159
def generate_command_schema(version: str) -> str:
1610
"""Generate a JSON Schema that all valid create commands can validate against."""
17-
raw_json_schema = CreateCommandUnion.schema_json()
18-
schema_as_dict = json.loads(raw_json_schema)
11+
schema_as_dict = CommandCreateAdapter.json_schema(mode="validation")
1912
schema_as_dict["$id"] = f"opentronsCommandSchemaV{version}"
2013
schema_as_dict["$schema"] = "http://json-schema.org/draft-07/schema#"
21-
return json.dumps(schema_as_dict, indent=2)
14+
return json.dumps(schema_as_dict, indent=2, sort_keys=True)
2215

2316

2417
if __name__ == "__main__":

0 commit comments

Comments
 (0)