From 831e528839bd759e36657a8ae763f5728bdb02a8 Mon Sep 17 00:00:00 2001 From: Anette Uttisrud Date: Fri, 14 Jan 2022 11:15:28 +0100 Subject: [PATCH] Add database infrastructure and mock db --- .gitignore | 2 + backend/setup.py | 8 +- backend/src/flotilla/api/api.py | 0 backend/src/flotilla/database/__init__.py | 0 backend/src/flotilla/database/db.py | 12 ++ .../database/mock_database/mock_database.py | 124 ++++++++++++++++++ backend/src/flotilla/database/models.py | 113 ++++++++++++++++ backend/src/flotilla/main.py | 1 + backend/tests/conftest.py | 12 ++ backend/tests/test_db.py | 5 + 10 files changed, 271 insertions(+), 6 deletions(-) create mode 100644 backend/src/flotilla/api/api.py create mode 100644 backend/src/flotilla/database/__init__.py create mode 100644 backend/src/flotilla/database/db.py create mode 100644 backend/src/flotilla/database/mock_database/mock_database.py create mode 100644 backend/src/flotilla/database/models.py create mode 100644 backend/tests/conftest.py create mode 100644 backend/tests/test_db.py diff --git a/.gitignore b/.gitignore index 906cab1f0..e9ed2c9a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. frontend/node_modules/ +venv/ +.vscode diff --git a/backend/setup.py b/backend/setup.py index 989e25d7b..92ef39acd 100644 --- a/backend/setup.py +++ b/backend/setup.py @@ -2,7 +2,7 @@ setup( name="flotilla", - version='0.0.0', + version="0.0.0", description="Backend for the Flotilla application", long_description=open("README.md").read(), long_description_content_type="text/markdown", @@ -19,11 +19,7 @@ "Topic :: Scientific/Engineering :: Physics", ], include_package_data=True, - install_requires=[ - "fastapi", - "uvicorn", - "requests", - ], + install_requires=["fastapi", "uvicorn", "requests", "SQLAlchemy", "pytz"], extras_require={ "dev": [ "pytest", diff --git a/backend/src/flotilla/api/api.py b/backend/src/flotilla/api/api.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/flotilla/database/__init__.py b/backend/src/flotilla/database/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/flotilla/database/db.py b/backend/src/flotilla/database/db.py new file mode 100644 index 000000000..96826de8a --- /dev/null +++ b/backend/src/flotilla/database/db.py @@ -0,0 +1,12 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() diff --git a/backend/src/flotilla/database/mock_database/mock_database.py b/backend/src/flotilla/database/mock_database/mock_database.py new file mode 100644 index 000000000..339012860 --- /dev/null +++ b/backend/src/flotilla/database/mock_database/mock_database.py @@ -0,0 +1,124 @@ +import datetime + +from flotilla.database.models import ( + Capability, + Entry, + EntryStatus, + Event, + InspectionType, + Report, + ReportStatus, + Robot, + RobotStatus, +) + + +def populate_mock_db(session, engine, base) -> None: + base.metadata.create_all(engine) + + robot_1 = Robot( + name="Harald", + model="King", + serial_number="VI", + logs="", + status=RobotStatus.available, + ) + + robot_2 = Robot( + name="Harold", + model="King", + serial_number="VII", + logs="", + status=RobotStatus.offline, + ) + + session.add_all([robot_1, robot_2]) + session.commit() + + report_1 = Report( + robot_id=robot_1.id, + isar_mission_id="isar_mission_id", + echo_mission_id=1, + log="", + status=ReportStatus.in_progress, + ) + report_2 = Report( + robot_id=robot_2.id, + isar_mission_id="isar_mission_id", + echo_mission_id=1, + log="", + status=ReportStatus.completed, + ) + + session.add(report_1, report_2) + session.commit() + + entry_1 = Entry( + report_id=report_1.id, + tag_id="tag_id", + status=EntryStatus.completed, + inspection_type=InspectionType.image, + time=datetime.datetime.now(tz=datetime.timezone.utc), + file_location="", + ) + + entry_2 = Entry( + report_id=report_2.id, + tag_id="tag_id", + status=EntryStatus.failed, + inspection_type=InspectionType.thermal_image, + time=datetime.datetime.now(tz=datetime.timezone.utc), + file_location="", + ) + + entry_3 = Entry( + report_id=report_2.id, + tag_id="tag_id", + status=EntryStatus.completed, + inspection_type=InspectionType.image, + time=datetime.datetime.now(tz=datetime.timezone.utc), + file_location="", + ) + + event_1 = Event( + robot_id=robot_1.id, + echo_mission_id=287, + report_id=report_1.id, + estimated_duration=datetime.timedelta(hours=1), + ) + + event_2 = Event( + robot_id=robot_2.id, + echo_mission_id=287, + report_id=report_2.id, + estimated_duration=datetime.timedelta(hours=2), + ) + + capability_1 = Capability( + robot_id=robot_2.id, + capability=InspectionType.image, + ) + + capability_2 = Capability( + robot_id=robot_2.id, + capability=InspectionType.thermal_image, + ) + + capability_3 = Capability( + robot_id=robot_1.id, + capability=InspectionType.image, + ) + + session.add_all( + [ + entry_1, + entry_2, + entry_3, + event_1, + event_2, + capability_1, + capability_2, + capability_3, + ] + ) + session.commit() diff --git a/backend/src/flotilla/database/models.py b/backend/src/flotilla/database/models.py new file mode 100644 index 000000000..5301f93f6 --- /dev/null +++ b/backend/src/flotilla/database/models.py @@ -0,0 +1,113 @@ +import datetime +import enum + +from sqlalchemy import Column, DateTime, Enum, ForeignKey, Integer, Interval, String +from sqlalchemy.orm import backref, relationship + +from flotilla.database.db import Base + + +class RobotStatus(enum.Enum): + available = "Available" + busy = "Busy" + offline = "Offline" + + +class InspectionType(enum.Enum): + image = "Image" + thermal_image = "ThermalImage" + + +class ReportStatus(enum.Enum): + scheduled = "Scheduled" + in_progress = "InProgress" + failed = "Failed" + completed = "Completed" + + +class EntryStatus(enum.Enum): + failed = "Failed" + completed = "Completed" + + +class Robot(Base): + __tablename__ = "robot" + id = Column(Integer, primary_key=True) + name = Column(String) + model = Column(String) + serial_number = Column(String) + logs = Column(String) + status = Column(Enum(RobotStatus)) + telemetry_topics = relationship("Topic", backref=backref("robot")) + streams = relationship("VideoStream", backref=backref("robot")) + capabilities = relationship("Capability", backref=backref("robot")) + events = relationship("Event", backref=backref("robot")) + + +class Report(Base): + __tablename__ = "report" + id = Column(Integer, primary_key=True) + robot_id = Column(Integer, ForeignKey("robot.id")) + isar_mission_id = Column(String) + echo_mission_id = Column(Integer) + log = Column(String) + status = Column(Enum(ReportStatus)) + entries = relationship("Entry", backref=backref("report")) + + +class Map(Base): + __tablename__ = "map" + id = Column(Integer, primary_key=True) + location = Column(String) + coordinate_reference_system = Column(String) + # grid = Column(String) + + +class Topic(Base): + __tablename__ = "topic" + id = Column(Integer, primary_key=True) + robot_id = Column(Integer, ForeignKey("robot.id")) + name = Column(String) + + +class VideoStream(Base): + __tablename__ = "video_stream" + id = Column(Integer, primary_key=True) + robot_id = Column(Integer, ForeignKey("robot.id")) + rtsp_url = Column(String) + port = Column(Integer) + camera_name = Column(String) + + +class Capability(Base): + __tablename__ = "capability" + id = Column(Integer, primary_key=True) + robot_id = Column(Integer, ForeignKey("robot.id")) + capability = Column(Enum(InspectionType)) + + +class Event(Base): + __tablename__ = "event" + id = Column(Integer, primary_key=True) + robot_id = Column(Integer, ForeignKey("robot.id")) + echo_mission_id = Column(Integer) + report_id = Column(Integer, ForeignKey("report.id")) + start_time = Column( + DateTime(timezone=True), default=datetime.datetime.now(tz=datetime.timezone.utc) + ) + estimated_duration = Column(Interval) + # foreign_key_constraint = ForeignKeyConstraint() + # 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? + + +class Entry(Base): + __tablename__ = "entry" + id = Column(Integer, primary_key=True) + report_id = Column(Integer, ForeignKey("report.id")) + tag_id = Column(String) + status = Column(Enum(EntryStatus)) + inspection_type = Column(Enum(InspectionType)) + time = Column( + DateTime(timezone=True), default=datetime.datetime.now(tz=datetime.timezone.utc) + ) + file_location = Column(String) diff --git a/backend/src/flotilla/main.py b/backend/src/flotilla/main.py index 2a268787c..ee60be1f9 100644 --- a/backend/src/flotilla/main.py +++ b/backend/src/flotilla/main.py @@ -2,6 +2,7 @@ app = FastAPI() + @app.get("/") async def root(): return {"message": "Hello World"} diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py new file mode 100644 index 000000000..d7f8cb5fe --- /dev/null +++ b/backend/tests/conftest.py @@ -0,0 +1,12 @@ +import pytest +from sqlalchemy.orm import Session + +from flotilla.database.db import Base, SessionLocal, engine +from flotilla.database.mock_database.mock_database import populate_mock_db + +mock_session: Session = SessionLocal() +populate_mock_db(session=mock_session,engine=engine,base=Base) + +@pytest.fixture() +def session() -> Session: + return mock_session diff --git a/backend/tests/test_db.py b/backend/tests/test_db.py new file mode 100644 index 000000000..2d0f2337f --- /dev/null +++ b/backend/tests/test_db.py @@ -0,0 +1,5 @@ +from flotilla.database.models import Robot + + +def test_mock_db(session): + assert len(session.query(Robot.name).all()) == 2