feat(backend): records models, schemas and tables

This commit is contained in:
Julian Lobbes 2023-05-13 05:06:23 +02:00
parent 03d5363932
commit a6f04a6c63
5 changed files with 337 additions and 1 deletions

View File

@ -14,7 +14,7 @@ from sqlalchemy.sql.functions import now
revision = '7e5a8cabd3a4'
down_revision = '335e07a98bc8'
branch_labels = None
depends_on = '335e07a98bc8'
depends_on = down_revision
def upgrade() -> None:

View File

@ -0,0 +1,79 @@
"""create records tables
Revision ID: a7522972878d
Revises: 7e5a8cabd3a4
Create Date: 2023-05-13 04:46:57.958926
"""
from alembic import op
from sqlalchemy import Column, ForeignKey, Integer, SmallInteger, DateTime, Enum, Numeric
from sqlalchemy.sql.functions import now
from backend.models.records import AvpuScore, RespirationScore
# revision identifiers, used by Alembic.
revision = 'a7522972878d'
down_revision = '7e5a8cabd3a4'
branch_labels = None
depends_on = down_revision
def upgrade() -> None:
op.create_table(
'heart_rate_records',
Column('id', Integer, primary_key=True, autoincrement=True, index=True),
Column('measured', DateTime(timezone=True), nullable=False, index=True),
Column('value', SmallInteger, nullable=False),
Column('device_id', Integer, ForeignKey('devices.id', ondelete="CASCADE"), nullable=False, index=True),
)
op.create_table(
'avpu_score_records',
Column('id', Integer, primary_key=True, autoincrement=True, index=True),
Column('measured', DateTime(timezone=True), nullable=False, index=True),
Column('value', Enum(AvpuScore), nullable=False),
Column('device_id', Integer, ForeignKey('devices.id', ondelete="CASCADE"), nullable=False, index=True),
)
op.create_table(
'body_temperature_records',
Column('id', Integer, primary_key=True, autoincrement=True, index=True),
Column('measured', DateTime(timezone=True), nullable=False, index=True),
Column('value', Numeric(precision=4, scale=2, decimal_return_scale=2), nullable=False),
Column('device_id', Integer, ForeignKey('devices.id', ondelete="CASCADE"), nullable=False, index=True),
)
op.create_table(
'blood_pressure_records',
Column('id', Integer, primary_key=True, autoincrement=True, index=True),
Column('measured', DateTime(timezone=True), nullable=False, index=True),
Column('value_systolic', SmallInteger, nullable=False),
Column('value_diastolic', SmallInteger, nullable=False),
Column('device_id', Integer, ForeignKey('devices.id', ondelete="CASCADE"), nullable=False, index=True),
)
op.create_table(
'blood_oxygen_records',
Column('id', Integer, primary_key=True, autoincrement=True, index=True),
Column('measured', DateTime(timezone=True), nullable=False, index=True),
Column('value', Numeric(precision=5, scale=2, decimal_return_scale=2), nullable=False),
Column('device_id', Integer, ForeignKey('devices.id', ondelete="CASCADE"), nullable=False, index=True),
)
op.create_table(
'respiration_score_records',
Column('id', Integer, primary_key=True, autoincrement=True, index=True),
Column('measured', DateTime(timezone=True), nullable=False, index=True),
Column('value', Enum(RespirationScore), nullable=False),
Column('device_id', Integer, ForeignKey('devices.id', ondelete="CASCADE"), nullable=False, index=True),
)
def downgrade() -> None:
op.drop_table('heart_rate_records')
op.drop_table('avpu_score_records')
op.drop_table('body_temperature_records')
op.drop_table('blood_pressure_records')
op.drop_table('blood_oxygen_records')
op.drop_table('respiration_score_records')

View File

@ -34,3 +34,10 @@ class Device(Base):
model = relationship(DeviceModel, back_populates="instances", uselist=False)
owner = relationship("Patient", back_populates="devices", uselist=False)
heart_rate_records = relationship("HeartRateRecord", back_populates="device", uselist=True, cascade="all, delete")
avpu_score_records = relationship("AvpuScoreRecord", back_populates="device", uselist=True, cascade="all, delete")
blood_pressure_records = relationship("BloodPressureRecord", back_populates="device", uselist=True, cascade="all, delete")
blood_oxygen_records = relationship("BloodOxygenRecord", back_populates="device", uselist=True, cascade="all, delete")
body_temperature_records = relationship("BodyTemperatureRecord", back_populates="device", uselist=True, cascade="all, delete")
respiration_score_records = relationship("RespirationScoreRecord", back_populates="device", uselist=True, cascade="all, delete")

