diff --git a/robot-server/robot_server/app_setup.py b/robot-server/robot_server/app_setup.py index 181021ebac5..80fda961119 100644 --- a/robot-server/robot_server/app_setup.py +++ b/robot-server/robot_server/app_setup.py @@ -54,6 +54,9 @@ ), version=__version__, exception_handlers=exception_handlers, + # Disable documentation hosting via Swagger UI, normally at /docs. + # We instead focus on the docs hosted by ReDoc, at /redoc. + docs_url=None, ) # cors diff --git a/robot-server/robot_server/deck_configuration/models.py b/robot-server/robot_server/deck_configuration/models.py index 284c948f35c..f0d2a7cd6bd 100644 --- a/robot-server/robot_server/deck_configuration/models.py +++ b/robot-server/robot_server/deck_configuration/models.py @@ -39,7 +39,10 @@ class DeckConfigurationRequest(pydantic.BaseModel): """A request to set the robot's deck configuration.""" cutoutFixtures: List[CutoutFixture] = pydantic.Field( - description="A full list of all the cutout fixtures that are mounted onto the deck." + description=( + "A full list of all the cutout fixtures that are mounted onto the deck." + " The order is arbitrary." + ) ) @@ -47,7 +50,10 @@ class DeckConfigurationResponse(pydantic.BaseModel): """A response for the robot's current deck configuration.""" cutoutFixtures: List[CutoutFixture] = pydantic.Field( - description="A full list of all the cutout fixtures that are mounted onto the deck." + description=( + "A full list of all the cutout fixtures that are mounted onto the deck." + " The order is arbitrary." + ) ) lastModifiedAt: Optional[datetime] = pydantic.Field( description=( diff --git a/robot-server/robot_server/deck_configuration/router.py b/robot-server/robot_server/deck_configuration/router.py index 8bfc4025346..4e00a3d707e 100644 --- a/robot-server/robot_server/deck_configuration/router.py +++ b/robot-server/robot_server/deck_configuration/router.py @@ -27,7 +27,7 @@ @PydanticResponse.wrap_route( router.put, path="/deck_configuration", - summary="Set the deck configuration", + summary="Set the Flex deck configuration", description=( "Inform the robot how its deck is physically set up." "\n\n" @@ -38,6 +38,9 @@ " configuration, such as loading a labware into a staging area slot that this deck" " configuration doesn't provide, the run command will fail with an error." "\n\n" + "After you set the deck configuration, it will persist, even across reboots," + " until you set it to something else." + "\n\n" "**Warning:**" " Currently, you can call this endpoint at any time, even while there is an active run." " However, the robot can't adapt to deck configuration changes in the middle of a run." @@ -45,8 +48,8 @@ " first played. In the future, this endpoint may error if you try to call it in the middle" " of an active run, so don't rely on being able to do that." "\n\n" - "After you set the deck configuration, it will persist, even across reboots," - " until you set it to something else." + "**Warning:** Only use this on Flex robots, never OT-2 robots. The behavior on" + " OT-2 robots is currently undefined and it may interfere with protocol execution." ), responses={ fastapi.status.HTTP_200_OK: { @@ -86,10 +89,13 @@ async def put_deck_configuration( # noqa: D103 @PydanticResponse.wrap_route( router.get, path="/deck_configuration", - summary="Get the deck configuration", + summary="Get the Flex deck configuration", description=( "Get the robot's current deck configuration." " See `PUT /deck_configuration` for background information." + "\n\n" + "**Warning:** The behavior of this endpoint is currently only defined for Flex" + " robots, not OT-2 robots." ), responses={ fastapi.status.HTTP_200_OK: { diff --git a/robot-server/robot_server/instruments/router.py b/robot-server/robot_server/instruments/router.py index f8e7448d5f1..1497b274a60 100644 --- a/robot-server/robot_server/instruments/router.py +++ b/robot-server/robot_server/instruments/router.py @@ -254,9 +254,14 @@ async def _get_attached_instruments_ot2( @PydanticResponse.wrap_route( instruments_router.get, path="/instruments", - summary="Get attached instruments.", - description="Get a list of all instruments (pipettes & gripper) currently attached" - " to the robot.", + summary="Get attached instruments", + description=( + "Get a list of all instruments (pipettes & gripper) currently attached" + " to the robot." + "\n\n" + "**Warning:** The behavior of this endpoint is currently only defined for Flex" + " robots. For OT-2 robots, use `/pipettes` instead." + ), responses={status.HTTP_200_OK: {"model": SimpleMultiBody[AttachedItem]}}, ) async def get_attached_instruments( diff --git a/robot-server/robot_server/protocols/protocol_models.py b/robot-server/robot_server/protocols/protocol_models.py index 79572dbf803..0e902d60034 100644 --- a/robot-server/robot_server/protocols/protocol_models.py +++ b/robot-server/robot_server/protocols/protocol_models.py @@ -102,4 +102,10 @@ class Protocol(ResourceModel): ), ) - key: Optional[str] = None + key: Optional[str] = Field( + None, + description=( + "An arbitrary client-defined string, set when this protocol was uploaded." + " See `POST /protocols`." + ), + ) diff --git a/robot-server/robot_server/protocols/router.py b/robot-server/robot_server/protocols/router.py index 65a98d77e58..a64990cf27c 100644 --- a/robot-server/robot_server/protocols/router.py +++ b/robot-server/robot_server/protocols/router.py @@ -19,6 +19,7 @@ FileHasher, ) from opentrons_shared_data.robot.dev_types import RobotType + from robot_server.errors.error_responses import ErrorDetails, ErrorBody from robot_server.hardware import get_robot_type from robot_server.service.task_runner import TaskRunner, get_task_runner @@ -154,7 +155,16 @@ async def create_protocol( files: List[UploadFile] = File(...), # use Form because request is multipart/form-data # https://fastapi.tiangolo.com/tutorial/request-forms-and-files/ - key: Optional[str] = Form(None), + key: Optional[str] = Form( + default=None, + description=( + "An arbitrary client-defined string to attach to the new protocol resource." + " This should be no longer than ~100 characters or so." + " It's intended to store something like a UUID, to help clients that store" + " protocols locally keep track of which local files correspond to which" + " protocol resources on the robot." + ), + ), protocol_directory: Path = Depends(get_protocol_directory), protocol_store: ProtocolStore = Depends(get_protocol_store), analysis_store: AnalysisStore = Depends(get_analysis_store), diff --git a/robot-server/robot_server/router.py b/robot-server/robot_server/router.py index 1693f2a638a..eec875df14f 100644 --- a/robot-server/robot_server/router.py +++ b/robot-server/robot_server/router.py @@ -72,7 +72,7 @@ router.include_router( router=deck_configuration_router, - tags=["Deck Configuration"], + tags=["Flex Deck Configuration"], dependencies=[Depends(check_version_header)], ) @@ -90,7 +90,7 @@ router.include_router( router=deprecated_session_router, - tags=["Session Management"], + tags=["OT-2 Calibration Sessions"], dependencies=[Depends(check_version_header)], ) @@ -120,7 +120,7 @@ router.include_router( router=subsystems_router, - tags=["Subsystem Management"], + tags=["Flex Subsystem Management"], dependencies=[Depends(check_version_header)], ) diff --git a/robot-server/robot_server/service/json_api/response.py b/robot-server/robot_server/service/json_api/response.py index a43e6c11568..dd2d0dc7b1d 100644 --- a/robot-server/robot_server/service/json_api/response.py +++ b/robot-server/robot_server/service/json_api/response.py @@ -110,7 +110,7 @@ class SimpleMultiBody(BaseResponseBody, GenericModel, Generic[ResponseDataT]): # to be covariant is to make data the covariant Sequence protocol. meta: MultiBodyMeta = Field( ..., - description="Metadata about the colletion response.", + description="Metadata about the collection response.", ) @@ -125,7 +125,7 @@ class MultiBody( links: ResponseLinksT = Field(..., description=DESCRIPTION_LINKS) meta: MultiBodyMeta = Field( ..., - description="Metadata about the colletion response.", + description="Metadata about the collection response.", ) diff --git a/robot-server/robot_server/service/labware/router.py b/robot-server/robot_server/service/labware/router.py index 95d404c84b0..930a6c91360 100644 --- a/robot-server/robot_server/service/labware/router.py +++ b/robot-server/robot_server/service/labware/router.py @@ -36,6 +36,7 @@ class LabwareCalibrationEndpointsRemoved(ErrorDetails): "This endpoint has been removed." " Use the `/runs` endpoints to manage labware offsets." ), + deprecated=True, response_model=None, responses={ status.HTTP_200_OK: {"model": lw_models.MultipleCalibrationsResponse}, @@ -62,6 +63,7 @@ async def get_all_labware_calibrations( "This endpoint has been removed." " Use the `/runs` endpoints to manage labware offsets." ), + deprecated=True, response_model=None, responses={ status.HTTP_404_NOT_FOUND: {"model": ErrorBody}, @@ -89,6 +91,7 @@ async def get_specific_labware_calibration( "This endpoint has been removed." " Use the `/runs` endpoints to manage labware offsets." ), + deprecated=True, response_model=None, responses={ status.HTTP_404_NOT_FOUND: {"model": ErrorBody}, diff --git a/robot-server/robot_server/service/legacy/models/deck_calibration.py b/robot-server/robot_server/service/legacy/models/deck_calibration.py index 401589c82a2..a1db8eef866 100644 --- a/robot-server/robot_server/service/legacy/models/deck_calibration.py +++ b/robot-server/robot_server/service/legacy/models/deck_calibration.py @@ -29,8 +29,22 @@ class InstrumentOffset(BaseModel): - single: Offset - multi: Offset + single: Offset = Field( + ..., + deprecated=True, + description=( + "This will always be `[0, 0, 0]`." + " Use the `GET /calibration/pipette_offset` endpoint instead." + ), + ) + multi: Offset = Field( + ..., + deprecated=True, + description=( + "This will always be `[0, 0, 0]`." + " Use the `GET /calibration/pipette_offset` endpoint instead." + ), + ) class InstrumentCalibrationStatus(BaseModel): @@ -59,7 +73,12 @@ class DeckCalibrationData(BaseModel): None, description="The ID of the pipette used in this calibration" ) tiprack: typing.Optional[str] = Field( - None, description="The sha256 hash of the tiprack used in this calibration" + None, + description="A hash of the labware definition of the tip rack that" + " was used in this calibration." + " This is deprecated because it was prone to bugs where semantically identical" + " definitions had different hashes.", + deprecated=True, ) source: typing.Optional[SourceType] = Field( None, description="The calibration source" diff --git a/robot-server/robot_server/service/legacy/models/networking.py b/robot-server/robot_server/service/legacy/models/networking.py index c8c7a1fd2d7..5b3351fdb3f 100644 --- a/robot-server/robot_server/service/legacy/models/networking.py +++ b/robot-server/robot_server/service/legacy/models/networking.py @@ -98,11 +98,11 @@ class WifiNetworkFull(WifiNetwork): signal: int = Field( ..., - description="A unitless signal strength; a higher number is a " "better signal", + description="A unitless signal strength; a higher number is a better signal", ) active: bool = Field(..., description="Whether there is a connection active") security: str = Field( - ..., description="The raw NetworkManager output about the wifi " "security" + ..., description="The raw NetworkManager output about the Wi-Fi security" ) securityType: NetworkingSecurityType @@ -133,32 +133,32 @@ class WifiConfiguration(BaseModel): ..., description="The SSID to connect to. If this isn't an SSID that " "is being broadcast by a network, you " - "should also set hidden to true.", + "should also set `hidden` to `true`.", ) hidden: typing.Optional[bool] = Field( False, - description="True if the network is hidden (not broadcasting an " - "ssid). False (default if key is not " - "present) otherwise", + description="`true` if the network is hidden (not broadcasting an SSID). " + "`false` (default if key is not " + "present) otherwise.", ) securityType: typing.Optional[NetworkingSecurityType] psk: typing.Optional[SecretStr] = Field( None, - description="If this is a PSK-secured network (securityType is " - "wpa-psk), the PSK", + description="If this is a PSK-secured network (`securityType` is " + '`"wpa-psk"`), the PSK', ) eapConfig: typing.Optional[typing.Dict[str, str]] = Field( None, description="All options required to configure EAP access to the" - " wifi. All options should match one of the cases " - "described in /wifi/eap-options; for instance, " + " Wi-Fi. All options should match one of the cases " + "described in `/wifi/eap-options`; for instance, " "configuring for peap/mschapv2 should have " - '"peap/mschapv2" as the eapType; it should have ' - '"identity" and "password" props, both of which ' - "are identified as mandatory in /wifi/eap-options; " - 'and it may also have "anonymousIdentity" and ' - '"caCert" properties, both of which are identified' + '`"peap/mschapv2"` as the `eapType`; it should have ' + '`"identity"` and `"password"` props, both of which ' + "are identified as mandatory in `/wifi/eap-options`; " + 'and it may also have `"anonymousIdentity"` and ' + '`"caCert"` properties, both of which are identified' " as present but not required.", required=["eapType"], ) diff --git a/robot-server/robot_server/service/legacy/models/settings.py b/robot-server/robot_server/service/legacy/models/settings.py index 1c8f2c1a96d..f77977dbe3a 100644 --- a/robot-server/robot_server/service/legacy/models/settings.py +++ b/robot-server/robot_server/service/legacy/models/settings.py @@ -17,6 +17,7 @@ class AdvancedSetting(BaseModel): ..., description="The ID by which the property used to be known; not" " useful now and may contain spaces or hyphens", + deprecated=True, ) title: str = Field( ..., diff --git a/robot-server/robot_server/service/legacy/routers/control.py b/robot-server/robot_server/service/legacy/routers/control.py index 4ed3240af8d..d3713b81bee 100644 --- a/robot-server/robot_server/service/legacy/routers/control.py +++ b/robot-server/robot_server/service/legacy/routers/control.py @@ -24,7 +24,8 @@ @router.post( "/identify", - description="Blink the OT-2's gantry lights so you can pick it " "out of a crowd", + summary="Blink the lights", + description="Blink the gantry lights so you can pick it out of a crowd", ) async def post_identify( seconds: int = Query(..., description="Time to blink the lights for"), @@ -37,8 +38,15 @@ async def post_identify( @router.get( "/robot/positions", - description="Get a list of useful positions", + summary="Get robot positions", + description=( + "Get a list of useful positions." + "\n\n" + "**Deprecated:** This data only makes sense for OT-2 robots, not Flex robots." + " There is currently no public way to get these positions for Flex robots." + ), response_model=control.RobotPositionsResponse, + deprecated=True, ) async def get_robot_positions() -> control.RobotPositionsResponse: """ @@ -60,14 +68,20 @@ async def get_robot_positions() -> control.RobotPositionsResponse: @router.post( path="/robot/move", + summary="Move the robot", description=( "Move the robot's gantry to a position (usually to a " - "position retrieved from GET /robot/positions)" + "position retrieved from `GET /robot/positions`)." + "\n\n" + "**Deprecated:**" + " Run a `moveToCoordinates` command in a maintenance run instead." + " See the `/maintenance_runs` endpoints." ), response_model=V1BasicResponse, responses={ status.HTTP_403_FORBIDDEN: {"model": LegacyErrorResponse}, }, + deprecated=True, ) async def post_move_robot( robot_move_target: control.RobotMoveTarget, @@ -85,7 +99,8 @@ async def post_move_robot( @router.post( path="/robot/home", - description="Home the robot", + summary="Home the robot", + description="Home the robot.", response_model=V1BasicResponse, responses={ status.HTTP_400_BAD_REQUEST: {"model": LegacyErrorResponse}, @@ -126,7 +141,8 @@ async def post_home_robot( @router.get( "/robot/lights", - description="Get the current status of the OT-2's rail lights", + summary="Get whether the lights are on", + description="Get the current status of the robot's rail lights", response_model=control.RobotLightState, ) async def get_robot_light_state( @@ -138,6 +154,7 @@ async def get_robot_light_state( @router.post( "/robot/lights", + summary="Turn the lights on or off", description="Turn the rail lights on or off", response_model=control.RobotLightState, ) diff --git a/robot-server/robot_server/service/legacy/routers/logs.py b/robot-server/robot_server/service/legacy/routers/logs.py index 224c38482dd..fe270611eb7 100644 --- a/robot-server/robot_server/service/legacy/routers/logs.py +++ b/robot-server/robot_server/service/legacy/routers/logs.py @@ -2,6 +2,7 @@ from typing import Dict from opentrons.system import log_control + from robot_server.service.legacy.models.logs import LogIdentifier, LogFormat router = APIRouter() @@ -15,7 +16,18 @@ } -@router.get("/logs/{log_identifier}", description="Get logs from the robot.") +@router.get( + path="/logs/{log_identifier}", + summary="Get troubleshooting logs", + description=( + "Get the robot's troubleshooting logs." + "\n\n" + "If you want the list of steps executed in a protocol," + ' like "aspirated 5 µL from well A1...", you probably want the' + " *protocol analysis commands* (`GET /protocols/{id}/analyses/{id}`)" + " or *run commands* (`GET /runs/{id}/commands`) instead." + ), +) async def get_logs( log_identifier: LogIdentifier, response: Response, diff --git a/robot-server/robot_server/service/legacy/routers/modules.py b/robot-server/robot_server/service/legacy/routers/modules.py index 71f40f7eee6..a0051941541 100644 --- a/robot-server/robot_server/service/legacy/routers/modules.py +++ b/robot-server/robot_server/service/legacy/routers/modules.py @@ -63,14 +63,18 @@ async def get_modules( description=( "Command a module to take an action. Valid actions " "depend on the specific module attached, which is " - "the model value from GET /modules/{serial}/data or " - "GET /modules" + "the model value from `GET /modules/{serial}/data` or " + "`GET /modules`." + "\n\n" + "**Deprecated:** Removed with `Opentrons-Version: 3`." + " Use `POST /commands` instead." ), response_model=SerialCommandResponse, responses={ status.HTTP_400_BAD_REQUEST: {"model": LegacyErrorResponse}, status.HTTP_404_NOT_FOUND: {"model": LegacyErrorResponse}, }, + deprecated=True, ) async def post_serial_command( command: SerialCommand, diff --git a/robot-server/robot_server/service/legacy/routers/networking.py b/robot-server/robot_server/service/legacy/routers/networking.py index 6f82269da0b..869fab1b139 100644 --- a/robot-server/robot_server/service/legacy/routers/networking.py +++ b/robot-server/robot_server/service/legacy/routers/networking.py @@ -62,18 +62,18 @@ async def get_networking_status() -> NetworkingStatus: "/wifi/list", summary="Scan for visible Wi-Fi networks", description="Returns the list of the visible wifi networks " - "along with some data about their security and strength. " - "Only use rescan=True based on the user needs like clicking on" - "the scan network button and not to just poll.", + "along with some data about their security and strength.", response_model=WifiNetworks, ) async def get_wifi_networks( rescan: Optional[bool] = Query( default=False, description=( - "If `true` it forces a rescan for beaconing WiFi networks, " - "this is an expensive operation which can take ~10 seconds." - "If `false` it returns the cached wifi networks, " + "If `true`, forces a rescan for beaconing Wi-Fi networks. " + "This is an expensive operation that can take ~10 seconds, " + 'so only do it based on user needs like clicking a "scan network" ' + "button, not just to poll. " + "If `false`, returns the cached Wi-Fi networks, " "letting the system decide when to do a rescan." ), ) @@ -123,6 +123,7 @@ async def post_wifi_configure( @router.get( "/wifi/keys", + summary="Get Wi-Fi keys", description="Get a list of key files known to the system", response_model=WifiKeyFiles, response_model_by_alias=True, @@ -146,6 +147,7 @@ async def get_wifi_keys(): @router.post( "/wifi/keys", + summary="Add a Wi-Fi key", description="Send a new key file to the robot", responses={ status.HTTP_200_OK: {"model": AddWifiKeyFileResponse}, @@ -179,6 +181,7 @@ async def post_wifi_key(key: UploadFile = File(...)): @router.delete( path="/wifi/keys/{key_uuid}", + summary="Delete a Wi-Fi key", description="Delete a key file from the robot", response_model=V1BasicResponse, responses={ @@ -204,6 +207,7 @@ async def delete_wifi_key( @router.get( "/wifi/eap-options", + summary="Get EAP options", description="Get the supported EAP variants and their " "configuration parameters", response_model=EapOptions, ) diff --git a/robot-server/robot_server/service/legacy/routers/pipettes.py b/robot-server/robot_server/service/legacy/routers/pipettes.py index 96d9cdfd599..8bcbc4cf1cc 100644 --- a/robot-server/robot_server/service/legacy/routers/pipettes.py +++ b/robot-server/robot_server/service/legacy/routers/pipettes.py @@ -20,21 +20,23 @@ summary="Get the pipettes currently attached", description="This endpoint lists properties of the pipettes " "currently attached to the robot like name, model, " - "and mount. It queries a cached value unless the " - "refresh query parameter is set to true, in which " - "case it will actively scan for pipettes. This " - "requires disabling the pipette motors (which is done " - "automatically) and therefore should only be done " - "through user intent.", + "and mount." + "\n\n" + "If you're controlling a Flex, and not an OT-2, you might prefer the" + " `GET /instruments` endpoint instead.", response_model=pipettes.PipettesByMount, ) async def get_pipettes( refresh: typing.Optional[bool] = Query( False, - description="If true, actively scan for attached pipettes. Note:" - " this requires disabling the pipette motors and" - " should only be done when no protocol is running " - "and you know it won't cause a problem", + description="If `false`, query a cached value. If `true`, actively scan for" + " attached pipettes." + "\n\n" + "**Warning:** Actively scanning disables the pipette motors and should only be done" + " when no protocol is running and you know it won't cause a problem." + "\n\n" + "**Warning:** Actively scanning is only valid on OT-2s. On Flex robots, it's" + " unnecessary, and the behavior is currently undefined.", ), hardware: HardwareControlAPI = Depends(get_hardware), ) -> pipettes.PipettesByMount: diff --git a/robot-server/robot_server/service/legacy/routers/settings.py b/robot-server/robot_server/service/legacy/routers/settings.py index b594aee5f49..16a732ff97f 100644 --- a/robot-server/robot_server/service/legacy/routers/settings.py +++ b/robot-server/robot_server/service/legacy/routers/settings.py @@ -66,6 +66,7 @@ @router.post( path="/settings", + summary="Change a setting", description="Change an advanced setting (feature flag)", response_model=AdvancedSettingsResponse, response_model_exclude_unset=True, @@ -96,6 +97,7 @@ async def post_settings( @router.get( "/settings", + summary="Get settings", description="Get a list of available advanced settings (feature " "flags) and their values", response_model=AdvancedSettingsResponse, @@ -142,6 +144,7 @@ def _create_settings_response(robot_type: str) -> AdvancedSettingsResponse: @router.post( path="/settings/log_level/local", + summary="Set the local log level", description="Set the minimum level of logs saved locally", response_model=V1BasicResponse, responses={ @@ -172,6 +175,7 @@ async def post_log_level_local( @router.post( path="/settings/log_level/upstream", + summary="Set the upstream log level", description=( "Set the minimum level of logs sent upstream via" " syslog-ng to Opentrons." @@ -189,7 +193,8 @@ async def post_log_level_upstream(log_level: LogLevel) -> V1BasicResponse: @router.get( "/settings/reset/options", - description="Get the settings that can be reset as part of factory reset", + summary="Get the things that can be reset", + description="Get the robot settings and data that can be reset through `POST /settings/reset`.", response_model=FactoryResetOptions, ) async def get_settings_reset_options( @@ -206,7 +211,15 @@ async def get_settings_reset_options( @router.post( "/settings/reset", - description="Perform a factory reset of some robot data", + summary="Reset specific settings or data", + description=( + "Perform a reset of the requested robot settings or data." + "\n\n" + "The valid properties are given by `GET /settings/reset/options`." + "\n\n" + "You should always restart the robot after using this endpoint to" + " reset something." + ), responses={ status.HTTP_403_FORBIDDEN: {"model": LegacyErrorResponse}, status.HTTP_503_SERVICE_UNAVAILABLE: {"model": LegacyErrorResponse}, diff --git a/robot-server/robot_server/service/pipette_offset/models.py b/robot-server/robot_server/service/pipette_offset/models.py index 23c65821125..401afc29273 100644 --- a/robot-server/robot_server/service/pipette_offset/models.py +++ b/robot-server/robot_server/service/pipette_offset/models.py @@ -34,7 +34,11 @@ class PipetteOffsetCalibration(DeprecatedResponseDataModel): ..., description="The pipette offset vector", max_items=3, min_items=3 ) tiprack: str = Field( - ..., description="The sha256 hash of the tiprack used " "in this calibration" + ..., + description="A hash of the labware definition of the tip rack" + " that was used in this calibration." + " This is deprecated because it was prone to bugs where semantically identical" + " definitions had different hashes. Use `tiprackUri` instead.", ) tiprackUri: str = Field( ..., diff --git a/robot-server/robot_server/service/session/router.py b/robot-server/robot_server/service/session/router.py index db127b304f4..b4b77a6b06b 100644 --- a/robot-server/robot_server/service/session/router.py +++ b/robot-server/robot_server/service/session/router.py @@ -44,7 +44,14 @@ def get_session(manager: SessionManager, session_id: IdentifierType) -> BaseSess @router.post( "/sessions", - description="Create a session", + summary="Create an OT-2 calibration session", + description=( + "Create a session to perform a calibration procedure on an OT-2." + "\n\n" + "**Warning:** These `/sessions/` endpoints are tightly coupled to the" + " Opentrons App and are not intended for general public use." + ), + deprecated=True, response_model=SessionResponse, status_code=http_status_codes.HTTP_201_CREATED, ) @@ -52,7 +59,6 @@ async def create_session_handler( create_request: SessionCreateRequest, session_manager: SessionManager = Depends(get_session_manager), ) -> SessionResponse: - """Create a session""" session_type = create_request.data.sessionType create_params = create_request.data.createParams @@ -68,13 +74,19 @@ async def create_session_handler( @router.delete( - PATH_SESSION_BY_ID, description="Delete a session", response_model=SessionResponse + PATH_SESSION_BY_ID, + summary="Delete an OT-2 calibration session", + description=( + "**Warning:** These `/sessions/` endpoints are tightly coupled to the" + " Opentrons App and are not intended for general public use." + ), + deprecated=True, + response_model=SessionResponse, ) async def delete_session_handler( sessionId: IdentifierType, session_manager: SessionManager = Depends(get_session_manager), ) -> SessionResponse: - """Delete a session""" session_obj = get_session(manager=session_manager, session_id=sessionId) await session_manager.remove(session_obj.meta.identifier) @@ -85,7 +97,14 @@ async def delete_session_handler( @router.get( - PATH_SESSION_BY_ID, description="Get session", response_model=SessionResponse + PATH_SESSION_BY_ID, + summary="Get an OT-2 calibration session", + description=( + "**Warning:** These `/sessions/` endpoints are tightly coupled to the" + " Opentrons App and are not intended for general public use." + ), + deprecated=True, + response_model=SessionResponse, ) async def get_session_handler( sessionId: IdentifierType, @@ -100,7 +119,14 @@ async def get_session_handler( @router.get( - "/sessions", description="Get all the sessions", response_model=MultiSessionResponse + "/sessions", + summary="Get all OT-2 calibration sessions", + description=( + "**Warning:** These `/sessions/` endpoints are tightly coupled to the" + " Opentrons App and are not intended for general public use." + ), + deprecated=True, + response_model=MultiSessionResponse, ) async def get_sessions_handler( session_type: SessionType = Query( @@ -108,7 +134,6 @@ async def get_sessions_handler( ), session_manager: SessionManager = Depends(get_session_manager), ) -> MultiSessionResponse: - """Get multiple sessions""" sessions = session_manager.get(session_type=session_type) return MultiSessionResponse( data=[session.get_response_model() for session in sessions], @@ -118,7 +143,12 @@ async def get_sessions_handler( @router.post( f"{PATH_SESSION_BY_ID}/commands/execute", - description="Create and execute a command immediately", + summary="Execute an OT-2 calibration command", + description=( + "**Warning:** These `/sessions/` endpoints are tightly coupled to the" + " Opentrons App and are not intended for general public use." + ), + deprecated=True, response_model=CommandResponse, ) async def session_command_execute_handler( @@ -126,9 +156,6 @@ async def session_command_execute_handler( command_request: CommandRequest, session_manager: SessionManager = Depends(get_session_manager), ) -> CommandResponse: - """ - Execute a session command - """ session_obj = get_session(manager=session_manager, session_id=sessionId) if not session_manager.is_active(session_obj.meta.identifier): raise CommandExecutionException( diff --git a/robot-server/robot_server/service/tip_length/models.py b/robot-server/robot_server/service/tip_length/models.py index c8126e4e6f3..2ff8f81b5ef 100644 --- a/robot-server/robot_server/service/tip_length/models.py +++ b/robot-server/robot_server/service/tip_length/models.py @@ -17,7 +17,15 @@ class TipLengthCalibration(DeprecatedResponseDataModel): """ tipLength: float = Field(..., description="The tip length value in mm") - tiprack: str = Field(..., description="The sha256 hash of the tiprack") + tiprack: str = Field( + ..., + description="A hash of the labware definition of the tip rack that" + " was used in this calibration." + " This is deprecated because it was prone to bugs where semantically identical" + " definitions had different hashes." + " Use `uri` instead.", + deprecated=True, + ) pipette: str = Field(..., description="The pipette ID") lastModified: datetime = Field( ..., description="When this calibration was last modified" diff --git a/robot-server/robot_server/service/tip_length/router.py b/robot-server/robot_server/service/tip_length/router.py index cb496330bc0..7758a96e8b8 100644 --- a/robot-server/robot_server/service/tip_length/router.py +++ b/robot-server/robot_server/service/tip_length/router.py @@ -1,5 +1,5 @@ from starlette import status -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Query from typing import Optional, cast from opentrons.calibration_storage import types as cal_types @@ -49,9 +49,23 @@ def _format_calibration( response_model=tl_models.MultipleCalibrationsResponse, ) async def get_all_tip_length_calibrations( - tiprack_hash: Optional[str] = None, - pipette_id: Optional[str] = None, - tiprack_uri: Optional[str] = None, + tiprack_hash: Optional[str] = Query( + None, + description=( + "Filter results by their `tiprack` field." + " This is deprecated because it was prone to bugs where semantically identical" + " definitions had different hashes." + " Use `tiprack_uri` instead." + ), + deprecated=True, + ), + pipette_id: Optional[str] = Query( + None, description="Filter results by their `pipette` field." + ), + tiprack_uri: Optional[str] = Query( + None, + description="Filter results by their `uri` field.", + ), _: API = Depends(get_ot2_hardware), ) -> tl_models.MultipleCalibrationsResponse: all_calibrations = tip_length.get_all_tip_length_calibrations() @@ -85,14 +99,45 @@ async def get_all_tip_length_calibrations( responses={status.HTTP_404_NOT_FOUND: {"model": ErrorBody}}, ) async def delete_specific_tip_length_calibration( - pipette_id: str, - tiprack_hash: Optional[str] = None, - tiprack_uri: Optional[str] = None, + pipette_id: str = Query( + ..., + description=( + "The `pipette` field value of the calibration you want to delete." + " (See `GET /calibration/tip_length`.)" + ), + ), + tiprack_hash: Optional[str] = Query( + None, + description=( + "The `tiprack` field value of the calibration you want to delete." + " (See `GET /calibration/tip_length`.)" + "\n\n" + " This is deprecated because it was prone to bugs where semantically identical" + " definitions had different hashes." + " Use `tiprack_uri` instead." + "\n\n" + "You must supply either this or `tiprack_uri`." + ), + deprecated=True, + ), + tiprack_uri: Optional[str] = Query( + None, + description=( + "The `uri` field value of the calibration you want to delete." + " (See `GET /calibration/tip_length`.)" + "\n\n" + " You must supply either this or `tiprack_hash`." + ), + ), _: API = Depends(get_ot2_hardware), ): try: tip_length.delete_tip_length_calibration( pipette_id, + # TODO(mm, 2024-03-06): This is a dangerous cast if, for example, the client + # supplies an invalid URI without slashes, and something internal tries to + # split it on slashes. We should have a custom Pydantic type so FastAPI can + # return a 422 error. tiprack_uri=cast(LabwareUri, tiprack_uri), tiprack_hash=tiprack_hash, ) diff --git a/robot-server/robot_server/subsystems/router.py b/robot-server/robot_server/subsystems/router.py index bb2786b9e70..bf0ca0edac8 100644 --- a/robot-server/robot_server/subsystems/router.py +++ b/robot-server/robot_server/subsystems/router.py @@ -109,8 +109,8 @@ class NoOngoingUpdate(ErrorDetails): @PydanticResponse.wrap_route( subsystems_router.get, path="/subsystems/status", - summary="Get attached subsystems.", - description="Get a list of subsystems currently attached to the robot.", + summary="Get all attached subsystems", + description="Get the details of all hardware subsystems attached to the robot.", responses={ status.HTTP_200_OK: {"model": SimpleMultiBody[PresentSubsystem]}, status.HTTP_403_FORBIDDEN: {"model": ErrorBody[NotSupportedOnOT2]}, @@ -141,6 +141,8 @@ async def get_attached_subsystems( @PydanticResponse.wrap_route( subsystems_router.get, path="/subsystems/status/{subsystem}", + summary="Get a specific attached subsystem", + description="Get the details of a single hardware subsystem attached to the robot.", responses={ status.HTTP_200_OK: {"model": SimpleBody[PresentSubsystem]}, status.HTTP_403_FORBIDDEN: {"model": ErrorBody[NotSupportedOnOT2]}, @@ -178,8 +180,13 @@ async def get_attached_subsystem( @PydanticResponse.wrap_route( subsystems_router.get, path="/subsystems/updates/current", - summary="Get a list of currently-ongoing subsystem updates.", - description="Get a list of currently-running subsystem firmware updates. This is a good snapshot of what, if anything, is currently being updated and may block other robot work. To guarantee data about an update you were previously interested in, get its id using /subsystems/updates/all.", + summary="Get all ongoing subsystem updates", + description=( + "Get a list of currently-running subsystem firmware updates." + " This is a good snapshot of what, if anything, is currently being updated" + " and may block other robot work. To guarantee data about an update you were" + " previously interested in, get its `id` using `/subsystems/updates/all`." + ), responses={status.HTTP_200_OK: {"model": SimpleMultiBody[UpdateProgressSummary]}}, ) async def get_subsystem_updates( @@ -205,8 +212,8 @@ async def get_subsystem_updates( @PydanticResponse.wrap_route( subsystems_router.get, path="/subsystems/updates/current/{subsystem}", - summary="Get any currently-ongoing update for a specific subsystem.", - description="As /subsystems/updates/current but filtered by the route parameter.", + summary="Get the ongoing update for a specific subsystem", + description="As `/subsystems/updates/current`, but filtered by the route parameter.", responses={ status.HTTP_200_OK: {"model": SimpleBody[UpdateProgressData]}, status.HTTP_404_NOT_FOUND: {"model": ErrorBody[NoOngoingUpdate]}, @@ -243,8 +250,15 @@ async def get_subsystem_update( @PydanticResponse.wrap_route( subsystems_router.get, path="/subsystems/updates/all", - summary="Get a list of all updates by id.", - description="Get a list of all updates, including both current updates and updates that started since the last boot but are now complete. Response includes each update's final status and whether it succeeded or failed. While an update might complete and therefore disappear from /subsystems/updates/current, you can always find that update in the response to this endpoint by its update id.", + summary="Get all subsystem updates", + description=( + "Get a list of all updates, including both ongoing updates and updates that" + " started since the last boot but are now complete." + "\n\n" + " While an update might complete and therefore disappear from" + "`/subsystems/updates/current`, you can always find that update in the response" + " to this endpoint by its `id`." + ), responses={status.HTTP_200_OK: {"model": SimpleMultiBody[UpdateProgressData]}}, ) async def get_update_processes( @@ -269,8 +283,8 @@ async def get_update_processes( @PydanticResponse.wrap_route( subsystems_router.get, path="/subsystems/updates/all/{id}", - summary="Get the details of a specific update by its id.", - description="As /subsystems/updates/all but returning only one resource: the one with the id matching the route parameter (if it exists).", + summary="Get a specific subsystem update", + description="As `/subsystems/updates/all`, but returning only one resource: the one with the `id` matching the route parameter (if it exists).", responses={status.HTTP_200_OK: {"model": SimpleBody[UpdateProgressData]}}, ) async def get_update_process( @@ -300,7 +314,7 @@ async def get_update_process( @PydanticResponse.wrap_route( subsystems_router.post, path="/subsystems/updates/{subsystem}", - summary="Start an update for a subsystem.", + summary="Start an update for a subsystem", description="Begin a firmware update for a given subsystem.", responses={ status.HTTP_201_CREATED: {"model": SimpleBody[UpdateProgressData]}, diff --git a/robot-server/robot_server/versioning.py b/robot-server/robot_server/versioning.py index 57d22a81478..3a53dfead24 100644 --- a/robot-server/robot_server/versioning.py +++ b/robot-server/robot_server/versioning.py @@ -59,9 +59,10 @@ async def check_version_header( opentrons_version: Union[Literal["*"], int] = Header( ..., description=( - "The HTTP API version to use for this request. Must be " - f"'{MIN_API_VERSION}' or higher. To use the latest " - f"version unconditionally, specify '{LATEST_API_VERSION_HEADER_VALUE}'" + f"The HTTP API version to use for this request." + f" Must be `{MIN_API_VERSION}` or higher." + f" To use the latest version unconditionally," + f" specify `{LATEST_API_VERSION_HEADER_VALUE}`." ), ), ) -> None: