Skip to content

Commit

Permalink
Add database schema initialization
Browse files Browse the repository at this point in the history
  • Loading branch information
berrydenhartog committed Jun 11, 2024
1 parent e5347bf commit 87bdd07
Show file tree
Hide file tree
Showing 40 changed files with 491 additions and 542 deletions.
24 changes: 0 additions & 24 deletions .env.test

This file was deleted.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ __pypackages__/
# tad tool
tad.log*
database.sqlite3
output/
8 changes: 6 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
"request": "launch",
"module": "uvicorn",
"justMyCode": false,
"args": [ "--log-level", "warning" ,"tad.main:app"],
"args": [
"--log-level",
"warning",
"tad.main:app"
],
"cwd": "${workspaceFolder}/",
"env": {
"PYTHONPATH": "${workspaceFolder}"
Expand All @@ -20,7 +24,7 @@
"request": "launch",
"module": "pytest",
"cwd": "${workspaceFolder}",
"justMyCode": true,
"justMyCode": false,
"args": []
}
]
Expand Down
7 changes: 5 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,13 @@ USER tad

COPY --chown=root:root --chmod=755 ./tad /app/tad
COPY --chown=root:root --chmod=755 alembic.ini /app/alembic.ini
COPY --chown=root:root --chmod=755 .env /app/.env
COPY --chown=root:root --chmod=755 prod.env /app/.env
COPY --chown=root:root --chmod=755 LICENSE /app/LICENSE
COPY --chown=tad:tad --chmod=755 docker-entrypoint.sh /app/docker-entrypoint.sh

ENV PYTHONPATH=/app/
WORKDIR /app/

CMD ["python", "-m", "uvicorn", "--host", "0.0.0.0", "tad.main:app", "--log-level", "warning" ]
ENV PATH="/app/:$PATH"

CMD [ "docker-entrypoint.sh" ]
7 changes: 4 additions & 3 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ services:
db:
condition: service_healthy
env_file:
- path: .env
- path: prod.env
required: true
environment:
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?Variable not set}
- ENVIRONMENT=demo
ports:
- 8000:8000
healthcheck:
Expand All @@ -25,7 +26,7 @@ services:
- app-db-data:/var/lib/postgresql/data/pgdata
- ./database/:/docker-entrypoint-initdb.d/:cached
env_file:
- path: .env
- path: prod.env
required: true
environment:
- PGDATA=/var/lib/postgresql/data/pgdata
Expand All @@ -40,7 +41,7 @@ services:
ports:
- 8080:8080
environment:
- PGADMIN_DEFAULT_EMAIL=${PGADMIN_DEFAULT_EMAIL:-[email protected]}
- PGADMIN_DEFAULT_EMAIL=${PGADMIN_DEFAULT_EMAIL:-[email protected]}
- PGADMIN_DEFAULT_PASSWORD=${PGADMIN_DEFAULT_PASSWORD:?Variable not set}
- PGADMIN_LISTEN_PORT=${PGADMIN_LISTEN_PORT:-8080}
depends_on:
Expand Down
3 changes: 1 addition & 2 deletions database/init-user-db.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,5 @@ set -e
# todo(berry): make user and database variables
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE USER tad WITH PASSWORD 'changethis';
CREATE DATABASE tad;
GRANT ALL PRIVILEGES ON DATABASE tad TO tad;
CREATE DATABASE tad OWNER tad;
EOSQL
51 changes: 51 additions & 0 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env bash

DATABASE_MIGRATE=""
HOST="0.0.0.0"
LOGLEVEL="warning"
PORT="8000"

while getopts "dh:l:p:" opt; do
case $opt in
d)
DATABASE_MIGRATE="True"
;;
h)
HOST=$OPTARG
;;
l)
LOGLEVEL=$OPTARG
;;
p)
PORT=$OPTARG
;;
:)
echo "Option -${OPTARG} requires an argument."
exit 1
;;

?)
echo "Invalid option: $OPTARG"

echo "Usage: docker-entrypoint.sh [-d] [-h host] [-l loglevel]"
exit 1
;;
esac
done

echo "DATABASE_MIGRATE: $DATABASE_MIGRATE"
echo "HOST: $HOST"
echo "LOGLEVEL: $LOGLEVEL"
echo "PORT: $PORT"


if [ -z $DATABASE_MIGRATE ]; then
echo "Upgrading database"
if ! alembic upgrade head; then
echo "Failed to upgrade database"
exit 1
fi
fi

echo "Starting server"
python -m uvicorn --host "$HOST" tad.main:app --port "$PORT" --log-level "$LOGLEVEL"
110 changes: 55 additions & 55 deletions poetry.lock

Large diffs are not rendered by default.

8 changes: 2 additions & 6 deletions .env → prod.env
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
# Domain
DOMAIN=localhost

# Environment: local, staging, production
ENVIRONMENT=local
PROJECT_NAME="TAD"
# Environment: local, production, demo
ENVIRONMENT=production

# TAD backend
BACKEND_CORS_ORIGINS="http://localhost,https://localhost,http://127.0.0.1,https://127.0.0.1"
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ pyyaml = "^6.0.1"
pytest = "^8.2.1"
coverage = "^7.5.3"
httpx = "^0.27.0"
urllib3 = "^2.2.1"
playwright = "^1.44.0"
pytest-playwright = "^0.5.0"

Expand Down Expand Up @@ -107,10 +106,12 @@ title = "tad"
testpaths = [
"tests"
]
addopts = "--strict-markers"
addopts = "--strict-markers -v -q"
filterwarnings = [
"ignore::UserWarning"
]
log_cli = true
log_cli_level = "INFO"

[tool.liccheck]
level = "PARANOID"
Expand Down
5 changes: 5 additions & 0 deletions script/build
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

set -x