110
backend/models/records.py Normal file
View File

@ -0,0 +1,110 @@
"""This module defines the SQL data model for vital parameter records."""
import enum
from sqlalchemy import Column, ForeignKey, DateTime, SmallInteger, Integer, Enum, Numeric
from sqlalchemy.orm import relationship
from backend.database.engine import Base
from backend.models.devices import Device
class HeartRateRecord(Base):
"""Model for the heart rate records table. Measured in beats per minute (bpm)."""
__tablename__ = "heart_rate_records"
id = Column('id', Integer, primary_key=True, autoincrement=True, index=True)
measured = Column('measured', DateTime(timezone=True), nullable=False, index=True)
value = Column('value', SmallInteger, nullable=False)
device_id = Column('device_id', Integer, ForeignKey('devices.id', ondelete="CASCADE"), nullable=False, index=True)
device = relationship(Device, back_populates="heart_rate_records", uselist=False)
class AvpuScore(enum.Enum):
alert = 'a'
voice = 'v'
pain = 'p'
unresponsive = 'u'
class AvpuScoreRecord(Base):
"""Model for the avpu score records table. Measured as a discrete classification."""
__tablename__ = "avpu_score_records"
id = Column('id', Integer, primary_key=True, autoincrement=True, index=True)
measured = Column('measured', DateTime(timezone=True), nullable=False, index=True)
value = Column('value', Enum(AvpuScore), nullable=False)
device_id = Column('device_id', Integer, ForeignKey('devices.id', ondelete="CASCADE"), nullable=False, index=True)
device = relationship(Device, back_populates="avpu_score_records", uselist=False)
class BodyTemperatureRecord(Base):
"""Model for the body temperature records table. Measured in degrees Celsius.
Two digits before and two digits after the decimal point: [-99.99, 99.99].
"""
__tablename__ = "body_temperature_records"
id = Column('id', Integer, primary_key=True, autoincrement=True, index=True)
measured = Column('measured', DateTime(timezone=True), nullable=False, index=True)
value = Column('value', Numeric(precision=4, scale=2, decimal_return_scale=2), nullable=False)
device_id = Column('device_id', Integer, ForeignKey('devices.id', ondelete="CASCADE"), nullable=False, index=True)
device = relationship(Device, back_populates="body_temperature_records", uselist=False)
class BloodPressureRecord(Base):
"""Model for the blood pressure records table. Measured in (mmHg)."""
__tablename__ = "blood_pressure_records"
id = Column('id', Integer, primary_key=True, autoincrement=True, index=True)
measured = Column('measured', DateTime(timezone=True), nullable=False, index=True)
value_systolic = Column('value_systolic', SmallInteger, nullable=False)
value_diastolic = Column('value_diastolic', SmallInteger, nullable=False)
device_id = Column('device_id', Integer, ForeignKey('devices.id', ondelete="CASCADE"), nullable=False, index=True)
device = relationship(Device, back_populates="blood_pressure_records", uselist=False)
class BloodOxygenRecord(Base):
"""Model for the blood oxygen records table. Measured as a decimal percentage.
Three digits before and two digits after the decimal point: [-999.99, 999.99].
"""
__tablename__ = "blood_oxygen_records"
id = Column('id', Integer, primary_key=True, autoincrement=True, index=True)
measured = Column('measured', DateTime(timezone=True), nullable=False, index=True)
value = Column('value', Numeric(precision=5, scale=2, decimal_return_scale=2), nullable=False)
device_id = Column('device_id', Integer, ForeignKey('devices.id', ondelete="CASCADE"), nullable=False, index=True)
device = relationship(Device, back_populates="blood_oxygen_records", uselist=False)
class RespirationScore(enum.Enum):
"""Measures whether patient is experiencing shortness of breath, and its severity."""
none = 0
low = 1
medium = 2
high = 3
class RespirationScoreRecord(Base):
"""Model for the respiration score records table. Measured as a discrete classification."""
__tablename__ = "respiration_score_records"
id = Column('id', Integer, primary_key=True, autoincrement=True, index=True)
measured = Column('measured', DateTime(timezone=True), nullable=False, index=True)
value = Column('value', Enum(RespirationScore), nullable=False)
device_id = Column('device_id', Integer, ForeignKey('devices.id', ondelete="CASCADE"), nullable=False, index=True)
device = relationship(Device, back_populates="respiration_score_records", uselist=False)

