Skip to content

Commit

Permalink
some rounding
Browse files Browse the repository at this point in the history
  • Loading branch information
motey committed Aug 30, 2024
1 parent 4f991e9 commit b9c4ea3
Show file tree
Hide file tree
Showing 16 changed files with 201 additions and 52 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build-push-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@ jobs:
repository: dzdde/dzdmedlog
short-description: "The server module for DZDMedLog, a system to document the medical history of study participant."
enable-url-completion: true
readme-filepath: "README.md"
readme-filepath: "DOCKERHUB_README.md"
23 changes: 23 additions & 0 deletions DOCKERHUB_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# DZDMedLog

A Webapplication to log medication history of studyparticipants.

For more details visit: https://github.com/DZD-eV-Diabetes-Research/DZDMedLog

# Run

## Prebuild container

Get or Update the container image

`docker pull dzdde/dzdmedlog`

Run the container

`docker run -v ./database:/opt/medlog/data -p 8888:8888 -e DEMO_MODE=true dzdde/dzdmedlog`

(Alternatively) If you have a WiDo GKV Arzneimittelindex to hand:

`docker run -v ./database:/opt/medlog/data ./GKV_AI_StammPlus:/opt/medlog/arzneimittelindex -p 8888:8888 -e DEMO_MODE=true dzdde/dzdmedlog`

visit http://localhost:8888
58 changes: 33 additions & 25 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,49 +9,57 @@ RUN bun install && bun run build && bunx nuxi generate

# BACKEND BUILD AND RUN STAGE
FROM python:3.11 AS medlog-backend
RUN python3 -m pip install --upgrade pip
COPY --from=medlog-frontend-build /frontend_build/.output/public /app
ENV DOCKER_MODE=1
ENV FRONTEND_FILES_DIR=/app
#RUN apt-get update && apt-get install git -y
#
ARG APPNAME=DZDMedLog
ARG BASEDIR=/opt/medlog
ARG MODULENAME=medlogserver
#
ENV MEDLOG_DOCKER_BASEDIR=$BASEDIR
ENV DOCKER_MODE=1
ENV FRONTEND_FILES_DIR=$BASEDIR/medlogfrontend

# prep stuff
RUN mkdir -p /opt/$APPNAME/$MODULENAME
WORKDIR /opt/$APPNAME
RUN mkdir -p $BASEDIR/medlogserver
RUN mkdir -p $BASEDIR/medlogfrontend
RUN mkdir -p $BASEDIR/data
RUN mkdir -p $BASEDIR/provisioning/database
RUN mkdir -p $BASEDIR/provisioning/arzneimittelindex

# Copy frontend dist from pre stage
COPY --from=medlog-frontend-build /frontend_build/.output/public $BASEDIR/medlogfrontend


# Install Server
WORKDIR $BASEDIR

RUN python3 -m pip install --upgrade pip
RUN pip install -U pip-tools

# Generate requirements.txt based on depenencies defined in pyproject.toml
COPY MedLog/backend/pyproject.toml /opt/$APPNAME/$MODULENAME
RUN pip-compile -o /opt/$APPNAME/requirements.txt /opt/$APPNAME/$MODULENAME/pyproject.toml
COPY MedLog/backend/pyproject.toml $BASEDIR/medlogserver/pyproject.toml
RUN pip-compile -o $BASEDIR/requirements.txt $BASEDIR/medlogserver/pyproject.toml

# Install requirements
RUN pip install -U -r /opt/$APPNAME/requirements.txt
RUN pip install -U -r $BASEDIR/requirements.txt

# install app
COPY MedLog/backend/medlogserver /opt/$APPNAME/$MODULENAME
COPY MedLog/backend/medlogserver $BASEDIR/medlogserver

# copy .git folder to be able to generate version file
COPY .git /opt/$APPNAME/.git
RUN echo "__version__ = '$(python -m setuptools_scm 2>/dev/null | tail -n 1)'" > /opt/$APPNAME/$MODULENAME/__version__.py
# Remove git folder to reduce image size
RUN rm -r /opt/$APPNAME/.git
COPY .git $BASEDIR/.git
RUN echo "__version__ = '$(python -m setuptools_scm 2>/dev/null | tail -n 1)'" > $BASEDIR/medlogserver/__version__.py
# Remove git folder
RUN rm -r $BASEDIR/.git

#Copy default app data provisioning files
RUN mkdir /prov
COPY MedLog/backend/provisioning_data /provisioning
COPY MedLog/backend/provisioning_data $BASEDIR/provisioning/database



