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

Add events api #91

Merged
merged 5 commits into from
Feb 24, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
133 changes: 133 additions & 0 deletions backend/src/flotilla/api/events_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
from http import HTTPStatus
from logging import getLogger
from typing import List

from fastapi import APIRouter, Body, Depends, Path, Response, Security
from flotilla_openapi.models.event import Event
from flotilla_openapi.models.event_request import EventRequest
from flotilla_openapi.models.problem_details import ProblemDetails
from pytest import Session

from flotilla.api.authentication import authentication_scheme
from flotilla.database.crud import (
DBException,
create_event,
read_event_by_id,
read_events,
remove_event,
)
from flotilla.database.db import get_db
from flotilla.database.models import EventDBModel

logger = getLogger("api")

router = APIRouter()

NOT_FOUND_DESCRIPTION = "Not Found - No event with given id"
INTERNAL_SERVER_ERROR_DESCRIPTION = "Internal Server Error"


@router.get(
"/events",
responses={
HTTPStatus.OK.value: {
"model": List[Event],
"description": "Request successful.",
},
},
tags=["Events"],
summary="Lookup events",
dependencies=[Security(authentication_scheme)],
)
async def get_events(
db: Session = Depends(get_db),
) -> List[Event]:
"""Lookup events."""
db_events: List[EventDBModel] = read_events(db)
events: List[Event] = [event.get_api_event() for event in db_events]
return events


@router.post(
"/events",
responses={
HTTPStatus.CREATED.value: {"model": Event, "description": "Request successful"},
},
tags=["Events"],
summary="Create new event",
dependencies=[Security(authentication_scheme)],
)
async def post_event(
response: Response,
db: Session = Depends(get_db),
event_request: EventRequest = Body(None, description="Time entry update"),
) -> Event:
"""Add a new event to the robot schedule"""
try:
event_id: int = create_event(
db,
event_request.robot_id,
event_request.mission_id,
event_request.start_time,
)
db_event: EventDBModel = read_event_by_id(db, event_id)
event: EventDBModel = db_event.get_api_event()
except DBException:
logger.exception(f"An error occured while creating event.")
response.status_code = HTTPStatus.INTERNAL_SERVER_ERROR.value
return ProblemDetails(title=INTERNAL_SERVER_ERROR_DESCRIPTION)
response.status_code = HTTPStatus.CREATED.value
return event


@router.delete(
"/events/{event_id}",
responses={
HTTPStatus.NO_CONTENT.value: {"description": "Event successfully deleted"},
},
tags=["Events"],
summary="Delete event with specified id",
dependencies=[Security(authentication_scheme)],
)
async def delete_event(
response: Response,
db: Session = Depends(get_db),
event_id: int = Path(None, description=""),
) -> None:
"""Deletes an event from the robot schedule. Can only be used for events that have not started yet."""
try:
remove_event(db, event_id)
except DBException:
logger.exception(f"Could not delete event with id {event_id}.")
response.status_code = HTTPStatus.NOT_FOUND.value
return ProblemDetails(title=NOT_FOUND_DESCRIPTION)
response.status_code = HTTPStatus.NO_CONTENT.value
return None


