From 861eac6d464cbe39c295d8b0dda83d7da893cc20 Mon Sep 17 00:00:00 2001 From: Jacob Windsor Date: Wed, 19 Feb 2025 12:39:01 +0100 Subject: [PATCH] More setup --- .gitignore | 2 + .vscode/settings.json | 3 + backend/Makefile => Makefile | 4 +- backend/app/dtos/predictions.py | 27 ----- backend/app/main.py | 2 +- backend/app/providers/db_provider.py | 118 ++++++++++++++++++++ backend/app/repositories/user_repository.py | 0 backend/requirements.txt | 3 +- 8 files changed, 128 insertions(+), 31 deletions(-) create mode 100644 .vscode/settings.json rename backend/Makefile => Makefile (87%) delete mode 100644 backend/app/dtos/predictions.py create mode 100644 backend/app/providers/db_provider.py create mode 100644 backend/app/repositories/user_repository.py diff --git a/.gitignore b/.gitignore index 8afbd3b..d6dc777 100644 --- a/.gitignore +++ b/.gitignore @@ -172,3 +172,5 @@ cython_debug/ # PyPI configuration file .pypirc + +venv \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9dddecc --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.defaultInterpreterPath": ".venv/bin/python", +} diff --git a/backend/Makefile b/Makefile similarity index 87% rename from backend/Makefile rename to Makefile index bff6668..4465bb3 100644 --- a/backend/Makefile +++ b/Makefile @@ -1,14 +1,14 @@ # Variables APP_NAME=app/main.py VENV_DIR=venv -REQ_FILE=requirements.txt +REQ_FILE=backend/requirements.txt # Create virtual environment venv: python3 -m venv $(VENV_DIR) # Install dependencies -install: venv +install: $(VENV_DIR)/bin/pip install -r $(REQ_FILE) # Run the application diff --git a/backend/app/dtos/predictions.py b/backend/app/dtos/predictions.py deleted file mode 100644 index aa8b6f1..0000000 --- a/backend/app/dtos/predictions.py +++ /dev/null @@ -1,27 +0,0 @@ -from fastapi import APIRouter, HTTPException -from app.models.prediction import HousePredictionInput, HousePredictionOutput -from app.services.ml_model import HousePricePredictor - -router = APIRouter() -model = HousePricePredictor() - -@router.get("/predictions/house-price", - response_model=HousePredictionOutput, - summary="Get a house price prediction", - description="Predicts the price of a house based on its features" -) -async def predict_price(input_data: HousePredictionInput) -> HousePredictionOutput: - try: - predicted_price, confidence_score, similar_listings = model.predict( - input_data.square_feet, - input_data.bedrooms, - input_data.bathrooms - ) - - return HousePredictionOutput( - predicted_price=predicted_price, - confidence_score=confidence_score, - similar_listings=similar_listings - ) - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py index d6becef..e017a97 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,6 +1,6 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from app.routers import predictions +from backend.app.dtos import predictions app = FastAPI( title="Housing Price Predictor API", diff --git a/backend/app/providers/db_provider.py b/backend/app/providers/db_provider.py new file mode 100644 index 0000000..5ac3d1c --- /dev/null +++ b/backend/app/providers/db_provider.py @@ -0,0 +1,118 @@ + +import os +import subprocess +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager + +import asyncpg +import pg8000 +import pg8000.dbapi +from google.cloud.sql.connector import Connector, IPTypes, create_async_connector +from sqlalchemy import Engine +from sqlalchemy.ext.asyncio import ( + AsyncConnection, + AsyncEngine, + AsyncSession, + async_sessionmaker, + create_async_engine, +) +from sqlmodel import create_engine + +from ..config import get_settings +from ..db.seed import seed +from ..models.base_db_model import BaseDBModel + + +async def _get_async_engine() -> AsyncEngine: + settings = get_settings() + if settings.app.environment == "development": + engine = create_async_engine( + f"postgresql+asyncpg://{settings.db.username}:{settings.db.password}@localhost/{settings.db.db_name}", + future=True, + ) + else: + connector = await create_async_connector() + + async def getconn() -> asyncpg.Connection: + return await connector.connect_async( + settings.db.connection_name, + "asyncpg", + user=settings.db.username, + password=settings.db.password, + db=settings.db.db_name, + ip_type=IPTypes.PUBLIC, + ) + + engine = create_async_engine("postgresql+asyncpg://", async_creator=getconn, future=True) + return engine + + +async def get_session() -> AsyncGenerator[AsyncSession, None]: + """ + Initialize a database session and return it. + + Use this when interacting via the ORM. + """ + engine = await _get_async_engine() + async_session = async_sessionmaker(bind=engine, expire_on_commit=False) + async with async_session() as session: + yield session + + +@asynccontextmanager +async def get_connection() -> AsyncGenerator[AsyncConnection, None]: + """ + Initialize a database connection and return it. + + Only use this when you need to execute raw SQL queries. + """ + engine = await _get_async_engine() + async with engine.connect() as connection: + yield connection + + await engine.dispose() + + +def _get_engine() -> Engine: + settings = get_settings() + if settings.app.environment == "development": + engine = create_engine( + f"postgresql+pg8000://{settings.db.username}:{settings.db.password}@localhost/{settings.db.db_name}" + ) + else: + connector = Connector() + + def getconn() -> pg8000.dbapi.Connection: + conn: pg8000.dbapi.Connection = connector.connect( + settings.db.connection_name, + "pg8000", + user=settings.db.username, + password=settings.db.password, + db=settings.db.db_name, + ip_type=IPTypes.PUBLIC, + ) + return conn + + engine = create_engine("postgresql+pg8000://", creator=getconn) + return engine + + +def create_db_and_tables(): + # TODO Move this to use asyncpg + engine = _get_engine() + BaseDBModel.metadata.create_all(engine) + + if get_settings().app.environment == "development": + seed(engine) + + +def startup_migrations(): + """Run Alembic migrations""" + print("Running Alembic migrations...") + + api_path = os.path.dirname(os.path.abspath(__file__)) + "/../.." + try: + subprocess.run(["alembic", "upgrade", "head"], check=True, cwd=api_path) + print("Migrations applied successfully!") + except subprocess.CalledProcessError as e: + print(f"Error applying migrations: {e}") diff --git a/backend/app/repositories/user_repository.py b/backend/app/repositories/user_repository.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/requirements.txt b/backend/requirements.txt index 0d5f412..6d4a429 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,4 +1,5 @@ fastapi uvicorn sqlmodel -pydantic \ No newline at end of file +pydantic +sqlalchemy