# Install data
RUN mkdir -p /data/db
# set base config
WORKDIR /opt/$APPNAME/$MODULENAME
WORKDIR $BASEDIR/medlogserver
# set base config
ENV SERVER_LISTENING_HOST=0.0.0.0
ENV APP_PROVISIONING_DATA_YAML_FILES='[]'
ENV DRUG_TABLE_PROVISIONING_SOURCE_DIR=/data/provisioning/arzneimittelindex
ENV SQL_DATABASE_URL=sqlite+aiosqlite:////data/db/local.sqlite
ENV DRUG_TABLE_PROVISIONING_SOURCE_DIR=$BASEDIR/provisioning/arzneimittelindex
ENV SERVER_HOSTNAME=localhost
ENV SQL_DATABASE_URL="sqlite+aiosqlite:///$BASEDIR/data/medlog.db"
ENTRYPOINT ["python", "./main.py"]
#CMD [ "python", "./main.py" ]
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
frontend:
./build_frontend.sh
container:
./build_docker.sh
26 changes: 20 additions & 6 deletions MedLog/backend/medlogserver/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import List, Annotated, Optional, Literal, Dict
from typing_extensions import Self
from pydantic_settings import BaseSettings, SettingsConfigDict
import os
from pydantic import (
Expand All @@ -9,7 +11,7 @@
StringConstraints,
model_validator,
)
from typing import List, Annotated, Optional, Literal, Dict

from pathlib import Path, PurePath
import socket
from textwrap import dedent
Expand All @@ -19,11 +21,13 @@


class Config(BaseSettings):
APP_NAME: str = "DZD MedLog"
APP_NAME: str = "DZDMedLog"
DOCKER_MODE: bool = False
FRONTEND_FILES_DIR: str = Field(
description="The generated nuxt dir that contains index.html,...",
default="MedLog/frontend/.output/public",
default=str(
Path(Path(__file__).parent.parent.parent, "frontend/.output/public")
),
)
LOG_LEVEL: Literal["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"] = Field(
default="INFO"
Expand All @@ -47,7 +51,8 @@ def demo_mode(self_data: dict):
self_data["APP_PROVISIONING_DATA_YAML_FILES"] = [
str(
Path(
"/provisioning/demo_data/single_study_demo_data.yaml",
os.getenv("MEDLOG_DOCKER_BASEDIR", "/opt/medlog"),
"provisioning/database/demo_data/single_study_demo_data.yaml",
)
)
]
Expand Down Expand Up @@ -103,8 +108,16 @@ def get_server_url(self) -> AnyHttpUrl:
return AnyHttpUrl(f"{proto}://{self.SERVER_HOSTNAME}")

CLIENT_URL: Optional[str] = Field(
default="http://localhost:5173", description="Origin url"
default=None,
description="The URL where the client is hosted. Usualy it comes with the server",
)

@model_validator(mode="after")
def set_empty_client_url(self: Self):
if self.CLIENT_URL is None:
self.CLIENT_URL = self.get_server_url()
return self

SQL_DATABASE_URL: AnyUrl = Field(default="sqlite+aiosqlite:///./local.sqlite")

ADMIN_USER_NAME: str = Field(default="admin")
Expand All @@ -130,7 +143,7 @@ def get_server_url(self) -> AnyHttpUrl:
description="Default data like some background jobs and vocabulary that is always loaded in the database. Under normal circustances this is nothing you need to changed. if you need to provision data like a Study into the database use the APP_PROVISIONING_DATA_YAML_FILES param.",
default=str(Path(Path(__file__).parent, "default_data.yaml")),
)

"""Remove me on next refactor
APP_CONFIG_PRESCRIBED_BY_DOC_ANSWERS: Dict = Field(
default={
"PRESCRIBED": "prescribed",
Expand All @@ -139,6 +152,7 @@ def get_server_url(self) -> AnyHttpUrl:
"UNKNOWN": "unknown",
}
)
"""

