feat(backend): devices CRUD and routes refactor

This commit is contained in:
Julian Lobbes 2023-05-13 02:54:04 +02:00
parent 5a08e54890
commit 03d5363932
10 changed files with 263 additions and 66 deletions

View File

@ -19,7 +19,11 @@ class Settings(BaseSettings):
"""
app_name: str = os.getenv("APP_NAME", "MEDWingS")
admin_email: str = os.getenv("ADMIN_EMAIL", "admin@example.com")
app_version: str = os.getenv("APP_VERSION", "Unspecified")
contact_name: str = os.getenv("CONTACT_NAME", "MEDWingS Development Team")
contact_email: str = os.getenv("CONTACT_EMAIL", "admin@example.com")
contact_url: str = os.getenv("CONTACT_URL", "https://www.example.com")
# Debug mode has the following effects:
# - logs SQL operations

68
backend/crud/devices.py Normal file
View File

@ -0,0 +1,68 @@
"""This module handles CRUD operations for users in the database, based on pydanctic schemas."""
from datetime import datetime
from sqlalchemy.orm import Session
from backend.models import devices as devicemodel
from backend.models import users as usermodel
from backend.schemas import devices as deviceschema
from backend.exceptions import DataIntegrityException, NotFoundException
def create_device(db: Session, device: deviceschema.DeviceCreate) -> deviceschema.Device:
"""Creates the specified device in the database."""
db_user = db.query(usermodel.User).filter(usermodel.User.id == device.owner_id).first()
if not db_user:
raise NotFoundException("Attempted to create a device for a nonexistent user.")
if not db_user.patient:
raise DataIntegrityException("Attempted to create a device for a user who is not a patient.")
db_device = devicemodel.Device(
model_id=device.model_id,
owner_id=device.owner_id,
)
db.add(db_device)
db.commit()
db.refresh(db_device)
return deviceschema.Device.from_orm(db_device)
def read_device(db: Session, id: int) -> deviceschema.Device | None:
"""Queries the db for a device with the specified id and returns it."""
db_device = db.query(devicemodel.Device).filter(devicemodel.Device.id == id).first()
if not db_device:
raise NotFoundException(f"Device with id '{id}' was not found.")
return deviceschema.Device.from_orm(db_device)
def update_device(db: Session, device: deviceschema.DeviceUpdate, id: int) -> deviceschema.Device:
"""Updates the specified device's last seen time."""
db_device = db.query(devicemodel.Device).filter(devicemodel.Device.id == id).first()
if not db_device:
raise NotFoundException(f"Device with id '{id}' was not found.")
db_device.last_seen = device.last_seen
db.commit()
db.refresh(db_device)
return deviceschema.Device.from_orm(db_device)
def delete_device(db: Session, id: int) -> deviceschema.Device:
"""Deletes the user with the provided id from the db."""
db_device = db.query(devicemodel.Device).filter(devicemodel.Device.id == id).first()
if not db_device:
raise NotFoundException(f"Device with id '{id}' was not found.")
device_copy = deviceschema.Device.from_orm(db_device)
db.delete(db_device)
db.commit()
return device_copy

View File

