Skip to content

Commit be401a2

Browse files
committed
feat(api): support an "experimental" api version
Add the ability to specify a protocol's API level as "experimental", and to specify API calls as experimental. The experimental API level is greater than any named API level. That means that - An api method that is experimental can only be used if the protocol requests the experimental api level - This will remain true as the max supported api version changes
1 parent e583471 commit be401a2

File tree

3 files changed

+90
-23
lines changed

3 files changed

+90
-23
lines changed
Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
1-
from .types import APIVersion
2-
3-
MAX_SUPPORTED_VERSION = APIVersion(2, 23)
4-
"""The maximum supported protocol API version in this release."""
5-
6-
MIN_SUPPORTED_VERSION = APIVersion(2, 0)
7-
"""The minimum supported protocol API version in this release, across all robot types."""
8-
9-
MIN_SUPPORTED_VERSION_FOR_FLEX = APIVersion(2, 15)
10-
"""The minimum protocol API version supported by the Opentrons Flex.
11-
12-
It's an infrastructural requirement for this to be at least newer than 2.14. Before then,
13-
the protocol API is backed by the legacy non-Protocol-engine backend, which is not prepared to
14-
handle anything but OT-2s.
15-
16-
The additional bump to 2.15 is because that's what we tested on, and because it adds all the
17-
Flex-specific features.
18-
"""
1+
from .types import (
2+
MAX_SUPPORTED_VERSION,
3+
MIN_SUPPORTED_VERSION,
4+
MIN_SUPPORTED_VERSION_FOR_FLEX,
5+
)
6+
7+
8+
__all__ = [
9+
"MAX_SUPPORTED_VERSION",
10+
"MIN_SUPPORTED_VERSION",
11+
"MIN_SUPPORTED_VERSION_FOR_FLEX",
12+
]

api/src/opentrons/protocols/api_support/types.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,55 @@
11
from __future__ import annotations
22
from typing import NamedTuple, TypedDict
3+
from functools import total_ordering
34

45

6+
@total_ordering
57
class APIVersion(NamedTuple):
68
major: int
79
minor: int
10+
experimental: bool = False
811

912
@classmethod
1013
def from_string(cls, inp: str) -> APIVersion:
14+
if inp == "experimental":
15+
return cls.as_experimental()
1116
parts = inp.split(".")
1217
if len(parts) != 2:
1318
raise ValueError(inp)
1419
intparts = [int(p) for p in parts]
1520

16-
return cls(major=intparts[0], minor=intparts[1])
21+
return cls(major=intparts[0], minor=intparts[1], experimental=False)
22+
23+
@classmethod
24+
def as_experimental(cls) -> APIVersion:
25+
return cls(
26+
major=MAX_SUPPORTED_VERSION.major,
27+
minor=MAX_SUPPORTED_VERSION.minor,
28+
experimental=True,
29+
)
1730

1831
def __str__(self) -> str:
19-
return f"{self.major}.{self.minor}"
32+
return f"{self.major}.{self.minor}" if not self.experimental else "experimental"
33+
34+
def __le__(self, other: tuple[int, int, bool] | tuple[int, int]) -> bool: # type: ignore[override]
35+
# this __le__ supports calling it against a version raw tuple or an API version;
36+
# the type ignore happens because NamedTuple says its parent is tuple[int...] in this
37+
# case, not tuple[int, int, bool]
38+
b_maj = other[0]
39+
b_min = other[1]
40+
b_exp = other[2] if len(other) > 2 else False
41+
# when you do a <= b, interpreter calls a.__le__(b)
42+
if self.experimental and b_exp:
43+
# both a and b are experimental: same version
44+
return True
45+
elif self.experimental:
46+
# a is experimental, and b is not: a > b, a !<= b
47+
return False
48+
elif b_exp:
49+
# a is not experimental, and b is: a < b, a <= b
50+
return True
51+
# both are not experimental, standard version compare
52+
return (self.major, self.minor) <= (b_maj, b_min)
2053

2154

2255
class ThermocyclerStepBase(TypedDict):
@@ -30,3 +63,21 @@ class ThermocyclerStep(ThermocyclerStepBase, total=False):
3063

3164
hold_time_seconds: float
3265
hold_time_minutes: float
66+
67+
68+
MAX_SUPPORTED_VERSION = APIVersion(2, 23)
69+
"""The maximum supported protocol API version in this release."""
70+
71+
MIN_SUPPORTED_VERSION = APIVersion(2, 0)
72+
"""The minimum supported protocol API version in this release, across all robot types."""
73+
74+
MIN_SUPPORTED_VERSION_FOR_FLEX = APIVersion(2, 15)
75+
"""The minimum protocol API version supported by the Opentrons Flex.
76+
77+
It's an infrastructural requirement for this to be at least newer than 2.14. Before then,
78+
the protocol API is backed by the legacy non-Protocol-engine backend, which is not prepared to
79+
handle anything but OT-2s.
80+
81+
The additional bump to 2.15 is because that's what we tested on, and because it adds all the
82+
Flex-specific features.
83+
"""

api/src/opentrons/protocols/api_support/util.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
""" Utility functions and classes for the protocol api """
1+
"""Utility functions and classes for the protocol api"""
2+
23
from __future__ import annotations
34

45
from collections import UserDict
@@ -18,8 +19,11 @@
1819
cast,
1920
KeysView,
2021
ItemsView,
22+
overload,
2123
)
2224

25+
from typing_extensions import Literal
26+
2327
from opentrons import types as top_types
2428
from opentrons.protocols.api_support.types import APIVersion
2529
from opentrons.hardware_control.types import Axis
@@ -303,7 +307,6 @@ def _verify_key(key: object) -> Axis:
303307
return checked_key
304308

305309
def __setitem__(self, key: object, value: object) -> None:
306-
307310
checked_key = AxisMaxSpeeds._verify_key(key)
308311
if value is None:
309312
del self[checked_key]
@@ -353,13 +356,32 @@ def clamp_value(
353356
FuncT = TypeVar("FuncT", bound=Callable[..., Any])
354357

355358

359+
@overload
360+
def requires_version(
361+
major: Literal["experimental"],
362+
) -> Callable[[FuncT], FuncT]:
363+
...
364+
365+
366+
@overload
356367
def requires_version(major: int, minor: int) -> Callable[[FuncT], FuncT]:
368+
...
369+
370+
371+
def requires_version(
372+
major: int | Literal["experimental"], minor: int | None = None
373+
) -> Callable[[FuncT], FuncT]:
357374
"""Decorator. Apply to Protocol API methods or attributes to indicate
358375
the first version in which the method or attribute was present.
359376
"""
360377

361378
def _set_version(decorated_obj: FuncT) -> FuncT:
362-
added_version = APIVersion(major, minor)
379+
if major == "experimental":
380+
added_version = APIVersion.as_experimental()
381+
else:
382+
assert major is not None
383+
assert minor is not None
384+
added_version = APIVersion(major, minor, False)
363385
setattr(decorated_obj, "__opentrons_version_added", added_version)
364386
if hasattr(decorated_obj, "__doc__"):
365387
# Add the versionadded stanza to everything decorated if we can

0 commit comments

Comments
 (0)