docker build . -t ghcr.io/minbzk/tad:latest "$@"
2 changes: 1 addition & 1 deletion script/format
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

set -x

ruff format $@
ruff format "$@"
3 changes: 1 addition & 2 deletions script/lint
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#!/usr/bin/env bash

set -e
set -x

ruff check --fix $@
ruff check --fix "$@"
8 changes: 4 additions & 4 deletions script/test
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
set -e
set -x

coverage run -m pytest $@
if [ $? -ne 0 ]; then

if ! coverage run -m pytest "$@" ; then
echo "Test failed"
exit 1
fi

coverage report
coverage html
coverage lcov
pyright $@
if [ $? -ne 0 ]; then

if ! pyright; then
echo "Typecheck failed"
exit 1
fi
4 changes: 1 addition & 3 deletions tad/api/routes/deps.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
from fastapi.templating import Jinja2Templates
from jinja2 import Environment

from tad.core.config import settings

env = Environment(
autoescape=True,
)
templates = Jinja2Templates(directory=settings.TEMPLATE_DIR, env=env)
templates = Jinja2Templates(directory="tad/site/templates/", env=env)
9 changes: 1 addition & 8 deletions tad/api/routes/root.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
from fastapi import APIRouter
from fastapi.responses import FileResponse, RedirectResponse

from tad.core.config import settings
from fastapi.responses import RedirectResponse

router = APIRouter()


@router.get("/")
async def base() -> RedirectResponse:
return RedirectResponse("/pages/")


@router.get("/favicon.ico", include_in_schema=False)
async def favicon():
return FileResponse(settings.STATIC_DIR + "/favicon.ico")
53 changes: 24 additions & 29 deletions tad/core/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import secrets
from functools import lru_cache
from typing import Any, TypeVar

from pydantic import (
Expand All @@ -12,39 +13,24 @@
from tad.core.exceptions import SettingsError
from tad.core.types import DatabaseSchemaType, EnvironmentType, LoggingLevelType

logger = logging.getLogger(__name__)

# Self type is not available in Python 3.10 so create our own with TypeVar
SelfSettings = TypeVar("SelfSettings", bound="Settings")

PROJECT_NAME: str = "TAD"
PROJECT_DESCRIPTION: str = "Transparency of Algorithmic Decision making"
VERSION: str = "0.1.0" # replace in CI/CD pipeline


class Settings(BaseSettings):
# todo(berry): investigate yaml, toml or json file support for SettingsConfigDict
# todo(berry): investigate multiple .env files support for SettingsConfigDict
model_config = SettingsConfigDict(
env_file=(".env", ".env.test", ".env.prod"), env_ignore_empty=True, extra="ignore"
)
SECRET_KEY: str = secrets.token_urlsafe(32)

DOMAIN: str = "localhost"
ENVIRONMENT: EnvironmentType = "local"

@computed_field # type: ignore[misc]
@property
def server_host(self) -> str:
if self.ENVIRONMENT == "local":
return f"http://{self.DOMAIN}"
return f"https://{self.DOMAIN}"

VERSION: str = "0.1.0"

LOGGING_LEVEL: LoggingLevelType = "INFO"
LOGGING_CONFIG: dict[str, Any] | None = None

PROJECT_NAME: str = "TAD"
PROJECT_DESCRIPTION: str = "Transparency of Algorithmic Decision making"

STATIC_DIR: str = "tad/site/static/"
TEMPLATE_DIR: str = "tad/site/templates"

# todo(berry): create submodel for database settings
APP_DATABASE_SCHEME: DatabaseSchemaType = "sqlite"
APP_DATABASE_DRIVER: str | None = None
Expand All @@ -55,22 +41,22 @@ def server_host(self) -> str:
APP_DATABASE_PASSWORD: str | None = None
APP_DATABASE_DB: str = "tad"

APP_DATABASE_FILE: str = "database.sqlite3"
APP_DATABASE_FILE: str = "/database.sqlite3"

model_config = SettingsConfigDict(extra="ignore")

@computed_field # type: ignore[misc]
@property
def SQLALCHEMY_DATABASE_URI(self) -> str:
logging.info(f"test: {self.APP_DATABASE_SCHEME}")

if self.APP_DATABASE_SCHEME == "sqlite":
return str(MultiHostUrl.build(scheme=self.APP_DATABASE_SCHEME, host="", path=self.APP_DATABASE_FILE))

scheme: str = (
f"{self.APP_DATABASE_SCHEME}+{self.APP_DATABASE_DRIVER}"
if isinstance(self.APP_DATABASE_DRIVER, str)
else self.APP_DATABASE_SCHEME
)

if self.APP_DATABASE_SCHEME == "sqlite":
return f"{scheme}://{self.APP_DATABASE_FILE}"

return str(
MultiHostUrl.build(
scheme=scheme,
Expand All @@ -82,11 +68,20 @@ def SQLALCHEMY_DATABASE_URI(self) -> str:
)
)

@SQLALCHEMY_DATABASE_URI.setter
def SQLALCHEMY_DATABASE_URI(self, value: str) -> None:
self.SQLALCHEMY_DATABASE_URI = value

@model_validator(mode="after")
def _enforce_database_rules(self: SelfSettings) -> SelfSettings:
if self.ENVIRONMENT != "local" and self.APP_DATABASE_SCHEME == "sqlite":
if self.ENVIRONMENT == "production" and self.APP_DATABASE_SCHEME == "sqlite":
raise SettingsError("SQLite is not supported in production")
return self


settings = Settings() # type: ignore
# TODO(berry): make it a function with lrucache


@lru_cache(maxsize=8)
def get_settings(**kwargs: Any) -> Settings:
return Settings(**kwargs)
Loading

0 comments on commit 87bdd07

Please sign in to comment.