diff --git a/backend/backend/db/db.py b/backend/backend/db/db.py index fdf02ce..8096676 100644 --- a/backend/backend/db/db.py +++ b/backend/backend/db/db.py @@ -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: diff --git a/backend/backend/settings.py b/backend/backend/settings.py new file mode 100644 index 0000000..481dc2e --- /dev/null +++ b/backend/backend/settings.py @@ -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() diff --git a/backend/config/config.sample.yaml b/backend/config/config.sample.yaml new file mode 100644 index 0000000..54b76e7 --- /dev/null +++ b/backend/config/config.sample.yaml @@ -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 diff --git a/backend/main.py b/backend/main.py index 80d615b..63c4ece 100755 --- a/backend/main.py +++ b/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, + ) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 2c7af37..e6aad44 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -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"