Skip to content

Commit

Permalink
✨(api) integrate postgresql database support
Browse files Browse the repository at this point in the history
We are now able to use a PostgreSQL database to persist data.
  • Loading branch information
jmaupetit committed Mar 28, 2024
1 parent 5e62436 commit d171839
Show file tree
Hide file tree
Showing 19 changed files with 772 additions and 13 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ and this project adheres to

## [Unreleased]

## Added

- Integrate PostgreSQL database persistency in an asynchronous context

## [0.1.0] - 2024-03-26

### Added
Expand Down
36 changes: 33 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,27 @@ default: help
# -- Docker/compose
bootstrap: ## bootstrap the project for development
bootstrap: \
build
build \
migrate-api \
create-api-test-db
.PHONY: bootstrap

build: ## build the app container(s)
$(COMPOSE) build
.PHONY: build

logs: ## display OCPI server logs (follow mode)
down: ## stop and remove all containers
@$(COMPOSE) down
.PHONY: down

logs: ## display all services logs (follow mode)
@$(COMPOSE) logs -f
.PHONY: logs

logs-api: ## display API server logs (follow mode)
@$(COMPOSE) logs -f api
.PHONY: logs-api

run: ## run the whole stack
$(COMPOSE) up -d
.PHONY: run
Expand All @@ -38,7 +48,27 @@ stop: ## stop all servers
@$(COMPOSE) stop
.PHONY: stop

# -- OCPI
# -- Provisioning
create-api-test-db: ## create API test database
@echo "Creating api service test database…"
@$(COMPOSE) exec postgresql bash -c 'psql "postgresql://$${POSTGRES_USER}:$${POSTGRES_PASSWORD}@$${QUALICHARGE_DB_HOST}:$${QUALICHARGE_DB_PORT}/postgres" -c "create database \"$${QUALICHARGE_TEST_DB_NAME}\";"' || echo "Duly noted, skipping database creation."
.PHONY: create-api-test-db

drop-api-test-db: ## drop API test database
@echo "Droping api service test database…"
@$(COMPOSE) exec postgresql bash -c 'psql "postgresql://$${POSTGRES_USER}:$${POSTGRES_PASSWORD}@$${QUALICHARGE_DB_HOST}:$${QUALICHARGE_DB_PORT}/postgres" -c "drop database \"$${QUALICHARGE_TEST_DB_NAME}\";"' || echo "Duly noted, skipping database deletion."
.PHONY: drop-api-test-db

migrate-api: ## run alembic database migrations for the api service
@echo "Running api service database engine…"
@$(COMPOSE) up -d --wait postgresql
@echo "Creating api service database…"
@$(COMPOSE) exec postgresql bash -c 'psql "postgresql://$${POSTGRES_USER}:$${POSTGRES_PASSWORD}@$${QUALICHARGE_DB_HOST}:$${QUALICHARGE_DB_PORT}/postgres" -c "create database \"$${QUALICHARGE_DB_NAME}\";"' || echo "Duly noted, skipping database creation."
@echo "Running migrations for api service…"
@bin/alembic upgrade head
.PHONY: migrate-api

# -- API
lint: ## lint api python sources
lint: \
lint-black \
Expand Down
13 changes: 13 additions & 0 deletions bin/alembic
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash

set -eo pipefail

declare DOCKER_USER
DOCKER_UID="$(id -u)"
DOCKER_GID="$(id -g)"
DOCKER_USER="${DOCKER_UID}:${DOCKER_GID}"

DOCKER_USER=${DOCKER_USER} \
DOCKER_UID=${DOCKER_UID} \
DOCKER_GID=${DOCKER_GID} \
docker compose run --rm api pipenv run alembic -c qualicharge/alembic.ini "$@"
2 changes: 1 addition & 1 deletion bin/pipenv
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

set -eo pipefail

docker compose run --rm -u "pipenv:pipenv" api pipenv "$@"
docker compose run --rm --no-deps -u "pipenv:pipenv" api pipenv "$@"
docker compose build api
18 changes: 17 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
version: '3.8'
version: "3.8"

services:
postgresql:
image: postgres:14
env_file:
- env.d/postgresql
- env.d/api
healthcheck:
test:
- "CMD-SHELL"
- "pg_isready"
- "-d"
- "$${QUALICHARGE_DB_NAME}"
interval: 10s
timeout: 5s
retries: 5

