♻️ Use of pydantic to manage config+env variables

FastAPI release has been updated allowing to use lifespan parameter
to prepare/shutdown sub components.
This commit is contained in:
2023-05-10 22:30:30 +02:00
parent ef26509b87
commit b894d68a7a
5 changed files with 102 additions and 62 deletions

View File

@@ -13,7 +13,7 @@ from sqlalchemy.ext.asyncio import (
)
from .base_class import Base
from ..settings import DatabaseSettings
logger = getLogger(__name__)
@@ -33,9 +33,17 @@ class Database:
return None
# TODO: Preserve UserLastStopSearchResults table from drop.
async def connect(self, db_path: str, clear_static_data: bool = False) -> bool:
async def connect(
self, settings: DatabaseSettings, clear_static_data: bool = False
) -> bool:
password = settings.password
path = (
f"{settings.driver}://{settings.user}:"
f"{password.get_secret_value() if password is not None else ''}"
f"@{settings.host}:{settings.port}/{settings.name}"
)
self._async_engine = create_async_engine(
db_path, pool_pre_ping=True, pool_size=10, max_overflow=20
path, pool_pre_ping=True, pool_size=10, max_overflow=20
)
if self._async_engine is not None:

View File

@@ -0,0 +1,26 @@
from pydantic import BaseModel, BaseSettings, Field, SecretStr
class HttpSettings(BaseModel):
host: str = "127.0.0.1"
port: int = 8080
cert: str | None = None
class DatabaseSettings(BaseModel):
name: str = "carrramba-encore-rate"
host: str = "127.0.0.1"
port: int = 5432
driver: str = "postgresql+psycopg"
user: str = "cer"
password: SecretStr | None = None
class Settings(BaseSettings):
app_name: str
idfm_api_key: SecretStr = Field(..., env="API_KEY")
clear_static_data: bool = Field(..., env="CLEAR_STATIC_DATA")
http: HttpSettings = HttpSettings()
db: DatabaseSettings = DatabaseSettings()

View File

@@ -0,0 +1,14 @@
app_name: carrramba-encore-rate
http:
host: 0.0.0.0
port: 4443
cert: ./config/cert.pem
db:
name: carrramba-encore-rate
host: 127.0.0.1
port: 5432
driver: postgresql+psycopg
user: cer
password: cer_password

View File

@@ -6,21 +6,18 @@ from os import environ, EX_USAGE
from typing import Sequence
import uvicorn
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.sdk.resources import Resource, SERVICE_NAME
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.logging import LoggingInstrumentor
from opentelemetry.sdk.resources import Resource as OtResource
from opentelemetry.sdk.trace import TracerProvider as OtTracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from rich import print
from starlette.types import ASGIApp
from yaml import safe_load
from backend.db import db
from backend.idfm_interface import Destinations as IdfmDestinations, IdfmInterface
@@ -34,65 +31,54 @@ from backend.schemas import (
StopArea as StopAreaSchema,
StopShape as StopShapeSchema,
)
API_KEY = environ.get("API_KEY")
if API_KEY is None:
print('No "API_KEY" environment variable set... abort.')
exit(EX_USAGE)
APP_NAME = environ.get("APP_NAME", "app")
MODE = environ.get("MODE", "grpc")
COLLECTOR_ENDPOINT_GRPC_ENDPOINT = environ.get(
"COLLECTOR_ENDPOINT_GRPC_ENDPOINT", "127.0.0.1:14250" # "jaeger-collector:14250"
)
# CREATE DATABASE "carrramba-encore-rate";
# CREATE USER cer WITH ENCRYPTED PASSWORD 'cer_password';
# GRANT ALL PRIVILEGES ON DATABASE "carrramba-encore-rate" TO cer;
# \c "carrramba-encore-rate";
# GRANT ALL ON schema public TO cer;
# CREATE EXTENSION IF NOT EXISTS pg_trgm;
# TODO: Remove postgresql+psycopg from environ variable
DB_PATH = "postgresql+psycopg://cer:cer_password@127.0.0.1:5432/carrramba-encore-rate"
from backend.settings import Settings
app = FastAPI()
CONFIG_PATH = environ.get("CONFIG_PATH", "./config.sample.yaml")
def load_settings(path: str) -> Settings:
with open(path, "r") as config_file:
config = safe_load(config_file)
return Settings(**config)
settings = load_settings(CONFIG_PATH)
@asynccontextmanager
async def lifespan(app: FastAPI):
await db.connect(settings.db, settings.clear_static_data)
if settings.clear_static_data:
await idfm_interface.startup()
yield
await db.disconnect()
app = FastAPI(lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://localhost:4443",
"https://localhost:3000",
],
allow_origins=["https://localhost:4443", "https://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
trace.set_tracer_provider(TracerProvider())
app.mount("/widget", StaticFiles(directory="../frontend/", html=True), name="widget")
FastAPIInstrumentor.instrument_app(app)
trace.set_tracer_provider(
TracerProvider(resource=Resource.create({SERVICE_NAME: settings.app_name}))
)
trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
tracer = trace.get_tracer(APP_NAME)
tracer = trace.get_tracer(settings.app_name)
idfm_interface = IdfmInterface(API_KEY, db)
# TODO: Add command line argument to force database reset.
@app.on_event("startup")
async def startup():
await db.connect(DB_PATH, clear_static_data=True)
await idfm_interface.startup()
# await db.connect(DB_PATH, clear_static_data=False)
print("Connected")
@app.on_event("shutdown")
async def shutdown():
await db.disconnect()
STATIC_ROOT = "../frontend/"
app.mount("/widget", StaticFiles(directory=STATIC_ROOT, html=True), name="widget")
idfm_interface = IdfmInterface(settings.idfm_api_key.get_secret_value(), db)
def optional_datetime_to_ts(dt: datetime | None) -> int | None:
@@ -281,7 +267,11 @@ async def get_stop_shape(stop_id: int) -> StopShapeSchema | None:
raise HTTPException(status_code=404, detail=msg)
FastAPIInstrumentor.instrument_app(app)
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=4443, ssl_certfile="./config/cert.pem")
http_settings = settings.http
uvicorn.run(
app,
host=http_settings.host,
port=http_settings.port,
ssl_certfile=http_settings.cert,
)

View File

@@ -11,7 +11,7 @@ python = "^3.11"
aiohttp = "^3.8.3"
rich = "^12.6.0"
aiofiles = "^22.1.0"
fastapi = "^0.88.0"
fastapi = "^0.95.0"
uvicorn = "^0.20.0"
msgspec = "^0.12.0"
pyshp = "^2.3.1"
@@ -25,6 +25,7 @@ opentelemetry-exporter-otlp-proto-http = "^1.17.0"
opentelemetry-instrumentation-sqlalchemy = "^0.38b0"
sqlalchemy = "^2.0.12"
psycopg = "^3.1.9"
pyyaml = "^6.0"
[build-system]
requires = ["poetry-core"]
@@ -48,6 +49,7 @@ yapf = "^0.32.0"
whatthepatch = "^1.0.4"
mypy = "^1.0.0"
types-sqlalchemy-utils = "^1.0.1"
types-pyyaml = "^6.0.12.9"
[tool.mypy]
plugins = "sqlalchemy.ext.mypy.plugin"