Skip to content

Commit

Permalink
refactor(robot-server): split apart experimental sessions router (#8057)
Browse files Browse the repository at this point in the history
Co-authored-by: Sanniti Pimpley <[email protected]>
  • Loading branch information
mcous and sanni-t authored Jul 7, 2021
1 parent 4d8529a commit 5549266
Show file tree
Hide file tree
Showing 11 changed files with 985 additions and 965 deletions.
14 changes: 14 additions & 0 deletions robot-server/robot_server/sessions/router/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Sessions router."""
from fastapi import APIRouter

from .base_router import base_router
from .commands_router import commands_router
from .actions_router import actions_router

sessions_router = APIRouter()

sessions_router.include_router(base_router)
sessions_router.include_router(commands_router)
sessions_router.include_router(actions_router)

__all__ = ["sessions_router"]
89 changes: 89 additions & 0 deletions robot-server/robot_server/sessions/router/actions_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""Router for /sessions actions endpoints."""
from fastapi import APIRouter, Depends, status
from datetime import datetime
from typing_extensions import Literal

from robot_server.errors import ErrorDetails, ErrorResponse
from robot_server.service.dependencies import get_current_time, get_unique_id
from robot_server.service.task_runner import TaskRunner
from robot_server.service.json_api import RequestModel, ResponseModel

from ..session_store import SessionStore, SessionNotFoundError
from ..session_view import SessionView
from ..action_models import SessionAction, SessionActionCreateData
from ..engine_store import EngineStore, EngineMissingError
from ..dependencies import get_session_store, get_engine_store
from .base_router import SessionNotFound

actions_router = APIRouter()


class SessionActionNotAllowed(ErrorDetails):
"""An error if one tries to issue an unsupported session action."""

id: Literal["SessionActionNotAllowed"] = "SessionActionNotAllowed"
title: str = "Session Action Not Allowed"


@actions_router.post(
path="/sessions/{sessionId}/actions",
summary="Issue a control action to the session",
description=(
"Provide an action to the session in order to control execution of the run."
),
status_code=status.HTTP_201_CREATED,
response_model=ResponseModel[SessionAction],
responses={
status.HTTP_400_BAD_REQUEST: {"model": ErrorResponse[SessionActionNotAllowed]},
status.HTTP_404_NOT_FOUND: {"model": ErrorResponse[SessionNotFound]},
},
)
async def create_session_action(
sessionId: str,
request_body: RequestModel[SessionActionCreateData],
session_view: SessionView = Depends(SessionView),
session_store: SessionStore = Depends(get_session_store),
engine_store: EngineStore = Depends(get_engine_store),
action_id: str = Depends(get_unique_id),
created_at: datetime = Depends(get_current_time),
task_runner: TaskRunner = Depends(TaskRunner),
) -> ResponseModel[SessionAction]:
"""Create a session control action.
Arguments:
sessionId: Session ID pulled from the URL.
request_body: Input payload from the request body.
session_view: Resource model builder.
session_store: Session storage interface.
engine_store: Protocol engine and runner storage.
action_id: Generated ID to assign to the control action.
created_at: Timestamp to attach to the control action.
task_runner: Background task runner.
"""
try:
prev_session = session_store.get(session_id=sessionId)

action, next_session = session_view.with_action(
session=prev_session,
action_id=action_id,
action_data=request_body.data,
created_at=created_at,
)

# TODO(mc, 2021-06-11): support actions other than `start`
# TODO(mc, 2021-06-24): ensure the engine homes pipette plungers
# before starting the protocol run
# TODO(mc, 2021-06-30): capture errors (e.g. uncaught Python raise)
# and place them in the session response
task_runner.run(engine_store.runner.run)

except SessionNotFoundError as e:
raise SessionNotFound(detail=str(e)).as_error(status.HTTP_404_NOT_FOUND)
except EngineMissingError as e:
raise SessionActionNotAllowed(detail=str(e)).as_error(
status.HTTP_400_BAD_REQUEST
)

session_store.upsert(session=next_session)

return ResponseModel(data=action)
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
"""Router for /sessions endpoints."""
"""Base router for /sessions endpoints.
Contains routes dealing primarily with `Session` models.
"""
from fastapi import APIRouter, Depends, status
from datetime import datetime
from typing import Optional, Union
from typing import Optional
from typing_extensions import Literal

from opentrons.protocol_engine import commands as pe_commands, errors as pe_errors

from robot_server.errors import ErrorDetails, ErrorResponse
from robot_server.service.dependencies import get_current_time, get_unique_id
from robot_server.service.task_runner import TaskRunner
from robot_server.service.json_api import (
RequestModel,
ResponseModel,
EmptyResponseModel,
MultiResponseModel,
Expand All @@ -23,15 +22,14 @@
get_protocol_store,
)

from .session_store import SessionStore, SessionNotFoundError
from .session_view import SessionView
from .session_models import Session, SessionCommandSummary, ProtocolSessionCreateData
from .action_models import SessionAction, SessionActionCreateData
from .schema_models import CreateSessionRequest, SessionResponse, SessionCommandResponse
from .engine_store import EngineStore, EngineConflictError, EngineMissingError
from .dependencies import get_session_store, get_engine_store
from ..session_store import SessionStore, SessionNotFoundError
from ..session_view import SessionView
from ..session_models import Session, ProtocolSessionCreateData
from ..schema_models import CreateSessionRequest, SessionResponse
from ..engine_store import EngineStore, EngineConflictError
from ..dependencies import get_session_store, get_engine_store

sessions_router = APIRouter()
base_router = APIRouter()


class SessionNotFound(ErrorDetails):
Expand All @@ -41,13 +39,6 @@ class SessionNotFound(ErrorDetails):
title: str = "Session Not Found"


class CommandNotFound(ErrorDetails):
"""An error if a given session command is not found."""

id: Literal["CommandNotFound"] = "CommandNotFound"
title: str = "Session Command Not Found"


# TODO(mc, 2021-05-28): evaluate multi-session logic
class SessionAlreadyActive(ErrorDetails):
"""An error if one tries to create a new session while one is already active."""
Expand All @@ -56,14 +47,7 @@ class SessionAlreadyActive(ErrorDetails):
title: str = "Session Already Active"


class SessionActionNotAllowed(ErrorDetails):
"""An error if one tries to issue an unsupported session action."""

id: Literal["SessionActionNotAllowed"] = "SessionActionNotAllowed"
title: str = "Session Action Not Allowed"


@sessions_router.post(
@base_router.post(
path="/sessions",
summary="Create a session",
description="Create a new session to track robot interaction.",
Expand Down Expand Up @@ -124,7 +108,7 @@ async def create_session(
return ResponseModel(data=data)


@sessions_router.get(
@base_router.get(
path="/sessions",
summary="Get all sessions",
description="Get a list of all active and inactive sessions.",
Expand Down Expand Up @@ -153,7 +137,7 @@ async def get_sessions(
return MultiResponseModel(data=data)


@sessions_router.get(
@base_router.get(
path="/sessions/{sessionId}",
summary="Get a session",
description="Get a specific session by its unique identifier.",
Expand Down Expand Up @@ -188,7 +172,7 @@ async def get_session(
return ResponseModel(data=data)


@sessions_router.delete(
@base_router.delete(
path="/sessions/{sessionId}",
summary="Delete a session",
description="Delete a specific session by its unique identifier.",
Expand All @@ -215,136 +199,3 @@ async def remove_session_by_id(
raise SessionNotFound(detail=str(e)).as_error(status.HTTP_404_NOT_FOUND)

return EmptyResponseModel()


@sessions_router.post(
path="/sessions/{sessionId}/actions",
summary="Issue a control action to the session",
description=(
"Provide an action to the session in order to control execution of the run."
),
status_code=status.HTTP_201_CREATED,
response_model=ResponseModel[SessionAction],
responses={
status.HTTP_400_BAD_REQUEST: {"model": ErrorResponse[SessionActionNotAllowed]},
status.HTTP_404_NOT_FOUND: {"model": ErrorResponse[SessionNotFound]},
},
)
async def create_session_action(
sessionId: str,
request_body: RequestModel[SessionActionCreateData],
session_view: SessionView = Depends(SessionView),
session_store: SessionStore = Depends(get_session_store),
engine_store: EngineStore = Depends(get_engine_store),
action_id: str = Depends(get_unique_id),
created_at: datetime = Depends(get_current_time),
task_runner: TaskRunner = Depends(TaskRunner),
) -> ResponseModel[SessionAction]:
"""Create a session control action.
Arguments:
sessionId: Session ID pulled from the URL.
request_body: Input payload from the request body.
session_view: Resource model builder.
session_store: Session storage interface.
engine_store: Protocol engine and runner storage.
action_id: Generated ID to assign to the control action.
created_at: Timestamp to attach to the control action.
task_runner: Background task runner.
"""
try:
prev_session = session_store.get(session_id=sessionId)

action, next_session = session_view.with_action(
session=prev_session,
action_id=action_id,
action_data=request_body.data,
created_at=created_at,
)

# TODO(mc, 2021-06-11): support actions other than `start`
# TODO(mc, 2021-06-24): ensure the engine homes pipette plungers
# before starting the protocol run
# TODO(mc, 2021-06-30): capture errors (e.g. uncaught Python raise)
# and place them in the session response
task_runner.run(engine_store.runner.run)

except SessionNotFoundError as e:
raise SessionNotFound(detail=str(e)).as_error(status.HTTP_404_NOT_FOUND)
except EngineMissingError as e:
raise SessionActionNotAllowed(detail=str(e)).as_error(
status.HTTP_400_BAD_REQUEST
)

session_store.upsert(session=next_session)

return ResponseModel(data=action)


@sessions_router.get(
path="/sessions/{sessionId}/commands",
summary="Get a list of all protocol commands in the session",
description=(
"Get a list of all commands in the session and their statuses. "
"This endpoint returns command summaries. Use "
"`GET /sessions/{sessionId}/commands/{commandId}` to get all "
"information available for a given command."
),
status_code=status.HTTP_200_OK,
response_model=MultiResponseModel[SessionCommandSummary],
responses={
status.HTTP_404_NOT_FOUND: {"model": ErrorResponse[SessionNotFound]},
},
)
async def get_session_commands(
session: ResponseModel[Session] = Depends(get_session),
) -> MultiResponseModel[SessionCommandSummary]:
"""Get a summary of all commands in a session.
Arguments:
session: Session response model, provided by the route handler for
`GET /session/{sessionId}`
"""
return MultiResponseModel(data=session.data.commands)


@sessions_router.get(
path="/sessions/{sessionId}/commands/{commandId}",
summary="Get full details about a specific command in the session",
description=(
"Get a command along with any associated payload, result, and "
"execution information."
),
status_code=status.HTTP_200_OK,
# TODO(mc, 2021-06-23): mypy >= 0.780 broke Unions as `response_model`
# see https://github.com/tiangolo/fastapi/issues/2279
response_model=SessionCommandResponse, # type: ignore[arg-type]
responses={
status.HTTP_404_NOT_FOUND: {
"model": Union[
ErrorResponse[SessionNotFound],
ErrorResponse[CommandNotFound],
]
},
},
)
async def get_session_command(
commandId: str,
engine_store: EngineStore = Depends(get_engine_store),
session: ResponseModel[Session] = Depends(get_session),
) -> ResponseModel[pe_commands.Command]:
"""Get a specific command from a session.
Arguments:
commandId: Command identifier, pulled from route parameter.
engine_store: Protocol engine and runner storage.
session: Session response model, provided by the route handler for
`GET /session/{sessionId}`. Present to ensure 404 if session
not found.
"""
try:
command = engine_store.engine.state_view.commands.get(commandId)
except pe_errors.CommandDoesNotExistError as e:
raise CommandNotFound(detail=str(e)).as_error(status.HTTP_404_NOT_FOUND)

return ResponseModel(data=command)
Loading

0 comments on commit 5549266

Please sign in to comment.