Skip to content

Commit

Permalink
Use Pydantic BaseSettings for config settings.
Browse files Browse the repository at this point in the history
This reflects the improved implementation in fastapi/full-stack-fastapi-template#87
  • Loading branch information
paul121 committed Feb 10, 2020
1 parent 68b8e76 commit ad317cf
Show file tree
Hide file tree
Showing 26 changed files with 249 additions and 223 deletions.
1 change: 0 additions & 1 deletion backend/app/app/api/api_v1/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from fastapi import APIRouter, Depends, Security

from app.core import config
from app.api.api_v1.endpoints import login, users, utils
from app.api.api_v1.endpoints.farms import farms, info, logs, assets, terms, areas
from app.api.utils.security import get_farm_access
Expand Down
4 changes: 2 additions & 2 deletions backend/app/app/api/api_v1/endpoints/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from app import crud
from app.api.utils.db import get_db
from app.api.utils.security import get_current_user
from app.core import config
from app.core.config import settings
from app.core.jwt import create_access_token
from app.core.security import get_password_hash
from app.models.user import User as DBUser
Expand Down Expand Up @@ -40,7 +40,7 @@ def login_access_token(
raise HTTPException(status_code=400, detail="Incorrect email or password")
elif not crud.user.is_active(user):
raise HTTPException(status_code=400, detail="Inactive user")
access_token_expires = timedelta(minutes=config.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
logger.debug(f"New user login with scopes: {form_data.scopes}")
return {
"access_token": create_access_token(
Expand Down
6 changes: 3 additions & 3 deletions backend/app/app/api/api_v1/endpoints/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from app import crud
from app.api.utils.db import get_db
from app.api.utils.security import get_current_active_superuser, get_current_active_user
from app.core import config
from app.core.config import settings
from app.models.user import User as DBUser
from app.schemas.user import User, UserCreate, UserInDB, UserUpdate
from app.utils import send_new_account_email
Expand Down Expand Up @@ -47,7 +47,7 @@ def create_user(
detail="The user with this username already exists in the system.",
)
user = crud.user.create(db, user_in=user_in)
if config.EMAILS_ENABLED and user_in.email:
if settings.EMAILS_ENABLED and user_in.email:
send_new_account_email(
email_to=user_in.email, username=user_in.email, password=user_in.password
)
Expand Down Expand Up @@ -100,7 +100,7 @@ def create_user_open(
"""
Create new user without the need to be logged in.
"""
if not config.USERS_OPEN_REGISTRATION:
if not settings.USERS_OPEN_REGISTRATION:
raise HTTPException(
status_code=403,
detail="Open user resgistration is forbidden on this server",
Expand Down
10 changes: 5 additions & 5 deletions backend/app/app/api/utils/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from app import crud
from app.api.utils.db import get_db
from app.core import config
from app.core.config import settings
from app.core.jwt import ALGORITHM
from app.models.user import User
from app.schemas.token import TokenData, FarmAccess
Expand All @@ -31,12 +31,12 @@
}

optional_oauth2 = OAuth2PasswordBearer(
tokenUrl="/api/v1/login/access-token",
tokenUrl=f"{settings.API_V1_STR}/login/access-token",
scopes=oauth_scopes,
auto_error=False
)
reusable_oauth2 = OAuth2PasswordBearer(
tokenUrl="/api/v1/login/access-token",
tokenUrl=f"{settings.API_V1_STR}/login/access-token",
scopes=oauth_scopes,
auto_error=True
)
Expand Down Expand Up @@ -193,7 +193,7 @@ def get_farm_access_allow_public(
farm_access = None

# If open registration is enabled, allow minimal access.
if config.AGGREGATOR_OPEN_FARM_REGISTRATION is True:
if settings.AGGREGATOR_OPEN_FARM_REGISTRATION is True:
farm_access = FarmAccess(scopes=[], farm_id_list=[], all_farms=False)

# Still check for a request with higher permissions.
Expand All @@ -217,7 +217,7 @@ def get_farm_access_allow_public(


def _validate_token(token):
payload = jwt.decode(token, config.SECRET_KEY, algorithms=[ALGORITHM])
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM])
user_id: int = payload.get("sub", None)
farm_id = payload.get("farm_id", [])
token_scopes = payload.get("scopes", [])
Expand Down
159 changes: 96 additions & 63 deletions backend/app/app/core/config.py
Original file line number Diff line number Diff line change
@@ -1,70 +1,103 @@
import os
import secrets
from typing import List

from pydantic import AnyHttpUrl, BaseSettings, EmailStr, HttpUrl, PostgresDsn, validator
from celery.schedules import crontab

def getenv_boolean(var_name, default_value=False):
result = default_value
env_value = os.getenv(var_name)
if env_value is not None:
result = env_value.upper() in ("TRUE", "1")
return result


API_V1_STR = "/api/v1"

SECRET_KEY = os.getenvb(b"SECRET_KEY")
if not SECRET_KEY:
SECRET_KEY = os.urandom(32)

ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 8 # 60 minutes * 24 hours * 8 days = 8 days

SERVER_NAME = os.getenv("SERVER_NAME")
SERVER_HOST = os.getenv("SERVER_HOST")
BACKEND_CORS_ORIGINS = os.getenv(
"BACKEND_CORS_ORIGINS"
) # a string of origins separated by commas, e.g: "http://localhost, http://localhost:4200, http://localhost:3000, http://localhost:8080, http://local.dockertoolbox.tiangolo.com"
PROJECT_NAME = os.getenv("PROJECT_NAME")
SENTRY_DSN = os.getenv("SENTRY_DSN")

POSTGRES_SERVER = os.getenv("POSTGRES_SERVER")
POSTGRES_USER = os.getenv("POSTGRES_USER")
POSTGRES_PASSWORD = os.getenv("POSTGRES_PASSWORD")
POSTGRES_DB = os.getenv("POSTGRES_DB")
SQLALCHEMY_DATABASE_URI = (
f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_SERVER}/{POSTGRES_DB}"
)

SMTP_TLS = getenv_boolean("SMTP_TLS", True)
SMTP_PORT = None
_SMTP_PORT = os.getenv("SMTP_PORT")
if _SMTP_PORT is not None:
SMTP_PORT = int(_SMTP_PORT)
SMTP_HOST = os.getenv("SMTP_HOST")
SMTP_USER = os.getenv("SMTP_USER")
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD")
EMAILS_FROM_EMAIL = os.getenv("EMAILS_FROM_EMAIL")
EMAILS_FROM_NAME = PROJECT_NAME
EMAIL_RESET_TOKEN_EXPIRE_HOURS = 48
EMAIL_TEMPLATES_DIR = "/app/app/email-templates/build"
EMAILS_ENABLED = SMTP_HOST and SMTP_PORT and EMAILS_FROM_EMAIL

FIRST_SUPERUSER = os.getenv("FIRST_SUPERUSER")
FIRST_SUPERUSER_PASSWORD = os.getenv("FIRST_SUPERUSER_PASSWORD")

USERS_OPEN_REGISTRATION = getenv_boolean("USERS_OPEN_REGISTRATION")
CELERY_WORKER_PING_INTERVAL = crontab(minute='0', hour='0,12')

TEST_FARM_NAME = "farmOS-test-instance"
TEST_FARM_URL = os.getenv("TEST_FARM_URL")
TEST_FARM_USERNAME = os.getenv("TEST_FARM_USERNAME")
TEST_FARM_PASSWORD = os.getenv("TEST_FARM_PASSWORD")
class Settings(BaseSettings):
API_V1_STR: str = "/api/v1"

SECRET_KEY: str = secrets.token_urlsafe(32)

ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 # 60 minutes * 24 hours * 8 days = 8 days

SERVER_NAME: str
SERVER_HOST: AnyHttpUrl
# BACKEND_CORS_ORIGINS is a JSON-formatted list of origins.
# e.g: '["http://localhost", "http://localhost:4200"]'

BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = []

@validator("BACKEND_CORS_ORIGINS", pre=True)
def assemble_cors_origins(cls, v):
if isinstance(v, str):
return [i.strip() for i in v.split(",")]
return v

PROJECT_NAME: str

SENTRY_DSN: HttpUrl = None
@validator("SENTRY_DSN", pre=True)
def sentry_dsn_can_be_blank(cls, v):
if len(v) == 0:
return None
return v

POSTGRES_SERVER: str
POSTGRES_USER: str
POSTGRES_PASSWORD: str
POSTGRES_DB: str
SQLALCHEMY_DATABASE_URI: PostgresDsn = None

@validator("SQLALCHEMY_DATABASE_URI", pre=True)
def assemble_db_connection(cls, v, values):
if isinstance(v, str):
return v
return PostgresDsn.build(
scheme="postgresql",
user=values.get("POSTGRES_USER"),
password=values.get("POSTGRES_PASSWORD"),
host=values.get("POSTGRES_SERVER"),
path=f"/{values.get('POSTGRES_DB') or ''}",
)

SMTP_TLS: bool = True
SMTP_PORT: int = None
SMTP_HOST: str = None
SMTP_USER: str = None
SMTP_PASSWORD: str = None
EMAILS_FROM_EMAIL: EmailStr = None
EMAILS_FROM_NAME: str = None

@validator("EMAILS_FROM_NAME")
def get_project_name(cls, v, values):
if not v:
return values["PROJECT_NAME"]
return v

EMAIL_RESET_TOKEN_EXPIRE_HOURS: int = 48
EMAIL_TEMPLATES_DIR: str = "/app/app/email-templates/build"
EMAILS_ENABLED: bool = False

@validator("EMAILS_ENABLED", pre=True)
def get_emails_enabled(cls, v, values):
return bool(
values.get("SMTP_HOST")
and values.get("SMTP_PORT")
and values.get("EMAILS_FROM_EMAIL")
)

EMAIL_TEST_USER: EmailStr = "[email protected]"

FIRST_SUPERUSER: EmailStr
FIRST_SUPERUSER_PASSWORD: str

USERS_OPEN_REGISTRATION: bool = False

TEST_FARM_NAME: str = "farmOS-test-instance"
TEST_FARM_URL: HttpUrl = None
TEST_FARM_USERNAME: str = None
TEST_FARM_PASSWORD: str = None

AGGREGATOR_OPEN_FARM_REGISTRATION: bool = False
AGGREGATOR_INVITE_FARM_REGISTRATION: bool = False
FARM_ACTIVE_AFTER_REGISTRATION: bool = False

class Config:
case_sensitive = True


def has_valid_test_configuration():
"""Check if sufficient info is provided to run integration tests with a farmOS server."""
return TEST_FARM_URL is not None and TEST_FARM_USERNAME is not None and TEST_FARM_PASSWORD is not None

CELERY_WORKER_PING_INTERVAL = crontab(minute='0', hour='0,12')

AGGREGATOR_OPEN_FARM_REGISTRATION = getenv_boolean("AGGREGATOR_OPEN_FARM_REGISTRATION")
AGGREGATOR_INVITE_FARM_REGISTRATION = getenv_boolean("AGGREGATOR_INVITE_FARM_REGISTRATION")
FARM_ACTIVE_AFTER_REGISTRATION = getenv_boolean("FARM_ACTIVE_AFTER_REGISTRATION")
settings = Settings()
8 changes: 4 additions & 4 deletions backend/app/app/core/jwt.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import jwt

from app.core import config
from app.core.config import settings

ALGORITHM = "HS256"

Expand All @@ -15,12 +15,12 @@ def create_access_token(*, data: dict, expires_delta: timedelta = None):
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, config.SECRET_KEY, algorithm=ALGORITHM)
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt


def create_farm_api_token(farm_id: List[int], scopes: List[str]):
delta = timedelta(hours=config.EMAIL_RESET_TOKEN_EXPIRE_HOURS)
delta = timedelta(hours=settings.EMAIL_RESET_TOKEN_EXPIRE_HOURS)
now = datetime.utcnow()
expires = now + delta
encoded_jwt = jwt.encode(
Expand All @@ -30,7 +30,7 @@ def create_farm_api_token(farm_id: List[int], scopes: List[str]):
"farm_id": farm_id,
"scopes": scopes,
},
config.SECRET_KEY,
settings.SECRET_KEY,
algorithm=ALGORITHM,
)
return encoded_jwt
4 changes: 2 additions & 2 deletions backend/app/app/crud/farm.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from sqlalchemy.orm import Session

from app import crud
from app.core import config
from app.core.config import settings
from app.models.farm import Farm
from app.schemas.farm import FarmCreate, FarmUpdate
from app.models.farm_token import FarmToken
Expand Down Expand Up @@ -55,7 +55,7 @@ def create(db_session: Session, *, farm_in: FarmCreate) -> Farm:
logging.debug(f"New farm provided 'active = {farm_in.active}'")
active = farm_in.active
# Enable farm profile if configured and not overridden above.
elif config.FARM_ACTIVE_AFTER_REGISTRATION:
elif settings.FARM_ACTIVE_AFTER_REGISTRATION:
logging.debug(f"FARM_ACTIVE_AFTER_REGISTRATION is enabled. New farm will be active.")
active = True

Expand Down
8 changes: 4 additions & 4 deletions backend/app/app/db/init_db.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from app import crud
from app.core import config
from app.core.config import settings
from app.schemas.user import UserCreate

# make sure all SQL Alchemy schemas are imported before initializing DB
Expand All @@ -13,11 +13,11 @@ def init_db(db_session):
# the tables un-commenting the next line
# Base.metadata.create_all(bind=engine)

user = crud.user.get_by_email(db_session, email=config.FIRST_SUPERUSER)
user = crud.user.get_by_email(db_session, email=settings.FIRST_SUPERUSER)
if not user:
user_in = UserCreate(
email=config.FIRST_SUPERUSER,
password=config.FIRST_SUPERUSER_PASSWORD,
email=settings.FIRST_SUPERUSER,
password=settings.FIRST_SUPERUSER_PASSWORD,
is_superuser=True,
)
user = crud.user.create(db_session, user_in=user_in)
4 changes: 2 additions & 2 deletions backend/app/app/db/session.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker

from app.core import config
from app.core.config import settings

engine = create_engine(config.SQLALCHEMY_DATABASE_URI, pool_pre_ping=True)
engine = create_engine(settings.SQLALCHEMY_DATABASE_URI, pool_pre_ping=True)
db_session = scoped_session(
sessionmaker(autocommit=False, autoflush=False, bind=engine)
)
Expand Down
17 changes: 5 additions & 12 deletions backend/app/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,25 @@
from starlette.requests import Request

from app.api.api_v1.api import api_router
from app.core import config
from app.core.config import settings
from app.db.session import Session

# Configure logging. Change INFO to DEBUG for development logging.
logging.basicConfig(level=logging.INFO)

app = FastAPI(title=config.PROJECT_NAME, openapi_url="/api/v1/openapi.json")

# CORS
origins = []
app = FastAPI(title=settings.PROJECT_NAME, openapi_url=f"{settings.API_V1_STR}/openapi.json")

# Set all CORS enabled origins
if config.BACKEND_CORS_ORIGINS:
origins_raw = config.BACKEND_CORS_ORIGINS.split(",")
for origin in origins_raw:
use_origin = origin.strip()
origins.append(use_origin)
if settings.BACKEND_CORS_ORIGINS:
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_origins=[str(origin for origin in settings.BACKEND_CORS_ORIGINS)],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
),

app.include_router(api_router, prefix=config.API_V1_STR)
app.include_router(api_router, prefix=settings.API_V1_STR)


@app.middleware("http")
Expand Down
Loading

0 comments on commit ad317cf

Please sign in to comment.