Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Connect the app to postgres #60

Merged
merged 6 commits into from Jan 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/test-backend.yaml
Expand Up @@ -27,6 +27,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 @@ -42,6 +62,7 @@ jobs:
env:
VERSION: test
RELEASEID: test
DB_URI: postgresql+asyncpg://admin:admin@postgres/aof
run: |
coverage run --concurrency=thread,greenlet --source=app -m pytest
coverage 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 @@ -40,6 +40,8 @@ services:

app:
container_name: app-aof
depends_on:
- postgres
build:
context: .
target: development
Expand All @@ -48,6 +50,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