@router.get(
"/events/{event_id}",
responses={
HTTPStatus.OK.value: {
"model": Event,
"description": "Request successful.",
},
},
tags=["Events"],
summary="Lookup event with specified id",
dependencies=[Security(authentication_scheme)],
)
async def get_event(
response: Response,
db: Session = Depends(get_db),
event_id: int = Path(None, description=""),
) -> Event:
"""Lookup event with specified id. Can only be used for events that have not started yet."""
try:
db_event: EventDBModel = read_event_by_id(db, event_id)
event: EventDBModel = db_event.get_api_event()
except DBException:
logger.exception(f"Could not get event with id {event_id}.")
response.status_code = HTTPStatus.NOT_FOUND.value
return ProblemDetails(title=NOT_FOUND_DESCRIPTION)
return event
12 changes: 5 additions & 7 deletions backend/src/flotilla/api/robots_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,7 @@
summary="List all robots on the asset.",
dependencies=[Security(authentication_scheme)],
)
async def get_robots(
response: Response, db: Session = Depends(get_db)
) -> List[RobotDBModel]:
async def get_robots(db: Session = Depends(get_db)) -> List[Robot]:
db_robots: List[RobotDBModel] = read_robots(db)
robots: List[Robot] = [robot.get_api_robot() for robot in db_robots]
return robots
Expand All @@ -66,12 +64,12 @@ async def get_robot(
response: Response,
robot_id: int = Path(None, description=""),
db: Session = Depends(get_db),
) -> RobotDBModel:
) -> Robot:
try:
db_robot: RobotDBModel = read_robot_by_id(db=db, robot_id=robot_id)
robot: Robot = db_robot.get_api_robot()
except DBException:
logger.error(f"Could not get robot with id {robot_id}.")
logger.exception(f"Could not get robot with id {robot_id}.")
response.status_code = HTTPStatus.NOT_FOUND.value
return ProblemDetails(
title=NOT_FOUND_DESCRIPTION, status=HTTPStatus.NOT_FOUND.value
Expand Down Expand Up @@ -113,7 +111,7 @@ async def post_start_robot(
report_status=ReportStatus.in_progress,
)
except DBException:
logger.error(f"Could not get robot with id {robot_id}.")
logger.exception(f"Could not get robot with id {robot_id}.")
response.status_code = HTTPStatus.NOT_FOUND.value
return ProblemDetails(title=NOT_FOUND_DESCRIPTION)
except HTTPError as e:
Expand Down Expand Up @@ -154,7 +152,7 @@ async def post_stop_robot(
host=robot.host, port=robot.port
)
except DBException:
logger.error(f"Could not get robot with id {robot_id}.")
logger.exception(f"Could not get robot with id {robot_id}.")
response.status_code = HTTPStatus.NOT_FOUND.value
return ProblemDetails(title=NOT_FOUND_DESCRIPTION)
except HTTPError as e:
Expand Down
48 changes: 47 additions & 1 deletion backend/src/flotilla/database/crud.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import datetime
from typing import List, Optional

from sqlalchemy.orm import Session

from flotilla.database.models import ReportDBModel, ReportStatus, RobotDBModel
from flotilla.database.models import (
EventDBModel,
ReportDBModel,
ReportStatus,
RobotDBModel,
)


class DBException(Exception):
Expand Down Expand Up @@ -37,6 +43,20 @@ def read_report_by_id(db: Session, report_id: int) -> ReportDBModel:
return report


def read_events(db: Session) -> List[EventDBModel]:
events: List[EventDBModel] = db.query(EventDBModel).all()
return events


def read_event_by_id(db: Session, event_id: int) -> EventDBModel:
event: Optional[EventDBModel] = (
db.query(EventDBModel).filter(EventDBModel.id == event_id).first()
)
if not event:
raise DBException(f"No event with id {event_id}")
return event


def create_report(
db: Session,
robot_id: int,
Expand All @@ -54,3 +74,29 @@ def create_report(
db.add(report)
db.commit()
return report.id


def create_event(
db: Session,
robot_id: int,
echo_mission_id: int,
start_time: datetime.datetime,
) -> int:

event: EventDBModel = EventDBModel(
robot_id=robot_id,
echo_mission_id=echo_mission_id,
report_id=None,
start_time=start_time,
estimated_duration=datetime.timedelta(hours=1),
)
db.add(event)
db.commit()
return event.id


def remove_event(db: Session, event_id: int) -> None:
event: EventDBModel = read_event_by_id(db, event_id)
db.delete(event)
db.commit()
return
10 changes: 10 additions & 0 deletions backend/src/flotilla/database/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import datetime
import enum

from flotilla_openapi.models.event import Event
from flotilla_openapi.models.report import Report
from flotilla_openapi.models.report_entry import ReportEntry
from flotilla_openapi.models.robot import Robot
Expand Down Expand Up @@ -132,6 +133,15 @@ class EventDBModel(Base):
# TODO: robot_id and report_id.robot_id can now point at different robots.
# Should there be a constraint forcing an event to point at only one robot?

def get_api_event(self) -> Event:
return Event(
id=self.id,
robot_id=self.robot_id,
mission_id=self.echo_mission_id,
start_time=self.start_time,
end_time=datetime.datetime.now(),
)


class ReportEntryDBModel(Base):
__tablename__ = "entry"
Expand Down
2 changes: 2 additions & 0 deletions backend/src/flotilla/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from fastapi.middleware.cors import CORSMiddleware

from flotilla.api.authentication import authenticator
from flotilla.api.events_api import router as events_router
from flotilla.api.missions_api import router as missions_router
from flotilla.api.reports_api import router as reports_router
from flotilla.api.robots_api import router as robots_router
Expand Down Expand Up @@ -46,6 +47,7 @@ def startup_event():
app.include_router(robots_router)
app.include_router(missions_router)
app.include_router(reports_router)
app.include_router(events_router)


if __name__ == "__main__":
Expand Down
86 changes: 86 additions & 0 deletions backend/tests/test_api/test_events_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import json
from datetime import datetime
from http import HTTPStatus

import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from flotilla_openapi.models.event_request import EventRequest


def test_get_events(test_app: FastAPI):

with TestClient(test_app) as client:
response = client.get("/events")
assert response.status_code == HTTPStatus.OK.value


@pytest.mark.parametrize(
"event_id, expected_status_code",
[
(
2,
HTTPStatus.OK.value,
),
(
4,
HTTPStatus.NOT_FOUND.value,
),
],
)
def test_get_event(
test_app: FastAPI,
event_id: int,
expected_status_code: int,
):

with TestClient(test_app) as client:
response = client.get(f"/events/{event_id}")
assert response.status_code == expected_status_code


@pytest.mark.parametrize(
"event_request, expected_status_code",
[
(
EventRequest(robot_id=1, mission_id=234, start_time=datetime.now()),
HTTPStatus.CREATED.value,
),
],
)
def test_post_event(
test_app: FastAPI,
event_request: EventRequest,
expected_status_code: int,
):

with TestClient(test_app) as client:
response = client.post(
f"/events",
data=json.dumps(event_request.dict(), default=str),
)
assert response.status_code == expected_status_code


@pytest.mark.parametrize(
"event_id, expected_status_code",
[
(
1,
HTTPStatus.NO_CONTENT.value,
),
(
5,
HTTPStatus.NOT_FOUND.value,
),
],
)
def test_delete_event(
test_app: FastAPI,
event_id: int,
expected_status_code: int,
):

with TestClient(test_app) as client:
response = client.delete(f"/events/{event_id}")
assert response.status_code == expected_status_code