Merge branch 'dev' into feature/search_component_t51

This commit is contained in:
abpwrs committed 2020-04-28 21:30:22 -05:00
commit 22ed740dce
20 files changed
+658 -3

No files matched your search

+5 -1
View File
@@ -1,4 +1,8 @@
**/.lighthouseci/**
env/*
.vscode/*
# postgres persisted data
pgdata/
.gitsecret/keys/random_seed
!*.secret
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
Whitespace-only changes.
+1
View File
@@ -0,0 +1 @@
**/__pycache__/**
+29
View File
@@ -0,0 +1,29 @@
# Database Management
How to load a backup into your postgres docker container
First, decrypt the pg_dump files.
```
git secret reveal
```
Second, restore the pg_dump into the container.
```bash
gunzip -c ./backups/version.sql.gz | docker exec -i hdhs_database_1 psql -U postgres -d app_db
```
## Running Notes
generate backups:
```bash
docker exec hdhs_database_1 pg_dump -U postgres app_db | gzip > backups/20200408.tar.gz
```
Enter database container
```bash
docker-compose -f dev-docker-compose.yml run database bash
```
Enter DB inside container
```bash
psql --host=database --username=postgres --dbname=app_db
```
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
# Author: "abpwrs"
# Date: 20200409
# script:
isort -rc .
black .
+71
View File
@@ -0,0 +1,71 @@
from abc import ABC, abstractmethod
import numpy as np
import pandas as pd
from utils import edit_distance
"""
DESC:
Abstract Base Class (ABC) for a Database Object
Extracts shared logic from the FMP and PSQL classes
"""
class DB(ABC):
def __init__(self):
self._model = None
self.datetime_format = "%Y%m%d_%H%M"
self._table_names = []
# ABSTRACT METHDOS TO BE IMPLEMENTED BY CHILD CLASSES
# ###################################################
@abstractmethod
def read_table(self, table_name) -> pd.DataFrame:
pass
@abstractmethod
def write_table(self, data: pd.DataFrame, table_name):
pass
# ###################################################
@property
def table_names(self) -> list:
return self._table_names
def get_tablename_from_fieldname(self, field_name: str):
try:
assert field_name[:3] == "pk_" or field_name[:3] == "fk_"
except AssertionError as e:
print(
"Can only get meaningful table names from field names on primary or foreign keys"
)
raise (e)
# remove "pk_" or "fk_" --> we only enter this function if this has already matched
extracted_field_to_match = field_name[3:]
# not all fields that start with "pk_" or "fk_" end in "_id"
if field_name[-3:] == "_id":
extracted_field_to_match = extracted_field_to_match[:-3]
else:
print(f"consider fixing fieldname: {field_name}")
# function that returns the edit distance of the table to the extracted field name
matcher = lambda table_name: edit_distance(table_name, extracted_field_to_match)
try:
# map lambda over list of table `names & extract min edit distance
index_of_match = int(
np.argmin(np.array(list(map(matcher, self.table_names))), axis=0)
)
except TypeError as e:
print("FAILURE: two tables have equally close edit_distance")
# TODO: potentially add a backup lookup table here
# for now we will re-raise the error and crash the program
raise (e)
return self.table_names[index_of_match]
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env python
# Author: "hertz"
# Date: 20200310
# ####################
# DESC:
# A script to execute a SQL script inside of postgres docker container
# ####################
# args:
# 1 -- name of container running postgres to execute the script inside of
# 2 -- path to .SQL script to be execute inside of the postgres container
import argparse
import pickle
import sys
from sqlalchemy import create_engine
from psql import PSQL
engine = create_engine("postgres+psycopg2://postgres:HaitiCHI@0.0.0.0:5432/app_db")
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("command", help="SQL command to run in container")
return parser.parse_args()
def main(argv):
# TODO: extract this from database.env (ideally but it in the PSQL class)
psql = PSQL("postgres+psycopg2://postgres:HaitiCHI@0.0.0.0:5432/app_db")
result = psql.execute_command(argv.command)
print(result)
if __name__ == "__main__":
sys.exit(main(parse_args()))
+168
View File
@@ -0,0 +1,168 @@
import json
from datetime import datetime
import numpy as np
import pandas as pd
import pyodbc
from tqdm import tqdm
from db import DB
from utils import edit_distance
"""
File Maker Pro Connection Object
Desc:
This file contains the primary logic for interacting with the file maker pro database
"""
class FMP(DB):
def __init__(self, dsn=None, username=None, password=None, connect=True):
DB.__init__(self)
if dsn is None:
dsn = input("Enter the FMP DSN: ")
if username is None:
username = input("Enter your FMP username: ")
if password is None:
password = input("Enter your FMP password: ")
self.__dsn = dsn
self.__username = username
self.__password = password
self.__connection_string = "DSN={0};UID={1};PWD={2}".format(
self.__dsn, self.__username, self.__password
)
# TODO: consider extracting this list to the DB class
self._table_names = [
"globals",
"clinics",
"patients",
"visits",
"diagnoses",
"diagnoses_made",
"meds",
"meds_dispensed",
"geonames",
"settings",
]
self._join_tables = [
"diagnoses_made",
"meds_dispensed"
]
self.__python_types_to_sql = {
"str": "VARCHAR",
"date": "TIMESTAMP",
"float64": "FLOAT",
"NoneType": "VARCHAR",
}
self.__pyodbc_connection = None
self.__cursor = None
if connect:
self.__connect()
# METHODS FROM DB SUPER CLASS
# ###########################
def read_table(self, table_name) -> pd.DataFrame:
query = "select * from {table}".format(table=table_name)
df = pd.read_sql(query, self.connection)
return df
def write_table(self, data, table_name):
# TODO: implement write_table in FMP class
print("IMPLEMENT WRITE_TABLE IN FMP")
# ###########################
def __connect(self):
if self.__pyodbc_connection is None:
self.__pyodbc_connection = pyodbc.connect(self.__connection_string)
self.__pyodbc_connection
def execute_command(self, command_string):
result = None
try:
result = pd.read_sql(command_string, self.connection)
except Exception as e:
print(f"Failure to execute: ['{command_string}']")
raise (e) # TODO: determine if crashing is appropriate
return result
@property
def python_types_to_sql(self):
return self.__python_types_to_sql
@property
def connection(self):
if self.__pyodbc_connection is None:
self.__connect()
return self.__pyodbc_connection
@property
def cursor(self):
if self.__cursor is None:
self.__cursor = self.connection.cursor()
return self.__cursor
# ABSTRACT BASE CLASS MEHTODS
def parse_database(self) -> dict():
self._model = dict()
for table_name in tqdm(self.table_names):
df = self.read_table(table_name)
self._model[table_name] = {}
col_types = [
df.iloc[0, index].__class__.__name__ for index in range(len(df.columns))
]
smallest_pk_edit_distance = float("inf")
previous_pk_field = None
for field_name, field_type in zip(df.columns, col_types):
self._model[table_name][field_name] = {}
self._model[table_name][field_name]["type"] = field_type
# extract primary keys
if field_name[:3] == "pk_":
# possible primary key
distance = edit_distance(field_name[3:-3], table_name)
if distance < smallest_pk_edit_distance:
# closest primary key so far
if previous_pk_field is not None:
# remove previously assumed pk
self._model[table_name][previous_pk_field]["pk"] = False
# set current pk
self._model[table_name][field_name]["pk"] = True
previous_pk_field = field_name
smallest_pk_edit_distance = distance
else:
self._model[table_name][field_name]["pk"] = False
else:
self._model[table_name][field_name]["pk"] = False
# extract foreign keys
if field_name[:3] == "fk_":
self._model[table_name][field_name]["fk"] = True
self._model[table_name][field_name][
"fk_table"
] = self.get_tablename_from_fieldname(field_name)
else:
self._model[table_name][field_name]["fk"] = False
return self._model
def write_model(self, model_object: dict = None):
curr_time = datetime.now().strftime(self.datetime_format)
with open("./models/{time}_model.json".format(time=curr_time), "w") as f:
if model_object is not None:
json.dump(model_object, f, indent=4)
else:
json.dump(self._model, f, indent=4)
+148
View File
@@ -0,0 +1,148 @@
import argparse
import os
import pickle
import sys
import pandas as pd
import numpy as np
from tqdm import tqdm
from fmp import FMP
from psql import PSQL
from utils import edit_distance
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("-d", "--dsn", help="dsn name")
parser.add_argument("-u", "--username", help="username for dsn")
parser.add_argument("-p", "--password", help="password for dsn")
return parser.parse_args()
def get_pk_and_fk_from_table(fmp: FMP, df: pd.DataFrame, table_name: str):
pk = None
fks = []
smallest_pk_edit_distance = float("inf")
for field_name in df.columns:
# extract primary keys
if field_name[:3] == "pk_" and field_name[-3:] == "_id":
# possible primary key
distance = edit_distance(field_name[3:-3], table_name)
if distance < smallest_pk_edit_distance:
pk = field_name
smallest_pk_edit_distance = distance
# extract foreign keys
if field_name[:3] == "fk_" and field_name[-3:] == "_id":
fks.append((field_name, fmp.get_tablename_from_fieldname(field_name)))
return pk, fks
def save_keys(filename: str, all_data: dict):
np.save(filename, all_data)
def load_dict(filename: str):
np_load_old = np.load
np.load = lambda *a, **k,: np_load_old(*a, allow_pickle=True, **k)
np_data = np.load(filename)[()]
# Restore np.load
np.load = np_load_old
return np_data
def main(argv):
fmp = FMP(argv.dsn, argv.username, argv.password)
# TODO: extract username, password, and database name from the database.env file
# TODO: encrypt the database.env file using git-secret
psql = PSQL("postgres+psycopg2://postgres:HaitiCHI@0.0.0.0:5432/app_db")
all_data = dict()
for table_name in tqdm(fmp.table_names):
print(table_name)
df = fmp.read_table(table_name)
# drop all exact duplicates
df.drop_duplicates(inplace=True)
if table_name == "meds_dispensed":
new_df = df[df["pk_meds_dispensed"].notna()]
new_df = new_df[new_df["fk_visit_id"].notna()]
new_df = new_df[new_df["fk_med_key"].notna()]
new_df.drop_duplicates(subset=["pk_meds_dispensed","fk_visit_id", "fk_med_key"], inplace=True)
print(f"Table ({table_name}) lost ({len(df) - len(new_df)}) entries")
df = new_df
elif table_name == "diagnoses_made":
new_df = df[df["pk_diagnoses_made"].notna()]
new_df = new_df[new_df["fk_visit_id"].notna()]
new_df = new_df[new_df["fk_diagnosis_key"].notna()]
new_df.drop_duplicates(subset=["pk_diagnoses_made", "fk_visit_id", "fk_diagnosis_key"], inplace=True)
print(f"Table ({table_name}) lost ({len(df) - len(new_df)}) entries")
df = new_df
elif table_name == "settings":
df['pk_settings_id'] = df.index
elif table_name == "patients":
df.drop(["face_photo"], axis=1, inplace=True)
query = "SELECT GetAs(face_photo, 'JPEG') FROM patients"
result = fmp.cursor.execute(query)
data = result.fetchall()
data = [item[0] for item in data]
df["face_photo"] = data
psql.write_table(df, table_name, if_exists="replace")
pk_field, fk_fields = get_pk_and_fk_from_table(fmp, df, table_name)
all_data[table_name] = (pk_field, fk_fields)
filename = "table_data.npy"
save_keys(filename, all_data)
filename = "table_data.npy"
all_data = load_dict(filename)
for table_name in all_data.keys():
if table_name not in fmp._join_tables:
pk_field, _ = all_data[table_name]
if pk_field is not None:
psql.execute_command(f"ALTER TABLE {table_name} ALTER COLUMN {pk_field} TYPE numeric USING {pk_field}::numeric;")
psql.execute_command(f"ALTER TABLE {table_name} ADD PRIMARY KEY ({pk_field});")
else:
if table_name == "meds_dispensed":
for pk_field in ["pk_meds_dispensed", "fk_visit_id"]:
psql.execute_command(f"ALTER TABLE {table_name} ALTER COLUMN {pk_field} TYPE numeric USING {pk_field}::numeric;")
psql.execute_command(f"ALTER TABLE {table_name} ADD PRIMARY KEY (pk_meds_dispensed, fk_visit_id, fk_med_key);")
elif table_name == "diagnoses_made":
for pk_field in ["pk_diagnoses_made", "fk_visit_id"]:
psql.execute_command(f"ALTER TABLE {table_name} ALTER COLUMN {pk_field} TYPE numeric USING {pk_field}::numeric;")
psql.execute_command(f"ALTER TABLE {table_name} ADD PRIMARY KEY (pk_diagnoses_made, fk_visit_id, fk_diagnosis_key);")
for table_name in all_data.keys():
_, fk_fields = all_data[table_name]
for fk_field, fk_table in fk_fields:
pk_fk_table = all_data[fk_table][0]
# ASSUMES WE ARE ALWAYS LINKING TO THE PK OF THE TABLE
constraint_name = f"fk_{fk_table}_id"
psql.execute_command(f"ALTER TABLE {table_name} ALTER COLUMN {fk_field} TYPE numeric USING {fk_field}::numeric;")
'''
psql.execute_command(
f"ALTER TABLE {table_name} DROP CONSTRAINT IF EXISTS {constraint_name};"
)
'''
try:
psql.execute_command(
f"ALTER TABLE {table_name} ADD CONSTRAINT {constraint_name} FOREIGN KEY ({fk_field}) REFERENCES {fk_table}({pk_fk_table});"
)
except Exception as e:
print(f"FAILURE TO IMPLEMENT CONSTRAINT ON ({table_name}.{fk_field}) to ({fk_table}.{pk_fk_table})")
if __name__ == "__main__":
sys.exit(main(parse_args()))
+58
View File
@@ -0,0 +1,58 @@
0%| | 0/10 [00:00<?, ?it/s]
globals
10%|██████████▌ | 1/10 [00:00<00:03, 2.90it/s]
clinics
patients
30%|███████████████████████████████▏ | 3/10 [29:52<31:22, 269.00s/it]
visits
40%|█████████████████████████████████████████▌ | 4/10 [33:20<25:04, 250.80s/it]
diagnoses
50%|████████████████████████████████████████████████████ | 5/10 [33:20<14:38, 175.70s/it]
diagnoses_made
Table (diagnoses_made) lost (0) entries
60%|██████████████████████████████████████████████████████████████▍ | 6/10 [35:53<11:14, 168.71s/it]
meds
70%|████████████████████████████████████████████████████████████████████████▊ | 7/10 [35:53<05:54, 118.17s/it]
meds_dispensed
Table (meds_dispensed) lost (1) entries
80%|███████████████████████████████████████████████████████████████████████████████████▏ | 8/10 [40:33<05:33, 166.78s/it]
geonames
90%|█████████████████████████████████████████████████████████████████████████████████████████████▌ | 9/10 [41:14<02:08, 128.92s/it]
settings
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████ | 10/10 [41:14<00:00, 247.44s/it]
PSQL EXECUTING: [ALTER TABLE clinics ALTER COLUMN pk_clinic_id TYPE numeric USING pk_clinic_id::numeric;]
PSQL EXECUTING: [ALTER TABLE clinics ADD PRIMARY KEY (pk_clinic_id);]
PSQL EXECUTING: [ALTER TABLE patients ALTER COLUMN pk_patient_id TYPE numeric USING pk_patient_id::numeric;]
PSQL EXECUTING: [ALTER TABLE patients ADD PRIMARY KEY (pk_patient_id);]
PSQL EXECUTING: [ALTER TABLE visits ALTER COLUMN pk_visit_id TYPE numeric USING pk_visit_id::numeric;]
PSQL EXECUTING: [ALTER TABLE visits ADD PRIMARY KEY (pk_visit_id);]
PSQL EXECUTING: [ALTER TABLE diagnoses ALTER COLUMN pk_diagnosis_id TYPE numeric USING pk_diagnosis_id::numeric;]
PSQL EXECUTING: [ALTER TABLE diagnoses ADD PRIMARY KEY (pk_diagnosis_id);]
PSQL EXECUTING: [ALTER TABLE diagnoses_made ALTER COLUMN pk_diagnoses_made TYPE numeric USING pk_diagnoses_made::numeric;]
PSQL EXECUTING: [ALTER TABLE diagnoses_made ALTER COLUMN fk_visit_id TYPE numeric USING fk_visit_id::numeric;]
PSQL EXECUTING: [ALTER TABLE diagnoses_made ADD PRIMARY KEY (pk_diagnoses_made, fk_visit_id, fk_diagnosis_key);]
PSQL EXECUTING: [ALTER TABLE meds ALTER COLUMN pk_med_id TYPE numeric USING pk_med_id::numeric;]
PSQL EXECUTING: [ALTER TABLE meds ADD PRIMARY KEY (pk_med_id);]
PSQL EXECUTING: [ALTER TABLE meds_dispensed ALTER COLUMN pk_meds_dispensed TYPE numeric USING pk_meds_dispensed::numeric;]
PSQL EXECUTING: [ALTER TABLE meds_dispensed ALTER COLUMN fk_visit_id TYPE numeric USING fk_visit_id::numeric;]
PSQL EXECUTING: [ALTER TABLE meds_dispensed ADD PRIMARY KEY (pk_meds_dispensed, fk_visit_id, fk_med_key);]
PSQL EXECUTING: [ALTER TABLE geonames ALTER COLUMN pk_geoname_id TYPE numeric USING pk_geoname_id::numeric;]
PSQL EXECUTING: [ALTER TABLE geonames ADD PRIMARY KEY (pk_geoname_id);]
PSQL EXECUTING: [ALTER TABLE settings ALTER COLUMN pk_settings_id TYPE numeric USING pk_settings_id::numeric;]
PSQL EXECUTING: [ALTER TABLE settings ADD PRIMARY KEY (pk_settings_id);]
PSQL EXECUTING: [ALTER TABLE patients ALTER COLUMN fk_geoname_id TYPE numeric USING fk_geoname_id::numeric;]
PSQL EXECUTING: [ALTER TABLE patients ADD CONSTRAINT fk_geonames_id FOREIGN KEY (fk_geoname_id) REFERENCES geonames(pk_geoname_id);]
PSQL EXECUTING: [ALTER TABLE visits ALTER COLUMN fk_patient_id TYPE numeric USING fk_patient_id::numeric;]
PSQL EXECUTING: [ALTER TABLE visits ADD CONSTRAINT fk_patients_id FOREIGN KEY (fk_patient_id) REFERENCES patients(pk_patient_id);]
Failure to execute: ['ALTER TABLE visits ADD CONSTRAINT fk_patients_id FOREIGN KEY (fk_patient_id) REFERENCES patients(pk_patient_id);']
FAILURE TO IMPLEMENT CONSTRAINT ON (visits.fk_patient_id) to (patients.pk_patient_id)
PSQL EXECUTING: [ALTER TABLE visits ALTER COLUMN fk_clinic_id TYPE numeric USING fk_clinic_id::numeric;]
PSQL EXECUTING: [ALTER TABLE visits ADD CONSTRAINT fk_clinics_id FOREIGN KEY (fk_clinic_id) REFERENCES clinics(pk_clinic_id);]
PSQL EXECUTING: [ALTER TABLE diagnoses_made ALTER COLUMN fk_visit_id TYPE numeric USING fk_visit_id::numeric;]
PSQL EXECUTING: [ALTER TABLE diagnoses_made ADD CONSTRAINT fk_visits_id FOREIGN KEY (fk_visit_id) REFERENCES visits(pk_visit_id);]
Failure to execute: ['ALTER TABLE diagnoses_made ADD CONSTRAINT fk_visits_id FOREIGN KEY (fk_visit_id) REFERENCES visits(pk_visit_id);']
FAILURE TO IMPLEMENT CONSTRAINT ON (diagnoses_made.fk_visit_id) to (visits.pk_visit_id)
PSQL EXECUTING: [ALTER TABLE meds_dispensed ALTER COLUMN fk_visit_id TYPE numeric USING fk_visit_id::numeric;]
PSQL EXECUTING: [ALTER TABLE meds_dispensed ADD CONSTRAINT fk_visits_id FOREIGN KEY (fk_visit_id) REFERENCES visits(pk_visit_id);]
Failure to execute: ['ALTER TABLE meds_dispensed ADD CONSTRAINT fk_visits_id FOREIGN KEY (fk_visit_id) REFERENCES visits(pk_visit_id);']
FAILURE TO IMPLEMENT CONSTRAINT ON (meds_dispensed.fk_visit_id) to (visits.pk_visit_id)
View File
Whitespace-only changes.
+40
View File
@@ -0,0 +1,40 @@
import pandas as pd
from sqlalchemy import create_engine
from db import DB
class PSQL(DB):
def __init__(self, db_uri):
self.__engine = create_engine(db_uri)
# METHODS FROM DB SUPER CLASS
# ###########################
def read_table(self, table_name) -> pd.DataFrame:
# TODO: implement read_table functionality in PSQL
print("NEED TO IMPLEMENT THIS")
def write_table(
self, data_frame: pd.DataFrame, table_name: str, if_exists: str = "fail"
):
data_frame.to_sql(
con=self.__engine, name=table_name, if_exists=if_exists, index=False
)
# ###########################
def open_connection(self):
return self.__engine.connect()
def execute_command(self, command_string):
print(f"PSQL EXECUTING: [{command_string}]")
result = None
try:
with self.open_connection() as connection:
result = connection.execute(command_string)
except Exception as e:
print(f"Failure to execute: ['{command_string}']")
raise (e) # TODO: determine if crashing is appropriate
return result
+58
View File
@@ -0,0 +1,58 @@
import argparse
import os
import pickle
import sys
import pyodbc
import pandas as pd
from tqdm import tqdm
from fmp import FMP
from psql import PSQL
from utils import edit_distance
from PIL import Image
import io
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("-d", "--dsn", help="dsn name")
parser.add_argument("-u", "--username", help="username for dsn")
parser.add_argument("-p", "--password", help="password for dsn")
return parser.parse_args()
def main(argv):
fmp = FMP(argv.dsn, argv.username, argv.password)
# query = "SELECT GetAs(face_photo, 'JPEG') FROM patients WHERE pk_patient_id=31883 OR pk_patient_id=1"
# result = fmp.cursor.execute(query)
# data = result.fetchall()
# data = [item[0] for item in data]
# print(data, len(data))
# images = [Image.open(io.BytesIO(d)) for d in data]
# [image.show() for image in images]
# table_name = "diagnoses_made"
# print(table_name)
# res_1 = fmp.read_table(table_name)
# print(res_1)
table_name = "globals"
print(table_name)
res_2 = fmp.read_table(table_name)
print(res_2.columns)
print(res_2)
# print(res_1 == res_2)
# table_name = "clinics"
# print(table_name)
# res_3 = fmp.read_table(table_name)
# print(res_3)
# print(type(fmp.execute_command("SELECT face_photo FROM patients WHERE pk_patient_id=31883")["face_photo"][0]))
# print(fmp.execute_command("SELECT face_photo FROM patients WHERE pk_patient_id=31883")["face_photo"][0])
if __name__ == "__main__":
sys.exit(main(parse_args()))
+8
View File
@@ -0,0 +1,8 @@
from psql import PSQL
psql = PSQL("postgres+psycopg2://postgres:HaitiCHI@0.0.0.0:5432/app_db")
psql.execute_command("DROP SCHEMA public CASCADE")
psql.execute_command("CREATE SCHEMA public")
psql.execute_command("GRANT ALL ON SCHEMA public TO postgres")
psql.execute_command("GRANT ALL ON SCHEMA public TO public")
+22
View File
@@ -0,0 +1,22 @@
def edit_distance(str1, str2):
return edit_distance_helper(str1, str2, len(str1) - 1, len(str2) - 1)
def edit_distance_helper(str1, str2, m, n):
if m == 0:
return n
if n == 0:
return m
if str1[m] == str2[n]:
return edit_distance_helper(str1, str2, m - 1, n - 1)
return (
min(
edit_distance_helper(str1, str2, m, n - 1),
edit_distance_helper(str1, str2, m - 1, n),
edit_distance_helper(str1, str2, m - 1, n - 1),
)
+ 1
)
+1 -1
View File
@@ -24,7 +24,7 @@ services:
ports:
- "5432:5432"
expose:
- "5422"
- "5432"
environment:
POSTGRES_DB: app_db
POSTGRES_USER: postgres
+1 -1
View File
@@ -16,7 +16,7 @@ services:
database:
image: "postgres" # use latest official postgres version
env_file:
- database.env # configure postgres
- ./database.env # configure postgres
volumes:
- "database-data:/var/lib/postgresql/data/" # persist data even if container shuts down
ports: