Skip to content

Commit

Permalink
Connect the app to postgres
Browse files Browse the repository at this point in the history
Using 2.0 sqlalchemy style for future compatibility.
The `get_db` function can be used as a dependency to inject a working
db session where it will be needed.
  • Loading branch information
pozsa committed Jan 19, 2022
1 parent a3f93cb commit 8d77ee9
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 2 deletions.
21 changes: 21 additions & 0 deletions .github/workflows/test-backend.yaml
Expand Up @@ -25,6 +25,26 @@ jobs:
run:
# Step into backend
working-directory: ./backend
# Service containers to run with `container-job`
strategy:
matrix:
postgres: [13-alpine, 14-alpine]
services:
# Label used to access the service container
postgres:
# Docker Hub image
image: postgres:${{ matrix.postgres }}
# Provide the password for postgres
env:
POSTGRES_USER: admin
POSTGRES_PASSWORD: admin
POSTGRES_DB: aof
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout Code
uses: actions/checkout@v2
Expand All @@ -45,6 +65,7 @@ jobs:
env:
VERSION: test
RELEASEID: test
DB_URI: postgresql+asyncpg://admin:admin@postgres/aof
run: |
python -m pytest --cov=app --cov-report=xml
Expand Down
1 change: 1 addition & 0 deletions backend/README.md
Expand Up @@ -9,6 +9,7 @@
.
├── app # src for the application
│ ├── config.py # reading in settings from env
│ ├── database.py # configuring database connection
│ ├── main.py # main entrypoint for the application
│ ├── routers # route definitions
│ └── schemas # pydantic model definitions
Expand Down
3 changes: 3 additions & 0 deletions backend/app/config.py
Expand Up @@ -6,6 +6,9 @@
class Settings(pydantic.BaseSettings):
version: str
releaseId: str
db_uri: pydantic.PostgresDsn
db_echo: bool = False
db_hide_params: bool = True


@functools.lru_cache
Expand Down
45 changes: 45 additions & 0 deletions backend/app/database.py
@@ -0,0 +1,45 @@
from typing import AsyncIterator

import sqlalchemy as sa
import sqlalchemy.ext.asyncio as saa
from sqlalchemy import orm

from app import config

settings = config.get_settings()


# echo: if True, the Engine will log all statements as well as a repr() of their
# parameter lists to the default log handler, which defaults to sys.stdout for output.
#
# hide_parameters: Boolean, when set to True, SQL statement parameters will not be
# displayed in INFO logging nor will they be formatted into the string representation
# of StatementError objects.
#
# future=True: Use the 2.0 style Engine and Connection API.
engine = saa.create_async_engine(
settings.db_uri,
echo=settings.db_echo,
hide_parameters=settings.db_hide_params,
future=True,
)

# https://alembic.sqlalchemy.org/en/latest/naming.html
convention = {
"ix": "ix_%(column_0_label)s",
"uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_%(constraint_name)s",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s",
}
mapper_registry = orm.registry(sa.MetaData(naming_convention=convention))


async def get_db() -> AsyncIterator[saa.AsyncSession]:
# https://docs.sqlalchemy.org/en/14/orm/extensions/asyncio.html#preventing-implicit-io-when-using-asyncsession
# expire_on_commit=False will prevent attributes from being expired after commit
async_session = saa.AsyncSession(engine, expire_on_commit=False)
try:
yield async_session
finally:
await async_session.close()
31 changes: 30 additions & 1 deletion backend/app/routers/health.py
@@ -1,7 +1,11 @@
import time

import fastapi
import sqlalchemy as sa
from fastapi import responses
from sqlalchemy.ext.asyncio import AsyncSession

from app import config, schemas
from app import config, database, schemas

router = fastapi.APIRouter()

Expand All @@ -18,12 +22,37 @@ class HealthResponse(responses.JSONResponse):
async def get_health(
response: HealthResponse,
settings: config.Settings = fastapi.Depends(config.get_settings),
db: AsyncSession = fastapi.Depends(database.get_db),
) -> schemas.Health:
response.headers["Cache-Control"] = "max-age=3600"

stmt = sa.text("SELECT now(), pg_postmaster_start_time()")
t0 = time.perf_counter_ns()
result = await db.execute(stmt)
response_time_ns = time.perf_counter_ns() - t0
now, start = result.one()

content = {
"status": schemas.Status.PASS,
"version": settings.version,
"releaseId": settings.releaseId,
"checks": {
"postgresql:responseTime": [
{
"componentType": "datastore",
"observedValue": response_time_ns / 1000000,
"observedUnit ": "ms",
"time": now,
}
],
"postgresql:uptime": [
{
"componentType": "datastore",
"observedValue": now - start,
"observedUnit ": "s",
"time": now,
}
],
},
}
return schemas.Health(**content)
2 changes: 2 additions & 0 deletions backend/app/schemas/health.py
@@ -1,4 +1,5 @@
import enum
from typing import Any

import pydantic

Expand All @@ -13,3 +14,4 @@ class Health(pydantic.BaseModel):
status: Status
version: str
releaseId: str
checks: dict[str, list[dict[str, Any]]]
5 changes: 5 additions & 0 deletions backend/docker-compose.yaml
Expand Up @@ -39,6 +39,8 @@ services:

app:
container_name: app-aof
depends_on:
- postgres
build:
context: .
target: development
Expand All @@ -47,6 +49,9 @@ services:
environment:
- VERSION=dev
- RELEASEID=dev
- DB_URI=postgresql+asyncpg://admin:admin@postgres/aof
- DB_ECHO=True
- DB_HIDE_PARAMS=False
ports:
- 8000:8000
command: uvicorn app.main:app --host 0.0.0.0 --reload
Expand Down
7 changes: 6 additions & 1 deletion backend/tests/routers/test_health.py
Expand Up @@ -15,11 +15,16 @@ def test_get_health() -> None:
assert response.headers["content-type"] == "application/health+json"
assert response.headers["cache-control"] == "max-age=3600"
payload = response.json()
for key in ["status", "version", "releaseId"]:
for key in ["status", "version", "releaseId", "checks"]:
assert key in payload
assert payload["status"] == "pass"
assert payload["version"] == settings.version
assert payload["releaseId"] == settings.releaseId
for key in ["postgresql:responseTime", "postgresql:uptime"]:
assert key in payload["checks"]
for item in payload["checks"][key]:
assert "componentType" in item
assert item["componentType"] == "datastore"


@pytest.mark.parametrize(
Expand Down

0 comments on commit 8d77ee9

Please sign in to comment.