-
Notifications
You must be signed in to change notification settings - Fork 177
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
fix(robot-server): Refactor hardware wrapper and other lifetime dependencies #8213
Changes from all commits
e77dd78
686b1fa
3e89b33
fca92e0
752fabb
656a100
0f2ea16
d7b29eb
bf04ca4
4dfa729
bf31cc0
e6bc2e4
ab183c3
3e0f901
e3eb9bd
9428465
6a33398
c389c36
7c62bf8
bcb6397
f2d54fb
e42916b
602eb75
0afd56a
c0dfca8
c818120
697e2fd
5ab454b
1aa55bd
6552b3c
774cfb4
3a95844
b6d95af
3b854b2
dc5032d
b7ac395
8827b2f
3573b80
59d46f3
615e06a
8d06237
abd5d19
72a47dd
1c8d3b9
84a6ff6
a960dd1
b3593da
233ef3d
5343b36
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
"""Main FastAPI application.""" | ||
import logging | ||
|
||
|
||
from opentrons import __version__ | ||
from fastapi import FastAPI | ||
|
@@ -8,21 +8,15 @@ | |
from starlette.requests import Request | ||
from starlette.middleware.base import RequestResponseEndpoint | ||
|
||
from .service.dependencies import ( | ||
get_rpc_server, | ||
get_protocol_manager, | ||
get_hardware_wrapper, | ||
get_session_manager, | ||
) | ||
|
||
from .errors import exception_handlers | ||
from .router import router | ||
from .service import initialize_logging | ||
from . import lifetime_dependencies | ||
from . import constants | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
# Our global ASGI app object. Our ASGI server (currently Uvicorn) finds this and | ||
# sends web requests to it. | ||
app = FastAPI( | ||
title="Opentrons OT-2 HTTP API Spec", | ||
description=( | ||
|
@@ -35,13 +29,15 @@ | |
version=__version__, | ||
) | ||
|
||
|
||
# exception handlers | ||
# TODO(mc, 2021-05-10): after upgrade to FastAPI > 0.61.2, we can pass these | ||
# to FastAPI's `exception_handlers` arg instead. Current version has bug, see: | ||
# https://github.com/tiangolo/fastapi/pull/1924 | ||
for exc_cls, handler in exception_handlers.items(): | ||
app.add_exception_handler(exc_cls, handler) | ||
|
||
|
||
# cors | ||
app.add_middleware( | ||
CORSMiddleware, | ||
|
@@ -51,31 +47,9 @@ | |
allow_headers=["*"], | ||
) | ||
|
||
# main router | ||
app.include_router(router=router) | ||
|
||
|
||
@app.on_event("startup") | ||
async def on_startup() -> None: | ||
"""Handle app startup.""" | ||
initialize_logging() | ||
# Initialize api | ||
(await get_hardware_wrapper()).async_initialize() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ignoring the fact that it was broken, I dislike this removal. If I'm trying to establish context while reading this server code, the question "how is the hardware connection created?" is really important, and when we put it here, in the app-level startup handler, the pathway to find that answer is really obvious |
||
|
||
|
||
@app.on_event("shutdown") | ||
async def on_shutdown() -> None: | ||
"""Handle app shutdown.""" | ||
s = await get_rpc_server() | ||
await s.on_shutdown() | ||
# Remove all sessions | ||
await (await get_session_manager()).remove_all() | ||
# Remove all uploaded protocols | ||
(await get_protocol_manager()).remove_all() | ||
|
||
|
||
@app.middleware("http") | ||
async def api_version_response_header( | ||
async def _api_version_response_header( | ||
request: Request, | ||
call_next: RequestResponseEndpoint, | ||
) -> Response: | ||
|
@@ -90,3 +64,14 @@ async def api_version_response_header( | |
response.headers[constants.API_VERSION_HEADER] = str(request.state.api_version) | ||
response.headers[constants.MIN_API_VERSION_HEADER] = str(constants.MIN_API_VERSION) | ||
return response | ||
|
||
|
||
# main router | ||
app.include_router(router=router) | ||
|
||
|
||
app.on_event("startup")(initialize_logging) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comment from @mcous:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is FastAPI's My understanding based on reading a bunch of GitHub threads is:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
From reading the source, FastAPI does not define I think using the |
||
lifetime_dependencies.install_startup_shutdown_handlers(app) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Passing
I think (1) should be removed in favor of the app setup being in charge of attached handlers exposed by the |
||
|
||
|
||
__all__ = ["app"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
"""Utilities for initializing the hardware interface.""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Utilities" is always a bit of a warning sign for me. Something you need utilities, but if we have a hardware connection we need to initialize and hand out as a dependency, shouldn't those all be in the same place? Is there a way we can restructure this PR to have For example, if this module was to export 2 or 3 methods, I think everything would be a lot easier to follow:
|
||
|
||
|
||
import logging | ||
from pathlib import Path | ||
|
||
from opentrons import ThreadManager, initialize as initialize_api | ||
from opentrons.hardware_control.simulator_setup import load_simulator | ||
from opentrons.hardware_control.types import HardwareEvent, HardwareEventType | ||
from opentrons.util.helpers import utc_now | ||
|
||
from notify_server.clients.publisher import Publisher | ||
from notify_server.models import event, topics | ||
from notify_server.models.hardware_event import DoorStatePayload | ||
|
||
from .settings import get_settings | ||
|
||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
async def initialize(event_publisher: Publisher) -> ThreadManager: | ||
"""Initialize the API.""" | ||
# todo(mm, 2021-08-12): It would be easier for tests to check that this function | ||
# can successfully initialize simulating hardware if app_settings were passed in | ||
# as an argument, instead of being fetched here. | ||
app_settings = get_settings() | ||
thread_manager: ThreadManager | ||
if app_settings.simulator_configuration_file_path: | ||
log.info( | ||
f"Loading simulator from " | ||
f"{app_settings.simulator_configuration_file_path}" | ||
) | ||
# A path to a simulation configuration is defined. Let's use it. | ||
thread_manager = ThreadManager( | ||
load_simulator, Path(app_settings.simulator_configuration_file_path) | ||
) | ||
else: | ||
# Create the hardware | ||
thread_manager = await initialize_api() | ||
|
||
log.info("Starting hardware-event-notify publisher") | ||
door_event_forwarder = DoorEventForwarder(event_publisher) | ||
# fixme(mm, 2021-08-12): This might be a typing error. forward() can only | ||
# take a HardwareEvent, but it looks like ThreadManager can also pass | ||
# other things as the argument to a callback? | ||
await thread_manager.register_callback(door_event_forwarder.forward) | ||
|
||
log.info("Opentrons API initialized") | ||
return thread_manager | ||
|
||
|
||
class DoorEventForwarder: | ||
"""Forwards front door open/close events to a notify-server publisher.""" | ||
|
||
def __init__(self, publisher: Publisher) -> None: | ||
"""Initialize the `DoorEventForwarder`. | ||
|
||
Params: | ||
|
||
publisher: Where subsequent calls to `forward_hardware_event` will forward | ||
to. | ||
""" | ||
self.publisher = publisher | ||
|
||
def forward(self, hardware_event: HardwareEvent) -> None: | ||
"""Forward `hardware_event` if it's a door event. | ||
|
||
Otherwise, no-op. | ||
""" | ||
if hardware_event.event == HardwareEventType.DOOR_SWITCH_CHANGE: | ||
payload = DoorStatePayload(state=hardware_event.new_state) | ||
else: | ||
return | ||
topic = topics.RobotEventTopics.HARDWARE_EVENTS | ||
publisher = "robot_server_event_publisher" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ℹ️ The |
||
event_to_publish = event.Event( | ||
createdOn=utc_now(), publisher=publisher, data=payload | ||
) | ||
self.publisher.send_nowait(topic, event_to_publish) | ||
|
||
|
||
__all__ = ["initialize", "DoorEventForwarder"] |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ℹ️ This rename avoids ambiguity in
from robot_server import app
. (Would it import theapp
FastAPI object, or theapp
module?)