@ -6,6 +6,7 @@ from sqlalchemy.orm import Session
from backend.models import users as usermodel
from backend.schemas import users as userschema
from backend.exceptions import NotFoundException
def hash_password(password: str) -> str:
@ -33,6 +34,7 @@ def _fill_missing_user_fields(db_user: usermodel.User) -> userschema.User:
full_user = userschema.User.from_orm(db_user)
if db_user.patient:
full_user.devices = db_user.patient.devices
full_user.gender = db_user.patient.gender
full_user.date_of_birth = db_user.patient.date_of_birth
full_user.is_patient = True
@ -54,7 +56,6 @@ def create_user(db: Session, user: userschema.UserCreate) -> userschema.User:
password=hash_password(user.password),
)
# Add user to database
if user.is_patient:
db_patient = usermodel.Patient(
user=db_user,
@ -70,27 +71,26 @@ def create_user(db: Session, user: userschema.UserCreate) -> userschema.User:
db.commit()
# Construct the updated user to return
db.refresh(db_user)
return _fill_missing_user_fields(db_user)
def read_user(db: Session, id: int) -> userschema.User | None:
"""Queries the db for a user with the specified id and returns them if they exist."""
"""Queries the db for a user with the specified id and returns them."""
db_user = db.query(usermodel.User).filter(usermodel.User.id == id).first()
if not db_user:
return None
raise NotFoundException(f"User with id '{id}' not found.")
return _fill_missing_user_fields(db_user)
def read_user_by_email(db: Session, email: str) -> userschema.User | None:
"""Queries the db for a user with the specified email and returns them if they exist."""
"""Queries the db for a user with the specified email and returns them."""
db_user = db.query(usermodel.User).filter(usermodel.User.email == email).first()
if not db_user:
return None
raise NotFoundException(f"User with email '{email}' not found.")
return _fill_missing_user_fields(db_user)
@ -111,7 +111,7 @@ def update_user(db: Session, user: userschema.UserUpdate, id: int) -> userschema
db_user = db.query(usermodel.User).filter(usermodel.User.id == id).first()
if not db_user:
raise RuntimeError("Query returned no user.") # should be checked by caller
raise NotFoundException(f"User with id '{id}' not found.")
for key in ['gender', 'date_of_birth']:
value = getattr(user, key)
@ -134,7 +134,7 @@ def delete_user(db: Session, id: int) -> userschema.User:
db_user = db.query(usermodel.User).filter(usermodel.User.id == id).first()
if not db_user:
raise RuntimeError("Query returned no user.") # should be checked by caller
raise NotFoundException(f"User with id '{id}' not found.")
user_copy = _fill_missing_user_fields(db_user)
db.delete(db_user)

View File

@ -17,3 +17,11 @@ engine = create_engine(_pg_dsn, echo=s.debug_mode)
SessionLocal = sessionmaker(engine)
# SQLalchemy base model
Base = declarative_base()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

13
backend/exceptions.py Normal file
View File

@ -0,0 +1,13 @@
"""This module is a collection of project-wide exceptions."""
class NotFoundException(Exception):
"""Raised when a resource was unexpectedly not found."""
pass
class DataIntegrityException(Exception):
"""Raised when a resource was unexpectedly not found."""
pass

View File

@ -3,24 +3,33 @@
This module defines the API routes provided by the backend.
"""
from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy.orm import Session
from fastapi import FastAPI
import backend.models.users as usermodel
import backend.schemas.users as userschema
import backend.crud.users as usercrud
from backend.database.engine import SessionLocal
from backend import config
from backend.routes.devices import router as devices_router
from backend.routes.devices import tag_metadata as devices_tags
from backend.routes.users import router as users_router
from backend.routes.users import tag_metadata as users_tags
app = FastAPI()
s = config.get_settings()
app = FastAPI(
title=f"{s.app_name} backend API",
description=f"This is the backend server API for {s.app_name}, a remote patient monitoring and early warning system.",
version=f"{s.app_version}",
contact={
"name": f"{s.contact_name}",
"email": f"{s.contact_email}",
"url": f"{s.contact_url}",
},
openapi_tags=[
users_tags,
devices_tags,
],
)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
app.include_router(devices_router)
app.include_router(users_router)
@app.get("/hello/")
@ -28,41 +37,3 @@ def hello():
"""Placeholder for a proper healthcheck endpoint."""
return "Hello World!"
@app.post("/users/", response_model=userschema.User)
def create_user(user: userschema.UserCreate, db: Session = Depends(get_db)):
existing_user = usercrud.read_user_by_email(db, email=user.email)
if existing_user:
raise HTTPException(status_code=400, detail="A user with this email address is already registered.")
return usercrud.create_user(db=db, user=user)
@app.get("/users/{id}", response_model=userschema.User)
def read_user(id: int, db: Session = Depends(get_db)):
user = usercrud.read_user(db=db, id=id)
if not user:
raise HTTPException(status_code=404, detail=f"No user with id '{id}' found.")
return user
@app.get("/users/", response_model=list[userschema.User])
def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
users = usercrud.read_users(db=db, skip=skip, limit=limit)
return users
@app.patch("/users/{id}", response_model=userschema.User)
def update_user(id: int, user: userschema.UserUpdate, db: Session = Depends(get_db)):
current_user = usercrud.read_user(db=db, id=id)
if not current_user:
raise HTTPException(status_code=404, detail=f"No user with id '{id}' found.")
return usercrud.update_user(db=db, user=user, id=id)
@app.delete("/users/{id}", response_model=userschema.User)
def delete_user(id: int, db: Session = Depends(get_db)):
user = usercrud.read_user(db=db, id=id)
if not user:
raise HTTPException(status_code=404, detail=f"No user with id '{id}' found.")
return usercrud.delete_user(db=db, id=id)

View File

52
backend/routes/devices.py Normal file
View File

@ -0,0 +1,52 @@
"""This module contains endpoints for operations related to devices."""
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from backend.database.engine import get_db
from backend.schemas import devices as deviceschema
import backend.crud.devices as devicecrud
from backend.exceptions import NotFoundException
router = APIRouter(
prefix="/devices",
tags=["devices"]
)
tag_metadata = {
"name": "devices",
"description": "Operations related to devices."
}
@router.post("/", response_model=deviceschema.Device)
def create_device(device: deviceschema.DeviceCreate, db: Session = Depends(get_db)):
try:
return devicecrud.create_device(db, device)
except NotFoundException as e:
raise HTTPException(404, str(e))
@router.get("/{id}", response_model=deviceschema.Device)
def read_device(id: int, db: Session = Depends(get_db)):
try:
return devicecrud.read_device(db, id)
except NotFoundException as e:
raise HTTPException(404, str(e))
@router.patch("/{id}", response_model=deviceschema.Device)
def update_device(id: int, device: deviceschema.DeviceUpdate, db: Session = Depends(get_db)):
try:
return devicecrud.update_device(db, device, id)
except NotFoundException as e:
raise HTTPException(404, str(e))
@router.delete("/{id}", response_model=deviceschema.Device)
def delete_device(id: int, db: Session = Depends(get_db)):
try:
return devicecrud.delete_device(db, id)
except NotFoundException as e:
raise HTTPException(404, str(e))

58
backend/routes/users.py Normal file
View File

@ -0,0 +1,58 @@
"""This module contains endpoints for operations related to users."""
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from backend.database.engine import get_db
import backend.schemas.users as userschema
import backend.crud.users as usercrud
from backend.exceptions import NotFoundException
router = APIRouter(
prefix="/users",
tags=["users"]
)
tag_metadata = {
"name": "users",
"description": "Operations related to users."
}
@router.post("/users/", response_model=userschema.User)
def create_user(user: userschema.UserCreate, db: Session = Depends(get_db)):
existing_user = usercrud.read_user_by_email(db, email=user.email)
if existing_user:
raise HTTPException(400, "A user with this email address is already registered.")
return usercrud.create_user(db=db, user=user)
@router.get("/users/{id}", response_model=userschema.User)
def read_user(id: int, db: Session = Depends(get_db)):
try:
return usercrud.read_user(db=db, id=id)
except NotFoundException as e:
raise HTTPException(404, str(e))
@router.get("/users/", response_model=list[userschema.User])
def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
users = usercrud.read_users(db=db, skip=skip, limit=limit)
return users
@router.patch("/users/{id}", response_model=userschema.User)
def update_user(id: int, user: userschema.UserUpdate, db: Session = Depends(get_db)):
try:
return usercrud.update_user(db=db, user=user, id=id)
except NotFoundException as e:
raise HTTPException(404, str(e))
@router.delete("/users/{id}", response_model=userschema.User)
def delete_user(id: int, db: Session = Depends(get_db)):
try:
return usercrud.delete_user(db=db, id=id)
except NotFoundException as e:
raise HTTPException(404, str(e))

View File

@ -1,6 +1,7 @@
"""This module declareds the pydantic schema representation for devices."""
from datetime import datetime
from abc import ABC
from pydantic import BaseModel, validator
@ -26,11 +27,32 @@ class DeviceModel(BaseModel):
orm_mode = True
class Device(BaseModel):
id: int
added: datetime
class DeviceCreate(BaseModel):
"""Device schema used for Device creation."""
owner_id: int
model_id: int
@validator('model_id')
def assert_model_id_is_valid(cls, model_id):
if not 1 <= model_id <= 3:
raise ValueError("Model id is invalid.")
return model_id
class DeviceUpdate(BaseModel):
"""Device schema used for Device updates."""
last_seen: datetime
class Device(BaseModel):
"""Device schema used for Device display."""
id: int
model: DeviceModel
added: datetime
last_seen: datetime | None
@validator('added')
def assert_added_is_valid(cls, added):
@ -40,8 +62,9 @@ class Device(BaseModel):
@validator('last_seen')
def assert_last_seen_is_valid(cls, last_seen):
if last_seen >= datetime.now(last_seen.tzinfo):
raise ValueError("Date last seen cannot be in the future.")
if last_seen:
if last_seen >= datetime.now(last_seen.tzinfo):
raise ValueError("Date last seen cannot be in the future.")
return last_seen
class Config: