♻️ 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:
@@ -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:
|
||||
|
26
backend/backend/settings.py
Normal file
26
backend/backend/settings.py
Normal 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()
|
14
backend/config/config.sample.yaml
Normal file
14
backend/config/config.sample.yaml
Normal 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
|
106
backend/main.py
106
backend/main.py
@@ -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,
|
||||
)
|
||||
|
@@ -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"
|
||||
|
Reference in New Issue
Block a user