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

Use pydantic-settings for app settings object #217

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ else
endif


.PHONY: migrate-and-run
migrate-and-run: migrations run


## docker-build: Build a Docker image
.PHONY: docker-build
docker-build: clean
Expand Down
6 changes: 3 additions & 3 deletions client/src/GovSingleConsent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,10 @@ export class GovSingleConsent {
request(
getConsentsUrl,
{ timeout: 1000 },
({ status: consents }: { status: Consents }) => {
this.updateBrowserConsents(consents)
({consent}) => {
this.updateBrowserConsents(consent)
this._consentsUpdateCallback(
consents,
consent,
GovSingleConsent.isConsentPreferencesSet(),
null
)
Expand Down
6 changes: 3 additions & 3 deletions consent_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from fastapi_etag import add_exception_handler
from starlette.middleware.sessions import SessionMiddleware

from consent_api import config
from consent_api.config import settings
from consent_api.routers import consent
from consent_api.routers import dummy_service
from consent_api.routers import healthcheck
Expand All @@ -18,7 +18,7 @@

@app.on_event("startup")
async def startup_event():
if config.FLAG_FIXTURES:
if settings.flag_fixtures:
await register_origins_for_e2e_testing()


Expand All @@ -28,7 +28,7 @@ async def startup_event():
)
app.add_middleware(
SessionMiddleware,
secret_key=config.SECRET_KEY,
secret_key=settings.secret_key,
)
app.include_router(consent.router)
app.include_router(healthcheck.router)
Expand Down
4 changes: 2 additions & 2 deletions consent_api/commands/delete_expired_consent.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from sqlalchemy.sql.expression import delete
from sqlmodel import col

from consent_api import config
from consent_api.config import settings
from consent_api.database import engine
from consent_api.models import UserConsent

Expand All @@ -18,7 +18,7 @@ async def delete_expired_consent():
await conn.execute(
delete(UserConsent).where(
col(UserConsent.updated_at)
< (datetime.now() - timedelta(days=config.CONSENT_EXPIRY_DAYS)),
< (datetime.now() - timedelta(days=settings.consent_expiry_days)),
)
)
await engine.dispose()
Expand Down
52 changes: 52 additions & 0 deletions consent_api/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""App configuration."""
import os

from pydantic import Field
from pydantic_settings import BaseSettings
from pydantic_settings import SettingsConfigDict


class Settings(BaseSettings):
secret_key: bytes = os.urandom(24)
sqlalchemy_database_uri: str = Field(
alias="DATABASE_URL",
default="postgresql+asyncpg://localhost:5432/consent_api",
)
consent_expiry_days: int = 7
consent_api_origin: str | None = None
other_service_origin: str | None = None
flag_fixtures: bool = True

model_config = SettingsConfigDict(env_file=".env", extra="ignore")


class Production(Settings):
...


class Staging(Settings):
consent_api_origin: str = "https://gds-single-consent-staging.app"


class Testing(Settings):
consent_api_origin: str = "http://consent-api"
other_service_origin: str = "http://dummy-service-1"


class Development(Settings):
...


def get_settings():
env = os.getenv("ENV", "development")
try:
return {subclass.__name__: subclass for subclass in Settings.__subclasses__()}[
env.title()
]()
except KeyError as err:
raise ValueError(f"Invalid ENV={env}") from err


settings = get_settings()

print(f"Environment: {settings.__class__.__name__}")
69 changes: 0 additions & 69 deletions consent_api/config/__init__.py

This file was deleted.

13 changes: 0 additions & 13 deletions consent_api/config/defaults.py

This file was deleted.

4 changes: 2 additions & 2 deletions consent_api/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.orm import sessionmaker

from consent_api import config
from consent_api.config import settings

engine = create_async_engine(
config.SQLALCHEMY_DATABASE_URI,
str(settings.sqlalchemy_database_uri),
json_serializer=FriendlyEncoder().encode,
pool_size=20,
max_overflow=20,
Expand Down
4 changes: 2 additions & 2 deletions consent_api/jinja.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import jinja2
from starlette.templating import Jinja2Templates

from consent_api.config import CONSENT_API_URL
from consent_api.config import settings

templates = Jinja2Templates(
directory="consent_api/templates", # required but overridden by loader arg
Expand All @@ -18,5 +18,5 @@
),
)
templates.env.globals.update(
{"CONSENT_API_URL": CONSENT_API_URL},
{"CONSENT_API_URL": f"{settings.consent_api_origin}/api/v1/consent/"},
)
4 changes: 2 additions & 2 deletions consent_api/routers/dummy_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
from fastapi import Request
from starlette.responses import Response

from consent_api import config
from consent_api.config import settings
from consent_api.jinja import templates

router = APIRouter(include_in_schema=False)
get = router.get

DUMMY_SERVICE_PREFIX = "/dummy-service"
OTHER_SERVICE_URL = f"{config.OTHER_SERVICE_ORIGIN}{DUMMY_SERVICE_PREFIX}/"
OTHER_SERVICE_URL = f"{settings.other_service_origin}{DUMMY_SERVICE_PREFIX}/"


@get("/")
Expand Down
4 changes: 2 additions & 2 deletions consent_api/tests/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ def __init__(self):
def get_consent(self, uid):
"""Retrieve the consent status for the given UID, if it exists."""
obj = httpx.get(f"{self.url}/api/v1/consent/{uid}").json()
if obj and obj["status"]:
return CookieConsent(**obj["status"])
if obj and obj["consent"]:
return CookieConsent(**obj["consent"])
return None
6 changes: 3 additions & 3 deletions consent_api/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ async def test_set_consent(client, db_session):
assert response.status_code == 201
response_json = response.json()
assert len(response_json["uid"]) == 22
assert response_json["status"] == CookieConsent.ACCEPT_ALL.dict()
assert response_json["consent"] == CookieConsent.ACCEPT_ALL.dict()

uc = (await get_user_consent(response_json["uid"], db_session)).first()
assert uc.consent == CookieConsent.ACCEPT_ALL.dict()
Expand All @@ -58,7 +58,7 @@ async def test_get_consent(client, db_session):
assert response.status_code == 200
response_json = response.json()
assert response_json["uid"] == existing.uid
assert response_json["status"] == existing.consent
assert response_json["consent"] == existing.consent


@pytest.mark.asyncio
Expand All @@ -79,7 +79,7 @@ async def test_update_consent(client, db_session):
assert response.status_code == 200
response_json = response.json()
assert response_json["uid"] == existing.uid
assert response_json["status"] == CookieConsent.REJECT_ALL.dict()
assert response_json["consent"] == CookieConsent.REJECT_ALL.dict()

uc = (await get_user_consent(existing.uid, db_session)).first()
assert uc.consent == CookieConsent.REJECT_ALL.dict()
Expand Down
10 changes: 6 additions & 4 deletions consent_api/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

from sqlalchemy.dialects.postgresql import insert

from consent_api import config
from consent_api.config import Development
from consent_api.config import Testing
from consent_api.config import settings
from consent_api.database import db_context
from consent_api.models import Origin

Expand All @@ -19,9 +21,9 @@ def generate_uid():


async def register_origins_for_e2e_testing():
if config.ENV in [
config.Environment.DEVELOPMENT.value,
config.Environment.TESTING.value,
if settings.__class__ in [
Development,
Testing,
]:
test_origins = [
"http://consent-api",
Expand Down
13 changes: 13 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ services:
volumes:
- "./client:/home/app/client"
- "./consent_api:/home/app/consent_api"
entrypoint: ["make", "migrate-and-run"]
environment:
DATABASE_URL: postgresql+asyncpg://postgres:postgres@db:5432/postgres
PORT: "80"
Expand All @@ -48,12 +49,18 @@ services:
image: "${DOCKER_IMAGE}"
ports:
- "8001:80"
depends_on:
db:
condition: service_healthy
consent-api:
condition: service_started
volumes:
- "./client:/home/app/client"
- "./Makefile:/home/app/Makefile"
- "./consent_api:/home/app/consent_api"
entrypoint: ["make", "run"]
environment:
DATABASE_URL: postgresql+asyncpg://postgres:postgres@db:5432/postgres
PORT: "80"
CONSENT_API_ORIGIN: "http://localhost:8000"
OTHER_SERVICE_ORIGIN_DOCKER: "http://dummy-service-2"
Expand All @@ -67,11 +74,17 @@ services:
image: "${DOCKER_IMAGE}"
ports:
- "8002:80"
depends_on:
db:
condition: service_healthy
consent-api:
condition: service_started
volumes:
- "./client:/home/app/client"
- "./consent_api:/home/app/consent_api"
entrypoint: ["make", "run"]
environment:
DATABASE_URL: postgresql+asyncpg://postgres:postgres@db:5432/postgres
CONSENT_API_ORIGIN: "http://localhost:8000"
PORT: "80"
OTHER_SERVICE_ORIGIN_DOCKER: "http://dummy-service-1"
Expand Down
8 changes: 5 additions & 3 deletions migrations/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from alembic import context
from consent_api import models # noqa
from consent_api.config import SQLALCHEMY_DATABASE_URI
from consent_api.config import settings
from sqlalchemy import pool
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config
Expand All @@ -21,7 +21,9 @@
# set_main_option value is passed to ConfigParser.set, which supports variable
# interpolation using pyformat (eg: '%(some_value)s'). Raw percent signs must be
# escaped (eg: '%%')
config.set_main_option("sqlalchemy.url", SQLALCHEMY_DATABASE_URI.replace("%", "%%"))
config.set_main_option(
"sqlalchemy.url", str(settings.sqlalchemy_database_uri).replace("%", "%%")
)

# add your model's MetaData object here
# for 'autogenerate' support
Expand All @@ -48,7 +50,7 @@ def run_migrations_offline() -> None:

"""
context.configure(
url=SQLALCHEMY_DATABASE_URI,
url=str(settings.sqlalchemy_database_uri),
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
Expand Down
Loading
Loading