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

refactor(robot-server): split apart experimental sessions router #8057

Merged
merged 3 commits into from
Jul 7, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
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 endpoints."""
mcous marked this conversation as resolved.
Show resolved Hide resolved
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