feat(backend): devices CRUD and routes refactor
This commit is contained in:
parent
5a08e54890
commit
03d5363932
@ -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
68
backend/crud/devices.py
Normal 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
|
@ -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)
|
||||
|
@ -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
13
backend/exceptions.py
Normal 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
|
@ -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)
|
||||
|
0
backend/routes/__init__.py
Normal file
0
backend/routes/__init__.py
Normal file
52
backend/routes/devices.py
Normal file
52
backend/routes/devices.py
Normal 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
58
backend/routes/users.py
Normal 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))
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user