diff --git a/Makefile b/Makefile index faa26fd..ff2acc3 100644 --- a/Makefile +++ b/Makefile @@ -28,13 +28,15 @@ web: @docker compose up -d web mig: - @echo "Creating database and applying migrations..." + @echo "Creating database and applying migrations for medical..." @docker compose up migs + @docker compose up migssanctuary automig: @echo "Autogenerating migration in docker context" @docker compose up -d --no-recreate api - @docker compose exec api bash -c "cd /app/api && alembic revision --autogenerate -m 'CHANGEME'" + @docker compose exec api bash -c "cd /app/api && alembic --name sanctuary revision --autogenerate -m 'CHANGEME'" + @docker compose exec api bash -c "cd /app/api && alembic --name medical revision --autogenerate -m 'CHANGEME'" prune: @echo "Pruning docker artifacts..." diff --git a/app/api/alembic.ini b/app/api/alembic.ini index 05fa1bc..8950382 100644 --- a/app/api/alembic.ini +++ b/app/api/alembic.ini @@ -1,8 +1,6 @@ -# A generic, single database configuration. - -[alembic] +[sanctuary] # path to migration scripts -script_location = alembic/ +script_location = alembic/sanctuary # template used to generate migration files # file_template = %%(rev)s_%%(slug)s @@ -10,7 +8,7 @@ file_template = %%(year)d-%%(month).2d-%%(day).2d_%%(rev)s_%%(slug)s # sys.path path, will be prepended to sys.path if present. # defaults to the current working directory. -prepend_sys_path = .. +prepend_sys_path = .., .. # timezone to use when rendering the date # within the migration file as well as the filename. @@ -32,9 +30,9 @@ prepend_sys_path = .. # sourceless = false # version location specification; this defaults -# to alembic//versions. When using multiple version +# to sanctuary/versions. When using multiple version # directories, initial revisions must be specified with --version-path -# version_locations = %(here)s/bar %(here)s/bat alembic//versions +# version_locations = %(here)s/bar %(here)s/bat sanctuary/versions # the output encoding used when revision files # are written from script.py.mako @@ -42,6 +40,11 @@ prepend_sys_path = .. sqlalchemy.url = driver://user:pass@localhost/dbname +[medical] +script_location = alembic/medical +sqlalchemy.url = driver://user:pass@localhost/dbname +prepend_sys_path = .., .. +file_template = %%(year)d-%%(month).2d-%%(day).2d_%%(rev)s_%%(slug)s [post_write_hooks] # post_write_hooks defines scripts or Python functions that are run diff --git a/app/api/alembic/env.py b/app/api/alembic/medical/env.py similarity index 86% rename from app/api/alembic/env.py rename to app/api/alembic/medical/env.py index ca94238..7f8afe5 100644 --- a/app/api/alembic/env.py +++ b/app/api/alembic/medical/env.py @@ -22,18 +22,13 @@ target_metadata = Base.metadata -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - # Environment based sqlalchemy url user = os.environ.get("POSTGRES_USER") password = os.environ.get("POSTGRES_PASSWORD") hostname = os.environ.get("POSTGRES_HOST") db = os.environ.get("POSTGRES_DB") SQLALCHEMY_DATABASE_URL = f"postgresql+psycopg2://{user}:{password}@{hostname}/{db}" -config.set_main_option('sqlalchemy.url', SQLALCHEMY_DATABASE_URL) +config.set_main_option("sqlalchemy.url", SQLALCHEMY_DATABASE_URL) def run_migrations_offline(): @@ -74,8 +69,7 @@ def run_migrations_online(): ) with connectable.connect() as connection: - context.configure(connection=connection, - target_metadata=target_metadata) + context.configure(connection=connection, target_metadata=target_metadata) with context.begin_transaction(): context.run_migrations() diff --git a/app/api/alembic/script.py.mako b/app/api/alembic/medical/script.py.mako similarity index 100% rename from app/api/alembic/script.py.mako rename to app/api/alembic/medical/script.py.mako diff --git a/app/api/alembic/versions/2022-07-11_676c96a21e88_initial.py b/app/api/alembic/medical/versions/2022-07-11_676c96a21e88_initial.py similarity index 100% rename from app/api/alembic/versions/2022-07-11_676c96a21e88_initial.py rename to app/api/alembic/medical/versions/2022-07-11_676c96a21e88_initial.py diff --git a/app/api/alembic/versions/2022-07-18_d14ecfcd7ffa_changeme.py b/app/api/alembic/medical/versions/2022-07-18_d14ecfcd7ffa_changeme.py similarity index 100% rename from app/api/alembic/versions/2022-07-18_d14ecfcd7ffa_changeme.py rename to app/api/alembic/medical/versions/2022-07-18_d14ecfcd7ffa_changeme.py diff --git a/app/api/alembic/versions/2022-07-19_b28b9c211ee0_update_user_and_form_entry_properties.py b/app/api/alembic/medical/versions/2022-07-19_b28b9c211ee0_update_user_and_form_entry_properties.py similarity index 100% rename from app/api/alembic/versions/2022-07-19_b28b9c211ee0_update_user_and_form_entry_properties.py rename to app/api/alembic/medical/versions/2022-07-19_b28b9c211ee0_update_user_and_form_entry_properties.py diff --git a/app/api/alembic/versions/2022-07-20_a98d7be28c23_changeme.py b/app/api/alembic/medical/versions/2022-07-20_a98d7be28c23_changeme.py similarity index 100% rename from app/api/alembic/versions/2022-07-20_a98d7be28c23_changeme.py rename to app/api/alembic/medical/versions/2022-07-20_a98d7be28c23_changeme.py diff --git a/app/api/alembic/medical/versions/2024-01-30_f2fff363a197_changeme.py b/app/api/alembic/medical/versions/2024-01-30_f2fff363a197_changeme.py new file mode 100644 index 0000000..95aa42d --- /dev/null +++ b/app/api/alembic/medical/versions/2024-01-30_f2fff363a197_changeme.py @@ -0,0 +1,52 @@ +"""CHANGEME + +Revision ID: f2fff363a197 +Revises: a98d7be28c23 +Create Date: 2024-01-30 00:30:26.398900 + +""" + +# revision identifiers, used by Alembic. +revision = 'f2fff363a197' +down_revision = 'a98d7be28c23' + +from alembic import op +import sqlalchemy as sa + +from alembic import context + + +def upgrade(): + schema_upgrades() + if context.get_x_argument(as_dictionary=True).get("data", None): + data_upgrades() + + +def downgrade(): + if context.get_x_argument(as_dictionary=True).get("data", None): + data_downgrades() + schema_downgrades() + + +def schema_upgrades(): + """schema upgrade migrations go here.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def schema_downgrades(): + """schema downgrade migrations go here.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def data_upgrades(): + """Add any optional data upgrade migrations here!""" + pass + + +def data_downgrades(): + """Add any optional data downgrade migrations here!""" + pass \ No newline at end of file diff --git a/app/api/alembic/sanctuary/__init__.py b/app/api/alembic/sanctuary/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/alembic/sanctuary/env.py b/app/api/alembic/sanctuary/env.py new file mode 100644 index 0000000..ef1a89d --- /dev/null +++ b/app/api/alembic/sanctuary/env.py @@ -0,0 +1,82 @@ +import os +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +from api.main.database import BaseSanctuary + +# Populates Base.metadata by importing all models +import api.models + +target_metadata = BaseSanctuary.metadata + +# Environment based sqlalchemy url +user = os.environ.get("POSTGRES_USER") +password = os.environ.get("POSTGRES_PASSWORD") +hostname = os.environ.get("POSTGRES_HOST") +db = "sanctuary" +SQLALCHEMY_DATABASE_URL = f"postgresql+psycopg2://{user}:{password}@{hostname}/{db}" +config.set_main_option("sqlalchemy.url", SQLALCHEMY_DATABASE_URL) + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/app/api/alembic/sanctuary/script.py.mako b/app/api/alembic/sanctuary/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/app/api/alembic/sanctuary/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/app/api/alembic/sanctuary/versions/2024-01-30_e7a96dd328c5_changeme.py b/app/api/alembic/sanctuary/versions/2024-01-30_e7a96dd328c5_changeme.py new file mode 100644 index 0000000..968308f --- /dev/null +++ b/app/api/alembic/sanctuary/versions/2024-01-30_e7a96dd328c5_changeme.py @@ -0,0 +1,64 @@ +"""CHANGEME + +Revision ID: e7a96dd328c5 +Revises: +Create Date: 2024-01-30 00:30:20.784035 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'e7a96dd328c5' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('intakes', + sa.Column('created_timestamp', sa.DateTime(), nullable=True), + sa.Column('last_updated_timestamp', sa.DateTime(), nullable=True), + sa.Column('deleted', sa.Boolean(), nullable=True), + sa.Column('intake_uuid', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('user_uuid', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('guest_rfid', sa.String(), nullable=True), + sa.Column('arrival_date', sa.DateTime(), nullable=True), + sa.Column('arrival_time', sa.DateTime(), nullable=True), + sa.Column('arrival_method', sa.String(), nullable=True), + sa.Column('identified_gender', sa.String(), nullable=True), + sa.Column('first_visit', sa.Boolean(), nullable=True), + sa.Column('presenting_complaint', sa.String(), nullable=True), + sa.Column('guest_consciousness_level', sa.String(), nullable=True), + sa.Column('guest_emotional_state', sa.String(), nullable=True), + sa.Column('substance_categories', sa.String(), nullable=True), + sa.Column('time_since_last_dose', sa.Integer(), nullable=True), + sa.Column('discharge_date', sa.DateTime(), nullable=True), + sa.Column('discharge_time', sa.DateTime(), nullable=True), + sa.Column('discharge_method', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('intake_uuid'), + sa.UniqueConstraint('intake_uuid') + ) + op.create_table('users', + sa.Column('created_timestamp', sa.DateTime(), nullable=True), + sa.Column('last_updated_timestamp', sa.DateTime(), nullable=True), + sa.Column('deleted', sa.Boolean(), nullable=True), + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('user_uuid', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('email', sa.String(), nullable=True), + sa.Column('hashed_password', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user_uuid') + ) + op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_users_id'), table_name='users') + op.drop_table('users') + op.drop_table('intakes') + # ### end Alembic commands ### diff --git a/app/api/main/database.py b/app/api/main/database.py index 84efc0d..a29149e 100644 --- a/app/api/main/database.py +++ b/app/api/main/database.py @@ -20,13 +20,21 @@ f"postgresql+psycopg2://{user}:{password}@{hostname}:{port}/{db}" ) -engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True) -if not database_exists(engine.url): # Check if the db exists - create_database(engine.url) # Create new DB +# TODO: rename "example" database to medical +# Engine for the 'medical' database +engine_example = create_engine( + f"postgresql+psycopg2://{user}:{password}@{hostname}:{port}/example" +) +if not database_exists(engine_example.url): # Check if the db exists + create_database(engine_example.url) # Create new DB -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +# TODO: Rename sessionLocal to sessionLocalMedical +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine_example) +# TODO: rename this to MedicalBase Base = declarative_base() +BaseSanctuary = declarative_base() + def get_db() -> Generator: db = SessionLocal() @@ -35,9 +43,13 @@ def get_db() -> Generator: finally: db.close() + +# TODO: implement engine and session for sanctuary def create_all_tables() -> None: - Base.metadata.create_all(engine, checkfirst=True) + Base.metadata.create_all(engine_example, checkfirst=True) +# BaseSanctuary.metadata.create_all(engine_example, checkfirst=True) def drop_all_tables(check_first: bool = False) -> None: - Base.metadata.drop_all(engine, checkfirst=check_first) + Base.metadata.drop_all(engine_example, checkfirst=check_first) +# BaseSanctuary.metadata.drop_all(engine_example, checkfirst=check_first) diff --git a/app/api/models/__init__.py b/app/api/models/__init__.py index b048812..b946659 100644 --- a/app/api/models/__init__.py +++ b/app/api/models/__init__.py @@ -1,6 +1,3 @@ -from api.models.user import User +from api.models.user import User, UserSanctuary, UserMedical from api.models.patient_encounter import PatientEncounter - -# TODO: uncomment this for sanctuary dev db -# Ticket: https://mediform.atlassian.net/browse/MEDI-30 -# from api.models.intake import Intake +from api.models.intake import Intake diff --git a/app/api/models/intake.py b/app/api/models/intake.py index 3f68d61..2bc7119 100644 --- a/app/api/models/intake.py +++ b/app/api/models/intake.py @@ -8,12 +8,12 @@ from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Session -from api.main.database import Base +from api.main.database import BaseSanctuary from api.models.mixins import BasicMetrics logger = logging.getLogger(__name__) -class Intake(Base, BasicMetrics): +class Intake(BaseSanctuary, BasicMetrics): __tablename__ = "intakes" intake_uuid = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, unique=True) diff --git a/app/api/models/user.py b/app/api/models/user.py index 8f6c5c8..1761696 100644 --- a/app/api/models/user.py +++ b/app/api/models/user.py @@ -7,35 +7,41 @@ from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Session -from api.main.database import Base +from api.main.database import Base, BaseSanctuary from api.models.mixins import BasicMetrics - -class User(Base, BasicMetrics): - __tablename__ = "users" +class User(BasicMetrics): id = Column(Integer, primary_key=True, index=True, autoincrement=True) user_uuid = Column(UUID(as_uuid=True), default=uuid.uuid4, unique=True) email = Column(String) hashed_password = Column(String) +# for Sanctuary +class UserSanctuary(User, BaseSanctuary): + __tablename__ = "users" + +# For medical +class UserMedical(User, Base): + __tablename__ = "users" + def get_user_by_email(db: Session, email: str) -> Union[User, None]: - return db.query(User).filter(User.email == email).first() + return db.query(UserMedical).filter(UserMedical.email == email).first() -def create_user(db: Session, email: str, password: str) -> User: +def create_user(db: Session, email: str, password: str) -> UserMedical: """Create a new user with the provided valid credentials. Params: db: A database session. - user: User data that has already been validated. + user: UserMedical data that has already been validated. Returns: - A UserResponse schema with all fields included except the access token. + A UserMedicalResponse schema with all fields included except the access token. """ password_hash = argon2.hash(password) - created_user = User( + created_user = UserMedical( email=email, hashed_password=password_hash, ) diff --git a/app/api/routes/patient_encounter/delete_patient_encounter_uuid.py b/app/api/routes/patient_encounter/delete_patient_encounter_uuid.py index fd0310d..7b9a78e 100644 --- a/app/api/routes/patient_encounter/delete_patient_encounter_uuid.py +++ b/app/api/routes/patient_encounter/delete_patient_encounter_uuid.py @@ -10,7 +10,7 @@ from api.main.auth import load_current_user from api.main.database import get_db -from api.models.user import User +from api.models.user import UserMedical as User from api.models.patient_encounter import PatientEncounter from api.models.patient_encounter import ( get_patient_encounter_by_uuid, diff --git a/app/api/routes/patient_encounter/get_latest_patient_encounter_rfid.py b/app/api/routes/patient_encounter/get_latest_patient_encounter_rfid.py index 7469e4a..5d639b0 100644 --- a/app/api/routes/patient_encounter/get_latest_patient_encounter_rfid.py +++ b/app/api/routes/patient_encounter/get_latest_patient_encounter_rfid.py @@ -10,7 +10,7 @@ from api.main.auth import load_current_user from api.main.database import get_db -from api.models.user import User +from api.models.user import UserMedical as User from api.models.patient_encounter import get_latest_patient_encounter_by_patient_rfid from api.schemas.patient_encounter import PatientEncounterResponseSchema diff --git a/app/api/routes/patient_encounter/get_patient_encounter_uuid.py b/app/api/routes/patient_encounter/get_patient_encounter_uuid.py index edd84b3..329d1a9 100644 --- a/app/api/routes/patient_encounter/get_patient_encounter_uuid.py +++ b/app/api/routes/patient_encounter/get_patient_encounter_uuid.py @@ -10,7 +10,7 @@ from api.main.auth import load_current_user from api.main.database import get_db -from api.models.user import User +from api.models.user import UserMedical as User from api.models.patient_encounter import get_patient_encounter_by_uuid from api.schemas.patient_encounter import PatientEncounterResponseSchema diff --git a/app/api/routes/patient_encounter/get_patient_encounters.py b/app/api/routes/patient_encounter/get_patient_encounters.py index 064ed7b..3ae5131 100644 --- a/app/api/routes/patient_encounter/get_patient_encounters.py +++ b/app/api/routes/patient_encounter/get_patient_encounters.py @@ -9,7 +9,7 @@ from api.main.auth import load_current_user from api.main.database import get_db -from api.models.user import User +from api.models.user import UserMedical as User from api.models.patient_encounter import get_all_patient_encounters from api.schemas.patient_encounter import PatientEncounterResponseSchema diff --git a/app/api/routes/patient_encounter/submit_patient_encounter.py b/app/api/routes/patient_encounter/submit_patient_encounter.py index 5f8b34d..0183216 100644 --- a/app/api/routes/patient_encounter/submit_patient_encounter.py +++ b/app/api/routes/patient_encounter/submit_patient_encounter.py @@ -9,7 +9,7 @@ from api.main.auth import load_current_user from api.main.database import get_db -from api.models.user import User +from api.models.user import UserMedical as User from api.models.patient_encounter import create_patient_encounter, PatientEncounter from api.schemas.patient_encounter import ( PatientEncounterSchema, diff --git a/app/api/routes/patient_encounter/update_patient_encounter.py b/app/api/routes/patient_encounter/update_patient_encounter.py index 082da53..7f94c5d 100644 --- a/app/api/routes/patient_encounter/update_patient_encounter.py +++ b/app/api/routes/patient_encounter/update_patient_encounter.py @@ -10,7 +10,7 @@ from api.main.auth import load_current_user from api.main.database import get_db -from api.models.user import User +from api.models.user import UserMedical as User from api.models.patient_encounter import ( get_patient_encounter_by_uuid, update_patient_encounter, diff --git a/app/api/routes/refresh_token.py b/app/api/routes/refresh_token.py index a3708f8..31334e2 100644 --- a/app/api/routes/refresh_token.py +++ b/app/api/routes/refresh_token.py @@ -6,7 +6,7 @@ from api.schemas.users import UserLogin, UserLoginResponse from api.main.auth import load_current_user, generate_auth_token from api.main.database import get_db -from api.models.user import User +from api.models.user import UserMedical as User router = APIRouter() diff --git a/app/api/seeder/seed_database.py b/app/api/seeder/seed_database.py index 6408ecd..0ced956 100644 --- a/app/api/seeder/seed_database.py +++ b/app/api/seeder/seed_database.py @@ -8,7 +8,7 @@ from fastapi.testclient import TestClient from api.main.app import api -from api.models import User, PatientEncounter +from api.models import UserMedical as User, PatientEncounter from api.main.database import get_db from api.seeder.seeds.users import USERS diff --git a/docker-compose.yaml b/docker-compose.yaml index 0cb6b8d..36051c0 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -25,6 +25,7 @@ services: retries: 5 depends_on: - migs + - migssanctuary - testapi - seeder @@ -36,7 +37,23 @@ services: env_file: app/api/.env environment: - POSTGRES_HOST=db - command: bash -c "cd /app/api && alembic upgrade head" + command: bash -c "cd /app/api && alembic --name=medical upgrade head" + volumes: + # Mount local codebase to reflect changes for local dev + - ./app/api:/app/api + depends_on: + db: + condition: service_healthy + + migssanctuary: + container_name: migssanctuary + build: + context: app/api/ + dockerfile: Dockerfile.dev + env_file: app/api/.env + environment: + - POSTGRES_HOST=db + command: bash -c "cd /app/api && alembic --name=sanctuary upgrade head" volumes: # Mount local codebase to reflect changes for local dev - ./app/api:/app/api @@ -59,6 +76,9 @@ services: interval: 5s timeout: 5s retries: 5 + volumes: + - ./scripts/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh + testapi: container_name: testapi diff --git a/scripts/init-db.sh b/scripts/init-db.sh new file mode 100755 index 0000000..ff565dc --- /dev/null +++ b/scripts/init-db.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -e + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL + CREATE DATABASE sanctuary; +EOSQL