140
backend/schemas/records.py Normal file
View File

@ -0,0 +1,140 @@
"""This module declareds the pydantic schema representation for devices."""
from datetime import datetime
from abc import ABC
from decimal import Decimal
from pydantic import BaseModel, validator
from backend.models.records import AvpuScore, RespirationScore
class AbstractRecordCreate(BaseModel, ABC):
"""Base class containing fields common to all vitals records during creation."""
measured: datetime
device_id: int
@validator('measured')
def assert_measured_is_valid(cls, measured: datetime) -> datetime:
if measured >= datetime.now():
raise ValueError("Time of measurement cannot be in the future.")
return measured
class AbstractRecord(BaseModel, ABC):
"""Base class containing fields common to all vitals records for display."""
id: int
measured: datetime
device_id: int
@validator('measured')
def assert_measured_is_valid(cls, measured: datetime) -> datetime:
if measured >= datetime.now():
raise ValueError("Time of measurement cannot be in the future.")
return measured
class AbstractHeartRateRecord(BaseModel, ABC):
value: int
@validator('value')
def assert_value_is_valid(cls, value):
if value < 0:
raise ValueError("Value cannot be negative.")
if value >= 32767:
raise ValueError("Value is too large.")
return value
class HeartRateRecordCreate(AbstractRecordCreate, AbstractHeartRateRecord):
pass
class HeartRateRecord(AbstractRecord, AbstractHeartRateRecord):
pass
class AbstractAvpuScoreRecord(BaseModel, ABC):
value: AvpuScore
class AvpuScoreRecordCreate(AbstractRecordCreate, AbstractAvpuScoreRecord):
pass
class AvpuScoreRecord(AbstractRecord, AbstractAvpuScoreRecord):
pass
class AbstractBodyTemperatureRecord(BaseModel, ABC):
value: Decimal
@validator('value')
def assert_value_is_valid(cls, value: Decimal) -> Decimal:
if value < 0:
raise ValueError("Value cannot be negative.")
if value >= 100:
raise ValueError("Value cannot exceed '99.99'.")
if len(value.as_tuple().digits) > 4:
raise ValueError("Value can have at most two digits after the decimal point.")
return value
class BodyTemperatureRecordCreate(AbstractRecordCreate, AbstractBodyTemperatureRecord):
pass
class BodyTemperatureRecord(AbstractRecord, AbstractBodyTemperatureRecord):
pass
class AbstractBloodPressureRecord(BaseModel, ABC):
value_systolic: int
value_diastolic: int
@validator('value_systolic')
def assert_value_systolic_is_valid(cls, value_systolic):
if value_systolic < 0:
raise ValueError("Value (systolic) cannot be negative.")
if value_systolic >= 32767:
raise ValueError("Value (systolic) is too large.")
return value_systolic
@validator('value_diastolic')
def assert_value_diastolic_is_valid(cls, value_diastolic):
if value_diastolic < 0:
raise ValueError("Value (diastolic) cannot be negative.")
if value_diastolic >= 32767:
raise ValueError("Value (diastolic) is too large.")
return value_diastolic
class BloodPressureRecordCreate(AbstractRecordCreate, AbstractBloodPressureRecord):
pass
class BloodPressureRecord(AbstractRecord, AbstractBloodPressureRecord):
pass
class AbstractBloodOxygenRecord(BaseModel, ABC):
value: Decimal
@validator('value')
def assert_value_is_valid(cls, value: Decimal) -> Decimal:
if value < 0:
raise ValueError("Value cannot be negative.")
if value > 100:
raise ValueError("Value cannot exceed '100.00'.")
if len(value.as_tuple().digits) > 5:
raise ValueError("Value can have at most two digits after the decimal point.")
return value
class BloodOxygenRecordCreate(AbstractRecordCreate, AbstractBloodOxygenRecord):
pass
class BloodOxygenRecord(AbstractRecord, AbstractBloodOxygenRecord):
pass
class AbstractRespirationScoreScoreRecord(BaseModel, ABC):
value: RespirationScore
class RespirationScoreScoreRecordCreate(AbstractRecordCreate, AbstractRespirationScoreScoreRecord):
pass
class RespirationScoreScoreRecord(AbstractRecord, AbstractRespirationScoreScoreRecord):
pass