APP_STUDY_PERMISSION_SYSTEM_DISABLED_BY_DEFAULT: bool = Field(
default=False,
Expand Down
32 changes: 30 additions & 2 deletions MedLog/backend/medlogserver/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,15 @@ async def serve_frontend(path_name: Optional[str] = None):
file = os.path.join(config.FRONTEND_FILES_DIR, "index.html")
return FileResponse(file)

if config.CLIENT_URL == config.get_server_url():
if (
not Path(config.FRONTEND_FILES_DIR).exists()
or not Path(config.FRONTEND_FILES_DIR, "index.html").exists()
):
raise ValueError(
"Can not find frontend files. Maybe you need to build the frontend first. Try to run 'make frontend'"
)

uvicorn_log_config: Dict = LOGGING_CONFIG
uvicorn_log_config["loggers"][APP_LOGGER_DEFAULT_NAME] = {
"handlers": ["default"],
Expand All @@ -107,10 +116,29 @@ async def serve_frontend(path_name: Optional[str] = None):
uvicorn_server = uvicorn.Server(config=uvicorn_config)

event_loop.run_until_complete(init_db())
if config.DEMO_MODE:
log.info(
f"Hey, we are in demo mode. Login as admin with the following account:"
)
log.info(
f"USERNAME: {config.ADMIN_USER_NAME}\nPASSWORD: {config.ADMIN_USER_PW}"
)
if with_background_worker:
# Start background worker in second process
run_background_worker(run_in_extra_process=True)
event_loop.run_until_complete(uvicorn_server.serve())
background_worker = run_background_worker(run_in_extra_process=True)
try:
event_loop.run_until_complete(uvicorn_server.serve())
except (KeyboardInterrupt, Exception) as e:
if isinstance(e, KeyboardInterrupt):
log.info("KeyboardInterrupt shutdown...")
if isinstance(e, Exception):
log.info("Panic shutdown...")
if background_worker is not None:
log.info("Stop background worker process...")
background_worker.kill()
background_worker.join()
if isinstance(e, Exception):
raise e


if __name__ == "__main__":
Expand Down
13 changes: 10 additions & 3 deletions MedLog/backend/medlogserver/model/intake.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,16 @@
log = get_logger()
config = Config()

AdministeredByDoctorAnswers = enum.Enum(
"AdministeredByDoctorAnswers", config.APP_CONFIG_PRESCRIBED_BY_DOC_ANSWERS
)
# AdministeredByDoctorAnswers = enum.Enum(
# "AdministeredByDoctorAnswers", config.APP_CONFIG_PRESCRIBED_BY_DOC_ANSWERS
# )


class AdministeredByDoctorAnswers(str, enum.Enum):
PRESCRIBED = "prescribed"
RECOMMENDED = "recommended"
NO = "no"
UNKNOWN = "unknown"


class IntakeRegularOrAsNeededAnswers(str, enum.Enum):
Expand Down
4 changes: 2 additions & 2 deletions MedLog/backend/medlogserver/model/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ def validmail(cls, email):
return email

@model_validator(mode="after")
def val_display_name(self, values):
def val_display_name(self):
"""if no display name is set for now, we copy the identifying `user_name`"""
if self.display_name is None:
self.display_name = self.user_name
return values
return self


class _UserWithName(UserBase, table=False):
Expand Down
12 changes: 10 additions & 2 deletions MedLog/backend/medlogserver/model/user_auth.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# Basics
from typing import AsyncGenerator, List, Optional, Literal, Sequence
from typing_extensions import Self

# Libs
import enum
import uuid
from pydantic import SecretStr
from pydantic import SecretStr, model_validator
from sqlmodel import Field, Column, Enum, UniqueConstraint
from passlib.context import CryptContext

Expand Down Expand Up @@ -45,7 +46,14 @@ class UserAuthUpdate(BaseTable, table=False):


class UserAuthCreate(_UserAuthBase, UserAuthUpdate, table=False):
pass
@model_validator(mode="after")
def check_password_needed(self: Self):
if (
self.password is None
and self.auth_source_type == AllowedAuthSourceTypes.local
):
raise ValueError("No empty password allowed for local users")
return self


class UserAuth(_UserAuthBase, table=True):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,12 @@ async def _parse_provisioning_file(self, path: Path):
file_content = yaml.safe_load(file_obj)
except:
log.error(
"Failed parsing provisioning data file at '{path}'. Data provisining will be canceled."
"Failed parsing provisioning data file at '{path}'. Data provisioning will be canceled."
)
raise
if not "items" in file_content:
raise ValueError(
"Unexpected format in provisioning data file at '{path}'. Data provisining will be canceled."
"Unexpected format in provisioning data file at '{path}'. Data provisioning will be canceled."
)
if "items" in file_content and file_content["items"] is None:
return
Expand Down
8 changes: 5 additions & 3 deletions MedLog/backend/medlogserver/worker/worker.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List
from typing import List, Optional
import asyncio
import multiprocessing
from apscheduler.schedulers.asyncio import AsyncIOScheduler
Expand Down Expand Up @@ -63,15 +63,17 @@ def _start_background_scheduler(event_loop=None):


def run_background_worker(
run_in_extra_process: bool = True, event_loop: asyncio.AbstractEventLoop = None
):
run_in_extra_process: bool = True,
event_loop: Optional[asyncio.AbstractEventLoop] = None,
) -> Optional[multiprocessing.Process]:
if run_in_extra_process:
log.info("Start background worker in extra process...")
background_worker_process = multiprocessing.Process(
target=_start_background_scheduler, name="DZDMedLogBackgroundWorker"
)
background_worker_process.start()
log.info(f"Started background worker (Process: {background_worker_process})")
return background_worker_process
else:
if event_loop is None:
event_loop = asyncio.get_event_loop()
Expand Down
2 changes: 0 additions & 2 deletions MedLog/backend/provisioning_data/default_data/README.md

This file was deleted.

Binary file modified MedLog/frontend/bun.lockb
Binary file not shown.
Loading

0 comments on commit b9c4ea3

Please sign in to comment.