api:
build:
Expand All @@ -16,3 +30,5 @@ services:
- env.d/api
volumes:
- ./src/api:/app
depends_on:
- postgresql
9 changes: 9 additions & 0 deletions env.d/api
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
PORT=8000
QUALICHARGE_ALLOWED_HOSTS=["http://localhost:8010"]
QUALICHARGE_DB_ENGINE=postgresql
QUALICHARGE_DB_HOST=postgresql
QUALICHARGE_DB_NAME=qualicharge-api
QUALICHARGE_DB_PASSWORD=pass
QUALICHARGE_DB_PORT=5432
QUALICHARGE_DB_USER=qualicharge
QUALICHARGE_DEBUG=1
QUALICHARGE_TEST_DB_NAME=test-qualicharge-api
3 changes: 3 additions & 0 deletions env.d/postgresql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
POSTGRES_DB=qualicharge-api
POSTGRES_USER=qualicharge
POSTGRES_PASSWORD=pass
8 changes: 6 additions & 2 deletions src/api/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,21 @@ verify_ssl = true
name = "pypi"

[packages]
alembic = "==1.13.1"
fastapi = "==0.110.0"
psycopg2-binary = "==2.9.9"
pydantic-settings = "==2.2.1"
setuptools = "==69.2.0"
uvicorn = {extras = ["standard"], version = "==0.29.0"}
sqlmodel = "==0.0.16"
uvicorn = {extras = ["standard"] }

[dev-packages]
black = "==24.3.0"
honcho = "==1.1.0"
httpx = "==0.27.0"
mypy = "==1.9.0"
polyfactory = "==2.15.0"
pytest = "==8.1.1"
pytest-httpx = "==0.30.0"
ruff = "==0.3.4"

[requires]
Expand Down
335 changes: 333 additions & 2 deletions src/api/Pipfile.lock

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions src/api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,5 @@ files = "./**/*.py"
exclude = ["/tests/"]

[[tool.mypy.overrides]]
module = [
"py_ocpi.*",
]
module = []
ignore_missing_imports = true
110 changes: 110 additions & 0 deletions src/api/qualicharge/alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# A generic, single database configuration.

[alembic]
# path to migration scripts
script_location = %(here)s/migrations

# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).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 = .

# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python-dateutil library that can be
# installed by adding `alembic[tz]` to the pip requirements
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =

# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40

# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false

# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false

# version location specification; This defaults
# to warren/migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:warren/migrations/versions

# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.

# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false

# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8

sqlalchemy.url = driver://user:pass@localhost/dbname


[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples

# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME

# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
14 changes: 13 additions & 1 deletion src/api/qualicharge/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
"""QualiCharge API root."""

from contextlib import asynccontextmanager

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from ..conf import settings
from ..db import get_engine
from .v1 import app as v1

app = FastAPI()

@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application life span."""
engine = get_engine()
yield
engine.dispose()


app = FastAPI(lifespan=lifespan)


app.add_middleware(
Expand Down
38 changes: 38 additions & 0 deletions src/api/qualicharge/conf.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""QualiCharge API settings."""

from pathlib import Path
from typing import List

from pydantic import computed_field
from pydantic_settings import BaseSettings, SettingsConfigDict


Expand All @@ -15,6 +17,42 @@ class Settings(BaseSettings):
"http://localhost:8010",
]

# API Core Root path
# (used at least by everything that is alembic-configuration-related)
ROOT_PATH: Path = Path(__file__).parent

# Alembic
ALEMBIC_CFG_PATH: Path = ROOT_PATH / "alembic.ini"

# Database
DB_ENGINE: str = "postgresql"
DB_HOST: str = "postgresql"
DB_NAME: str = "warren-api"
DB_USER: str = "fun"
DB_PASSWORD: str = "pass"
DB_PORT: int = 5432
TEST_DB_NAME: str = "test-warren-api"

@computed_field
@property
def DATABASE_URL(self) -> str:
"""Get the database URL as required by SQLAlchemy."""
return (
f"{self.DB_ENGINE}://"
f"{self.DB_USER}:{self.DB_PASSWORD}@"
f"{self.DB_HOST}/{self.DB_NAME}"
)

@computed_field
@property
def TEST_DATABASE_URL(self) -> str:
"""Get the database URL as required by SQLAlchemy."""
return (
f"{self.DB_ENGINE}://"
f"{self.DB_USER}:{self.DB_PASSWORD}@"
f"{self.DB_HOST}/{self.TEST_DB_NAME}"
)

model_config = SettingsConfigDict(
case_sensitive=True, env_nested_delimiter="__", env_prefix="QUALICHARGE_"
)
Expand Down
Loading

0 comments on commit d171839

Please sign in to comment.