Skip to content

Commit

Permalink
restructure backend to be standalone for deployment
Browse files Browse the repository at this point in the history
  • Loading branch information
alyssadsouza committed Jan 5, 2024
1 parent 4b59c60 commit ff780fb
Show file tree
Hide file tree
Showing 20 changed files with 299 additions and 226 deletions.
3 changes: 1 addition & 2 deletions .env.schema
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
CHROMEDRIVER_PATH=/opt/homebrew/bin/chromedriver # replace with path to your chromedriver install
DATABASE_URL="postgresql://user:password@postgresserver/db" # replace with your DB credentials
CHROMEDRIVER_PATH=/opt/homebrew/bin/chromedriver # replace with path to your chromedriver install
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,5 @@ python -m main
To start the backend API from the root directory, run:

```bash
python -m uvicorn backend.app:app --reload
python -m uvicorn app:app --reload
```
1 change: 1 addition & 0 deletions backend/.env.schema
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DATABASE_URL="postgresql://user:password@postgresserver/db" # replace with your DB credentials
11 changes: 3 additions & 8 deletions backend/app.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
from fastapi import FastAPI

from backend.database import engine, Base
from backend.routers import predict
from backend.routers import analysis
from backend.db_models import Building, Unit


Base.metadata.drop_all(bind=engine,tables=[Unit.__table__, Building.__table__])
Base.metadata.create_all(engine)
from database import engine, Base
from routers import predict
from routers import analysis

app = FastAPI()

Expand Down
116 changes: 116 additions & 0 deletions backend/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
from enum import Enum

class TableHeaders(Enum):
BUILDING = 'Building'
NEIGHBOURHOOD = 'Neighbourhood'
ADDRESS = 'Address'
CITY = 'City'
LISTING = 'Listing'
BED = 'Bed'
BATH = 'Bath'
SQFT = 'SqFt'
PRICE = 'Price'
UNIT_AMENITIES = 'Unit Amenities'
BUILDING_AMENITIES = 'Building Amenities'
PETS = 'Pets'
LAT = 'Latitude'
LON = 'Longitude'
DATE = 'Date'

class UnitAmenities(Enum):
BALCONY = 'Balcony'
IN_UNIT_LAUNDRY = 'In Unit Laundry'
AIR_CONDITIONING = 'Air Conditioning'
HIGH_CEILINGS = 'High Ceilings'
FURNISHED = 'Furnished'
HARDWOOD_FLOOR = 'Hardwood Floor'

class BuildingAmenities(Enum):
CONTROLLED_ACCESS = 'Controlled Access'
FITNESS_CENTER = 'Fitness Center'
SWIMMING_POOL = 'Swimming Pool'
ROOF_DECK = 'Roof Deck'
STORAGE = 'Storage'
RESIDENTS_LOUNGE = 'Residents Lounge'
OUTDOOR_SPACE = 'Outdoor Space'


UnitAmenitiesDict = {
UnitAmenities.BALCONY.value: 0,
UnitAmenities.IN_UNIT_LAUNDRY.value: 0,
UnitAmenities.AIR_CONDITIONING.value: 0,
UnitAmenities.HIGH_CEILINGS.value: 0,
UnitAmenities.FURNISHED.value: 0,
UnitAmenities.HARDWOOD_FLOOR.value: 0
}

BuildingAmenitiesDict = {
BuildingAmenities.CONTROLLED_ACCESS.value: 0,
BuildingAmenities.FITNESS_CENTER.value: 0,
BuildingAmenities.SWIMMING_POOL.value: 0,
BuildingAmenities.ROOF_DECK.value: 0,
BuildingAmenities.STORAGE.value: 0,
BuildingAmenities.RESIDENTS_LOUNGE.value: 0,
BuildingAmenities.OUTDOOR_SPACE.value: 0
}

