Skip to content

Commit

Permalink
Finished Documenting Util Files.
Browse files Browse the repository at this point in the history
Finished Code docstrings
  • Loading branch information
ribsthakkar committed Aug 18, 2020
1 parent 5b2a530 commit 5956bf3
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 32 deletions.
4 changes: 2 additions & 2 deletions avicena/models/RevenueRate.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from sqlalchemy import Column, Integer, String, Float
from sqlalchemy.orm import Session

from avicena.util.Exceptions import InvalidRevenueRateMilageException
from avicena.util.Exceptions import InvalidRevenueRateMileageException
from . import Base


Expand Down Expand Up @@ -46,7 +46,7 @@ def calculate_revenue(self, miles: float) -> float:
:return: revenue made for trip with given distance
"""
if self.lower_mileage_bound <= miles <= self.upper_mileage_bound:
raise InvalidRevenueRateMilageException(
raise InvalidRevenueRateMileageException(
f"{miles} miles not within RevenueRate bounds [{self.lower_mileage_bound},{self.upper_mileage_bound}]")
return self.base_rate + self.revenue_per_mile * miles

Expand Down
12 changes: 12 additions & 0 deletions avicena/parsers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ All parser modules must implement to the following function header:
Dict[str:MergeAddress], revenue_table: Dict[str:List[RevenueRate]],
output_directory: str) -> Pandas.DataFrame`

The resulting Pandas DataFrame must have the following columns:

`{'trip_id', 'trip_pickup_address', 'trip_pickup_time', 'trip_pickup_lat', 'trip_pickup_lon',
'trip_dropoff_address', 'trip_dropoff_time', 'trip_dropoff_lat', 'trip_dropoff_lon', 'trip_los',
'trip_miles', 'merge_flag', 'trip_revenue'}`

Every patient must have the same `trip_id` except for the final
character. The final character of the string must indicate the relative
order in which the trips must be completed. The final character of the
`trip_id` must be either 'A' , 'B' , or 'C'


## CSV
The CSV Parser takes a CSV of input trips with the following header:
`date,trip_id,customer_name,trip_pickup_time,trip_pickup_name,trip_pickup_address,trip_dropoff_time,trip_dropoff_name,trip_dropoff_address,trip_los,trip_miles`
Expand Down
16 changes: 14 additions & 2 deletions avicena/util/ConfigValidation.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
from typing import Any, Dict

from avicena.util.Exceptions import InvalidConfigException


def _validate_db_details(db_config):
def _validate_db_details(db_config: Dict[str, Any]) -> bool:
"""
Validate the database details in the app_config dictionary that has been loaded
:param db_config: the child dictionary with database specific details
"""
required_types = {'enabled': bool, 'url': str}
for field in required_types:
if field not in db_config:
raise InvalidConfigException(f"app_config.database missing required field {field}")
if type(db_config[field]) != required_types[field]:
raise InvalidConfigException(f"app_config.database.{field} is expected to be {required_types[field]}, found {type(db_config[field])} instead")
return db_config['enabled']


def validate_app_config(loaded_config):
def validate_app_config(loaded_config: Dict[str, Any]) -> None:
"""
Validate the overall app_config.yaml file.
This validation raises Exceptions for name mismatches and type mismatches in the configuration file
:param loaded_config: A dictionary loaded from the app_config.yaml
"""
required_types = {'database':dict, 'geocoder_key':str, 'trips_parser':str, 'optimizer':str, 'seed':int}
for field in required_types:
if field not in loaded_config:
Expand Down
35 changes: 30 additions & 5 deletions avicena/util/Database.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,51 @@
from typing import Dict, Any

from sqlalchemy.orm import Session
from sqlalchemy import create_engine


def create_db_session(db_config):
def create_db_session(db_config: Dict[str, Any]) -> Session:
"""
Create a SQLAlchemy Database Connection session from to the database using the database configuration
:param db_config: database specific section of the app_config.yaml
:return: SQLAlchmey database connection
"""
engine = create_engine(db_config['url'])
session = Session(engine)
return session


def save_to_db_session(session, item):
def save_to_db_session(session: Session, item: Any) -> None:
"""
Stage an object to the session
:param session: Database Connection Session
:param item: Object to be added to session updates
"""
session.add(item)


def commit_db_session(session):
def commit_db_session(session: Session) -> None:
"""
Commit session updates to database
:param session: Database Connection Session
"""
session.commit()


def close_db_session(session):
def close_db_session(session: Session) -> None:
"""
Close database connection session
:param session: Database Connection Session
"""
session.close()


def save_and_commit_to_db(session, item):
def save_and_commit_to_db(session: Session, item: Any):
"""
Stage and commit and object to a database in one action
:param session: Database connection session
:param item: Object to add to database
"""
save_to_db_session(session, item)
commit_db_session(session)
return item
24 changes: 23 additions & 1 deletion avicena/util/Exceptions.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,48 @@
class InvalidTripException(Exception):
"""
Raised if trip is trivially infeasible when constructed
"""
pass


class InvalidConfigException(Exception):
"""
Raised when there is an issue with the configuration
"""
pass


class RevenueCalculationException(Exception):
"""
Raised when Revenue Table is missing the level of service for a trip
"""
pass


class SolutionNotFoundException(Exception):
"""
Raised when the Optimizer could not find a solution after N attempts
"""
pass


class UnknownDriverException(Exception):
"""
Raised when a driver ID was passed in that was not part of the original CSV or Database set of drivers
"""
pass


class MissingTripDetailsException(Exception):
"""
Raised when the Trip Dataframe is missing necessary details
"""
pass


class InvalidRevenueRateMilageException(Exception):
class InvalidRevenueRateMileageException(Exception):
"""
Raised with there is an issue with the revenue calculation. (i.e. there is not mileage window for the input number
of miles)
"""
pass
19 changes: 17 additions & 2 deletions avicena/util/Geolocator.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import os
from typing import Optional

from opencage.geocoder import OpenCageGeocode

locations = {}

def find_coord_lat_lon(addr, key=None):

def find_coord_lat_lon(addr: str, key: Optional[str] = None) -> (float, float):
"""
Find the latitude and longitude for an address
:param addr: Address to geocode
:param key: optional string for geocoder key
:return: Latitude, Longitude of address
"""
if key is None:
key = os.environ.get("GEOCODER_KEY")
if addr in locations:
Expand All @@ -18,5 +26,12 @@ def find_coord_lat_lon(addr, key=None):
except IndexError:
print("Couldn't find coordinates for ", addr)

def find_coord_lon_lat(addr, key=None):

def find_coord_lon_lat(addr: str, key: Optional[str] = None) -> (float, float):
"""
Find the longitude and latitude for an address
:param addr: Address to geocode
:param key: optional string for geocoder key
:return: Longitude, Latitude of address
"""
return tuple(reversed(find_coord_lat_lon(addr, key)))
20 changes: 10 additions & 10 deletions avicena/util/ParserUtil.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def _revenue_calculation(table: Dict[str, List[RevenueRate]], miles: float, los:

def _get_trip_coordinates(df: DataFrame) -> None:
"""
Populate dataframe with coordinates of pickup and dropoff addresses
Populate DataFrame with coordinates of pickup and dropoff addresses
:param df: Dataframe to update
"""
df[['trip_pickup_lat', 'trip_pickup_lon']] = df['trip_pickup_address'].apply(
Expand All @@ -91,7 +91,7 @@ def _get_trip_coordinates(df: DataFrame) -> None:
def _compute_trip_revenues(df: DataFrame, revenue_table: Dict[str, List[RevenueRate]]) -> None:
"""
Calculate revenues for all trips
:param df: dataframe to be updated with trip details
:param df: DataFrame to be updated with trip details
:param revenue_table: dictionary mapping level of service to a list of associated revenue rates
"""
df['trip_revenue'] = df[['trip_miles', 'trip_los']].apply(
Expand All @@ -101,7 +101,7 @@ def _compute_trip_revenues(df: DataFrame, revenue_table: Dict[str, List[RevenueR
def _fill_in_missing_times_and_merge_details(df: DataFrame, merge_details: Dict[str, MergeAddress]) -> None:
"""
Update the missing travel times, correct for merge trip timings, set the merge indication flags
:param df: dataframe to be updated with trip details
:param df: DataFrame to be updated with trip details
:param merge_details: dictionary mapping address substring to actual MergeAddress object
"""
df[['trip_pickup_time', 'trip_dropoff_time', 'merge_flag']] = \
Expand All @@ -112,8 +112,8 @@ def _fill_in_missing_times_and_merge_details(df: DataFrame, merge_details: Dict[

def _standardize_time_format_trip_df(df: DataFrame) -> None:
"""
Convert all the times stored in the dataframe to floats representing fraction of the day
:param df: dataframe with trip_pickup_time and trip_dropoff_time
Convert all the times stored in the DataFrame to floats representing fraction of the day
:param df: DataFrame with trip_pickup_time and trip_dropoff_time
"""
df['trip_pickup_time'] = df['trip_pickup_time'].apply(convert_time)
df['trip_dropoff_time'] = df['trip_dropoff_time'].apply(convert_time)
Expand All @@ -122,8 +122,8 @@ def _standardize_time_format_trip_df(df: DataFrame) -> None:
def standardize_trip_df(df: DataFrame, merge_details: Dict[str, MergeAddress], revenue_table: Dict[str, List[RevenueRate]]) -> None:
"""
Apply time standardization, merge trip updates, missing time updates, revenue calculations, and coordinates to
the trip dataframe
:param df: input trip dataframe to be updated
the trip DataFrame
:param df: input trip DataFrame to be updated
:param merge_details: dictionary mapping address substring to actual MergeAddress object
:param revenue_table: dictionary mapping level of service to a list of associated revenue rates
"""
Expand All @@ -135,9 +135,9 @@ def standardize_trip_df(df: DataFrame, merge_details: Dict[str, MergeAddress], r

def verify_and_save_parsed_trips_df_to_csv(df: DataFrame, path_to_save: str) -> None:
"""
Check that all required columns are in the input dataframe of parsed trips (i.e. it has been standardized) and
Check that all required columns are in the input DataFrame of parsed trips (i.e. it has been standardized) and
save it to the output_directory in a file called 'parsed_trips.csv'
:param df: dataframe that will be verified and saved to CSV
:param df: DataFrame that will be verified and saved to CSV
:param path_to_save: path to save parsed CSV
:return:
"""
Expand All @@ -146,7 +146,7 @@ def verify_and_save_parsed_trips_df_to_csv(df: DataFrame, path_to_save: str) ->
'trip_miles', 'merge_flag', 'trip_revenue'}
for column in required_columns:
if column not in df.columns:
raise MissingTripDetailsException(f"Expected {column} to be in dataframe")
raise MissingTripDetailsException(f"Expected {column} to be in DataFrame")

parsed_df = df[required_columns]
parsed_df.to_csv(path_to_save)
44 changes: 37 additions & 7 deletions avicena/util/TimeWindows.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,53 @@
import datetime
from datetime import datetime, timedelta
from typing import Optional

ONE_MINUTE = 0.00069444444

def get_time_window_by_hours_minutes(hours, minutes):

def get_time_window_by_hours_minutes(hours: int, minutes: int) -> float:
"""
Get a fractional of a day representation given the hours and minutes
:param hours: number of hours
:param minutes: number of minutes
:return: fraction of a day equivalent if the given hours and minutes passed from midnight
"""
return ONE_MINUTE * (60 * hours + minutes)


def fifteen_minutes():
"""
:return: Get a precalculated 15 minute fraction of day equivalent
"""
return get_time_window_by_hours_minutes(0, 15)

def timedelta_to_hhmmss(td):

def timedelta_to_hhmmss(td: timedelta) -> str:
"""
Convert time delta object to a HH:MM:SS string
:param td: input timedelta
:return: HH:MM:SS rounded string of time delta
"""
return str(td).split('.')[0]

def date_to_day_of_week(date):

def date_to_day_of_week(date: Optional[str] = None) -> int:
"""
Convert a date into day of week
:param date: Optional. If not passed in, then current day is used.
:return: Day of the week starting with [0=Monday, 6=Sunday]
"""
if date is None:
day_of_week = datetime.datetime.now().timetuple().tm_wday
day_of_week = datetime.now().timetuple().tm_wday
else:
m, d, y = date.split('-')
day_of_week = datetime.datetime(int(y), int(m), int(d)).timetuple().tm_wday
day_of_week = datetime(int(y), int(m), int(d)).timetuple().tm_wday
return day_of_week

def timedelta_to_fraction_of_day(td):

def timedelta_to_fraction_of_day(td: timedelta) -> float:
"""
Convert timedelta object to a fraction of a day completed representation
:param td: timedelta object
:return: fraction of day passed by timedelta
"""
return td.total_seconds() / (60 * 60 * 24)
22 changes: 19 additions & 3 deletions avicena/util/VisualizationUtil.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
import datetime

from pandas import Series

def generate_html_label_for_addr(trips, addr):
from avicena.models import Driver


def generate_html_label_for_addr(trips: Series, addr: str) -> str:
"""
Generate specialized string label with HTML tags of trips passing by a specific address
:param trips: Trips passing by address
:param addr: Original address of location
:return: HTML Formatted details about trips going through specified address
"""
data = "<br>".join(
"0" * (10 - len(str(t['trip_id']))) + str(t['trip_id']) + " | " + str(
datetime.timedelta(days=float(t['est_pickup_time']))).split('.')[0] +
" | " + str(t['driver_id']) for t in trips
)
return addr + "<br><b>TripID, Time, DriverID </b><br>" + data

def generate_html_label_for_driver_addr(d):
return d.address[:-4] + "<br>Driver " + str(d.id) + " Home"

def generate_html_label_for_driver_addr(d: Driver) -> str:
"""
Generate a driver HTML Label for visualization
:param d: Driver object
:return: HTML formatted driver address
"""
return d.address.get_clean_address() + "<br>Driver " + str(d.id) + " Home"

0 comments on commit 5956bf3

Please sign in to comment.