Skip to content

Commit

Permalink
created seeder container for populating db with mock data (#61)
Browse files Browse the repository at this point in the history
## Problem
The development database does not have mock data to aid development.

## Solution
* Created `app/api/seeder/Dockerfile.seed` for development database seeder.
* Added `seeder` service in`docker-compose.yaml` 
    - Service `api` now also depends on `seeder` service to ensure seeder is run
* Seeder container uses SQLAlchemy ORM models and `get_db` generator from API
* Added `seed` target in the project Makefile
* Some users created for seeding db.

## Ticket URL
https://mediform.atlassian.net/browse/MEDI-3

## Documentation
N/A

## Tests Run
Executed `make all` and `make seeder` to test seeder. Could sign in using mock users.
  • Loading branch information
critch646 authored Jan 10, 2024
1 parent 9f93cac commit 5bff779
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 15 deletions.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ testapi:
@echo "Running api unit tests..."
@docker compose up testapi --exit-code-from testapi

seed:
@echo "Seeding database..."
@docker compose up seeder --exit-code-from seeder

db:
@echo "Starting db..."
@docker compose up -d db
Expand Down
21 changes: 21 additions & 0 deletions app/api/seeder/Dockerfile.seed
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Dockerfile.seed for seeding development database
FROM python:3.9.1-slim

RUN mkdir -p /app/api
WORKDIR /app/api

COPY seeder/seeder_reqs.txt /app/api
RUN pip install --no-cache-dir -r seeder_reqs.txt
COPY main /app/api/
COPY models /app/api/
COPY seeder /app/api/
WORKDIR /app

# Setup user to represent developer permissions in container
ARG USERNAME=python
ARG USER_UID=1000
ARG USER_GID=1000
RUN useradd -rm -d /home/$USERNAME -s /bin/bash -g root -G sudo -u $USER_UID $USERNAME
USER $USERNAME

EXPOSE 5000
Empty file added app/api/seeder/__init__.py
Empty file.
110 changes: 110 additions & 0 deletions app/api/seeder/seed_database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from json import JSONDecodeError
from passlib.hash import argon2
from typing import List, Dict

from sqlalchemy.orm import Session
from fastapi.testclient import TestClient

from api.main.app import api
from api.models import User
from api.main.database import get_db

from api.seeder.seeds.users import USERS

def create_authed_client(email: str, password: str, client: TestClient):
""" Create an authenticated client for testing
Args:
email (str): User email
password (str): Plain text password
client (FastAPI.TestClient): Test client to be authenticated
Returns:
FastAPI.TestClient: Authenticated test client
"""

# Get token
print(f"Creating authed client for {email}")
login_data = {"email": email, "password": password}
r = client.post("/api/login", json=login_data)

# Check if login was successful
if r.status_code != 200:
print("create_authed_client response: ", r.json())
raise Exception("Login failed")

print("create_authed_client response: ", r.json())
token = r.json()["access_token"]

# Add authorization token header
client.headers = {"Authorization": f"Bearer {token}"}


return client


def create_users(users: List[Dict], db: Session) -> List[User]:
""" Create users in database
Args:
users (List[Dict]): List of users to create
db (Session): Database session
Returns:
List[User]: List of created users
"""

created_users = []

# Create users
for user in users:

# Check if user already exists
user_obj = db.query(User).filter(User.email == user["email"]).first()
if user_obj:
print(f"User {user_obj.email} already exists")
created_users.append(user_obj)
continue

# Hash password
hashed_password = argon2.hash(user["password"])
# Create user
user_obj = User(
email=user["email"],
hashed_password=hashed_password,
)
try:
## Add user to database
db.add(user_obj)
db.commit()
db.refresh(user_obj)
print(
f"User {user_obj.email} created with uuid: {user_obj.user_uuid}"
)
created_users.append(user_obj)
except Exception as e:
print(e)
db.rollback()
db.flush()
print(f"User {user_obj.email} already exists")

return created_users

def seed_database():
""" Seed database with mock data.
"""

db = next(get_db())

# Create test client and check if healthy
client = TestClient(api)
r = client.get("/api/")
if r.json()["status"] == "healthy":
print("API is healthy")

# Create users
created_users = create_users(USERS, db)


if __name__ == "__main__":
seed_database()
16 changes: 16 additions & 0 deletions app/api/seeder/seeder_reqs.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
argon2-cffi==21.3.0
argon2-cffi-bindings==21.2.0
cffi==1.15.0
email-validator==1.2.1
fastapi==0.65.1
passlib==1.7.4
psycopg2-binary==2.8.6
py==1.11.0
pyasn1==0.4.8
python-dateutil==2.8.2
python-dotenv==0.20.0
python-editor==1.0.4
python-jose==3.3.0
requests==2.25.1
SQLAlchemy==1.4.15
typing_extensions==4.2.0
Empty file.
21 changes: 21 additions & 0 deletions app/api/seeder/seeds/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""
Mock data for user model
"""


DEFAULT_USER = {
"email": "default_user@default.com",
"password": "Password123!"
}

TEST_USER_1 = {
"email": "not_bruce_wayne@gothamcity.com",
"password": "B@tCaveSecret1"
}

TEST_USER_2 = {
"email": "lethal@company.com",
"password": "WeMetQu0ta!"
}

USERS = [DEFAULT_USER, TEST_USER_1, TEST_USER_2]
44 changes: 29 additions & 15 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version: '3.9'
version: "3.9"

# LOCAL DEV

Expand All @@ -19,13 +19,14 @@ services:
# Mount local codebase to reflect changes for local dev
- ./app/api:/app/api
healthcheck:
test: [ "CMD", "curl", "localhost:5000/api/health" ]
test: ["CMD", "curl", "localhost:5000/api/health"]
interval: 5s
timeout: 5s
retries: 5
depends_on:
- migs
- testapi
- seeder

migs:
container_name: migs
Expand Down Expand Up @@ -54,7 +55,7 @@ services:
ports:
- 5432:5432
healthcheck:
test: [ "CMD", "pg_isready" ]
test: ["CMD", "pg_isready"]
interval: 5s
timeout: 5s
retries: 5
Expand All @@ -75,6 +76,22 @@ services:
depends_on:
- migs

seeder:
container_name: seeder
user: 1000:1000
build:
context: app/api/
dockerfile: seeder/Dockerfile.seed
env_file: app/api/.env
tty: true
environment:
- POSTGRES_HOST=db
volumes:
- ./app/api/:/app/api
command: python -m api.seeder.seed_database
depends_on:
- testapi

web:
container_name: web
user: node
Expand Down Expand Up @@ -114,8 +131,7 @@ services:
- ./app/api:/app/api
command: bash -c "cd /app/api && alembic upgrade head"


#PRODUCTION IMAGE VERIFICATION
#PRODUCTION IMAGE VERIFICATION

apitarget:
container_name: apitarget
Expand All @@ -130,17 +146,17 @@ services:
- 5000:5000
command: uvicorn api.main.app:api --reload --host=0.0.0.0 --port=5000
healthcheck:
test: [ "CMD", "curl", "localhost:5000/api/health" ]
test: ["CMD", "curl", "localhost:5000/api/health"]
interval: 5s
timeout: 5s
retries: 5
depends_on:
- migstarget
- testapitarget

migstarget:
migstarget:
container_name: migstarget
build:
build:
context: app/api
dockerfile: Dockerfile.prod
env_file: app/api/.env
Expand All @@ -150,7 +166,7 @@ services:
depends_on:
db:
condition: service_healthy

testapitarget:
container_name: testapitarget
user: 1000:1000
Expand All @@ -162,22 +178,20 @@ services:
- POSTGRES_HOST=db
#run as module so basedir (root) is added to python path
command: python -m pytest api/tests/
depends_on:
depends_on:
- migstarget

webtarget:
container_name: webtarget
user: node
build:
build:
context: app/web/
dockerfile: Dockerfile.prod
ports:
- 3000:3000
depends_on:
depends_on:
- apitarget



networks:
default:
driver: "bridge"

0 comments on commit 5bff779

Please sign in to comment.