locations = [
{
'location': 'Downtown Core',
'bounding_box': {
'southwest': {'lon': '-79.398', 'lat': '43.643'},
'northeast': {'lon': '-79.3762', 'lat': '43.66'}
}
},
{
'location': 'Midtown',
'bounding_box': {
'southwest': {'lon': '-79.4165', 'lat': '43.67'},
'northeast': {'lon': '-79.388', 'lat': '43.7'}
}
},
{
'location': 'West End',
'bounding_box': {
'southwest': {'lon': '-79.449', 'lat': '43.628'},
'northeast': {'lon': '-79.402', 'lat': '43.65'}
}
},
{
'location': 'East End',
'bounding_box': {
'southwest': {'lon': '-79.36', 'lat': '43.65'},
'northeast': {'lon': '-79.315', 'lat': '43.685'}
}
},
{
'location': 'North Toronto',
'bounding_box': {
'southwest': {'lon': '-79.425', 'lat': '43.7'},
'northeast': {'lon': '-79.383', 'lat': '43.73'}
}
},
{
'location': 'University Area',
'bounding_box': {
'southwest': {'lon': '-79.4042', 'lat': '43.6572'},
'northeast': {'lon': '-79.39', 'lat': '43.6675'}
}
},
{
'location': 'Scarborough',
'bounding_box': {
'southwest': {'lon': '-79.21498455469643', 'lat': '43.74522758306715'},
'northeast': {'lon': '-79.17281544530357', 'lat': '43.78977241693285'}
}
},
{
'location': 'Etobicoke',
'bounding_box': {
'southwest': {'lon': '-79.57890339741783', 'lat': '43.6379061704074'},
'northeast': {'lon': '-79.53609660258218', 'lat': '43.682093829592596'}
}
}
]

table_columns = [table_header.value for table_header in TableHeaders]
13 changes: 8 additions & 5 deletions backend/db_models.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from backend.database import Base
from database import Base
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, Float, DateTime
from sqlalchemy.orm import relationship
from datetime import datetime


class Unit(Base):
__tablename__ = "units"
id = Column(Integer, primary_key=True, index=True, autoincrement=True, unique=True)
id = Column(Integer, primary_key=True, index=True,
autoincrement=True, unique=True)
building_id = Column(Integer, ForeignKey("buildings.id"))
bed = Column(Integer, index=True)
bath = Column(Float)
Expand All @@ -21,9 +23,11 @@ class Unit(Base):

building = relationship("Building", back_populates="units")


class Building(Base):
__tablename__ = "buildings"
id = Column(Integer, primary_key=True, index=True, autoincrement=True, unique=True)
id = Column(Integer, primary_key=True, index=True,
autoincrement=True, unique=True)
name = Column(String, index=True)
address = Column(String, index=True)
city = Column(String, index=True)
Expand All @@ -37,6 +41,5 @@ class Building(Base):
storage = Column(Boolean, default=False)
swimming_pool = Column(Boolean, default=False)
pets = Column(Boolean, default=False)

units = relationship("Unit", back_populates="building")

5 changes: 3 additions & 2 deletions backend/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from backend.database import SessionLocal
from database import SessionLocal


def get_db():
db = SessionLocal()
try:
return db
yield db
finally:
db.close()
Binary file added backend/model.joblib
Binary file not shown.
30 changes: 30 additions & 0 deletions backend/pydantic_schemas/Analysis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from pydantic import BaseModel
from datetime import datetime

class AddListing(BaseModel):
building: str
neighbourhood: str
address: str
city: str
listing: str
bed: int
bath: float
sqft: float
price: float
pets: float
latitude: float
longitude: float
date: datetime
controlled_access: bool
fitness_center: bool
outdoor_space: bool
residents_lounge: bool
roof_deck: bool
storage: bool
swimming_pool: bool
air_conditioning: bool
balcony: bool
furnished: bool
hardwood_floor: bool
high_ceilings: bool
in_unit_laundry: bool
5 changes: 4 additions & 1 deletion backend/pydantic_schemas/Building.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from pydantic import BaseModel
from backend.pydantic_schemas.Unit import Unit
from pydantic_schemas.Unit import Unit


class BuildingBase(BaseModel):
name: str
Expand All @@ -16,9 +17,11 @@ class BuildingBase(BaseModel):
storage: bool = False
swimming_pool: bool = False


class BuildingCreate(BuildingBase):
pass


class Building(BuildingBase):
id: int
units: list[Unit]
Expand Down
19 changes: 19 additions & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
annotated-types==0.6.0
anyio==4.2.0
click==8.1.7
fastapi==0.108.0
h11==0.14.0
httptools==0.6.1
idna==3.6
pydantic==2.5.3
pydantic_core==2.14.6
python-dotenv==1.0.0
PyYAML==6.0.1
sniffio==1.3.0
SQLAlchemy==2.0.25
starlette==0.32.0.post1
typing_extensions==4.9.0
uvicorn==0.25.0
uvloop==0.19.0
watchfiles==0.21.0
websockets==12.0
19 changes: 18 additions & 1 deletion backend/routers/analysis.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
from fastapi import APIRouter
from fastapi import APIRouter, Depends, HTTPException
import pandas as pd
from sqlalchemy.orm import Session
from services.create import add_listing_data_to_db
from dependencies import get_db
from pydantic_schemas.Analysis import AddListing

router = APIRouter(
prefix="/analysis",
Expand All @@ -10,3 +15,15 @@
@router.get("")
def get_analysis():
return {"data": {}}


@router.post("", status_code=201)
def add_listings(request: list[AddListing], db: Session = Depends(get_db)):
df = pd.DataFrame([obj.dict() for obj in request])
df.columns = df.columns.str.replace('_', ' ').str.title()
df = df.rename(columns={'Sqft': 'SqFt'})
try:
add_listing_data_to_db(db, df)
return {"message": "Database updated successfully"}
except Exception as e:
raise HTTPException(status_code=500, detail="Database error occurred")
6 changes: 3 additions & 3 deletions backend/routers/predict.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from fastapi import APIRouter
from model.model import get_model
import joblib
import numpy
from backend.pydantic_schemas import Predict
from pydantic_schemas import Predict

router = APIRouter(
prefix="/predict",
Expand All @@ -12,7 +12,7 @@

@router.post("")
def get_prediction(request: Predict.PredictRequestBody):
model = get_model()
model = joblib.load("model.joblib")
input = numpy.array(
[
request.bed,
Expand Down
4 changes: 2 additions & 2 deletions backend/scripts/seed.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from backend.database import engine, Base, SessionLocal
from backend.db_models import Building, Unit
from database import engine, Base, SessionLocal
from db_models import Building, Unit

# Create a session
session = SessionLocal()
Expand Down
27 changes: 14 additions & 13 deletions backend/services/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@
from sqlalchemy.orm import Session

from constants import TableHeaders, UnitAmenities, BuildingAmenities
from backend.db_models import Building, Unit
from backend.dependencies import get_db
from backend.services.search import get_building_units_by_timestamp, get_building_by_lat_lon
from data.data_cleaner import get_cleaned_df
from db_models import Building, Unit
from dependencies import get_db
from services.search import get_building_units_by_timestamp, get_building_by_lat_lon
from services.delete import delete_units_by_timestamp

from .delete import delete_units_by_timestamp


def row_to_building(row, db: Session) -> Building:
Expand Down Expand Up @@ -54,18 +53,22 @@ def add_listing_data_to_db(db: Session, df: pd.DataFrame):
subset=[TableHeaders.LAT.value, TableHeaders.LON.value], keep='first').apply(row_to_building, args=(db,), axis=1).dropna()
create_buildings(db, buildings)
# Now add all Unit objects associated with each building
building_groups = df.groupby([TableHeaders.LAT.value, TableHeaders.LON.value])
building_groups = df.groupby(
[TableHeaders.LAT.value, TableHeaders.LON.value])
for (lat, lon), building_df in building_groups:
building = get_building_by_lat_lon(db, lat=lat, lon=lon)
timestamp = building_df[TableHeaders.DATE.value].iloc[0]
existing_units = get_building_units_by_timestamp(db, building.id, building_df[TableHeaders.DATE.value].iloc[0]).first()
existing_units = get_building_units_by_timestamp(
db, building.id, building_df[TableHeaders.DATE.value].iloc[0]).first()
if existing_units is not None:
print(f"Units for building {building.id} for timestamp {timestamp} already exist")
print(
f"Units for building {building.id} for timestamp {timestamp} already exist")
continue
units = building_df.apply(row_to_unit, args=(building.id,), axis=1)
create_units(db, units)
create_units(db, units, building.id)
except Exception as e:
print(f"An error occurred while adding listing data to the database: {e}")
print(
f"An error occurred while adding listing data to the database, rolling back entries")
# Delete all units for the current date timestamp
timestamp = df[TableHeaders.DATE.value].iloc[0]
delete_units_by_timestamp(db=db, timestamp=timestamp)
Expand All @@ -78,9 +81,7 @@ def create_buildings(db: Session, buildings: list[Building]):
print(f"Created buildings in db")



def create_units(db: Session, units: list[Unit]):
building_id = units[0].building_id
def create_units(db: Session, units: pd.DataFrame, building_id: int):
db.bulk_save_objects(units)
db.commit()
print(f"Created units in db for building with id {building_id}")
4 changes: 3 additions & 1 deletion backend/services/delete.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from sqlalchemy.orm import Session
from datetime import datetime

from backend.db_models import Unit
from db_models import Unit


def delete_units_by_timestamp(db: Session, timestamp: datetime):
try:
print("Deleting units by timestamp", timestamp)
db.query(Unit).filter(Unit.timestamp == timestamp).delete()
db.commit()
except Exception as e:
Expand Down
Loading

0 comments on commit ff780fb

Please sign in to comment.