def get_snacks() -> dict: """ combines snacks from snack API with vote numbers. Sorts into map of three lists: - permanent: snacks that are always ordered - suggestedCurrent: snacks that are suggested for the current month - suggestedExpired: snacks whose suggestions have expired Raises: - ApiNotAvailableException if snacks API is down - AuthorizationError if request to snacks API fails """ snax: dict = get_snacks_remote() snacks_permanent: list = [] snacks_suggested_valid: list = [] snacks_suggested_expired: list = [] final_snacks = { "permanent": snacks_permanent, "suggestedCurrent": snacks_suggested_valid, "suggestedExpired": snacks_suggested_expired } # collect snack ids to query votes for snack_ids: list = snax.keys() # collect votes session = Session() rows = session.query(Vote.snack_id, func.count(Vote.id))\ .filter(Vote.snack_id.in_(snack_ids))\ .filter(datetime.datetime.now() < Vote.vote_expiry)\ .group_by(Vote.snack_id) session.close() # add votes to snack objects for snack_id, count in rows: snack = snax[snack_id].to_dict() snack["votes"] = count if snack["suggestionExpiry"] > datetime.datetime.now(): snacks_suggested_valid.append(snack) else: snacks_suggested_expired.append(snack) # remove from dictionary so it isn't double added del snax[snack_id] # add remaining snax (no votes) to final for k, v in snax.items(): if v.optional: now = datetime.datetime.now() if v.suggestionExpiry and v.suggestionExpiry > now: snacks_suggested_valid.append(v.to_dict()) else: snacks_suggested_expired.append(v.to_dict()) else: snacks_permanent.append(v.to_dict()) return final_snacks
def test_create_user(): create_user('test_user', 'test_password') session = Session() test_user = session.query(User).filter(User.username == 'test_user').first() assert test_user
def add_vote(user_id: int, snack_id: int) -> int: """ Adds a vote for the snack by the user raises: - VotesExceededException if the user has exceeded allotted votes - UserNotFoundException if the user_id is not found returns: - vote_count int """ # count votes for the user that haven't expired user_votes: int = users.get_user_votes(user_id) if user_votes >= max_votes: raise VotesExceededException # create new vote new_vote = Vote(user_id=user_id, snack_id=snack_id, vote_expiry=vote_expiration()) session = Session() session.add(new_vote) session.commit() session.close() return user_votes + 1
def get_user_by_id(user_id: int) -> User: """ Looks up user by id. Raises: - UserNotFoundException if user id does not exist Returns: User """ session = Session() # verify user_id exists vote_user: User = session.query(User).filter(User.id == user_id).first() session.close() if not vote_user: raise UserNotFoundException return vote_user
def get_user_votes(user_id: int) -> int: """ Gets the number of votes for the user in the current time period Raises: - UserNotFoundException if user id does not exist Returns: - int: count of votes """ session = Session() # get user by id to ensure user exists get_user_by_id(user_id) # count votes for the user that haven't expired user_votes: int = session.query(Vote)\ .filter(Vote.user_id == user_id)\ .filter(Vote.vote_expiry > datetime.datetime.now()).count() session.close() return user_votes
def test_vote_expire(): my_user: User = create_user('test_user', 'test_password') vote_count: int = get_user_votes(my_user.id) assert vote_count == 0 # add votes that expired before now session = Session() vote = Vote( user_id = my_user.id, snack_id = 234, vote_expiry = datetime.datetime.now() - datetime.timedelta(days=1) ) # assert that the vote count is still 0 with expired votes vote_count: int = get_user_votes(my_user.id) assert vote_count == 0
def set_user_suggestion(user_id: int): """ sets user suggestion expiry to the end of the alotted period Raises: - UserNotFoundException if user id does not exist """ session = Session() user = get_user_by_id(user_id) user.suggestion_expiry = properties.vote_expiration() session.merge(user) session.commit() session.close()
def verify_user_password(username: str, password: str) -> bool: """ verifies whether the user password is correct raises: - UserNotFoundException if user does not exist returns: - bool of whether pw is valid """ session = Session() user: User = session.query(User).filter(User.username == username).first() pw_input = password.encode('utf-8') if user and bcrypt.checkpw(pw_input, user.password_hash.encode('utf-8')): session.refresh(user) return user.id session.close() if not user: raise UserNotFoundException return False
def create_user(username: str, password: str) -> User: """ adds user to database :param username: str username :param password: str password :return: User """ session = Session() hash_pw = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') new_user: User = User(username=username, password_hash=hash_pw) # TODO: handle generic sql errors try: session.add(new_user) session.commit() # gets id session.refresh(new_user) session.close() return new_user except IntegrityError as e: session.rollback() session.close() raise UserAlreadyExistsException
def get_snacks_remote() -> dict: """ Fetches snacks from snack service and adds to snack table Returns map of snack_id -> snack Raises: - ApiNotAvailableException if snacks API is down - AuthorizationError if request to snacks API fails """ res: requests.Response = requests.get( properties.snacks_api_url, headers={ "Authorization": "ApiKey {}".format(properties.snacks_api_key) } ) # check for auth errors if res.status_code == 401: raise AuthorizationError( json.dumps({"error": "Unable to authorize to snacks api"})) # check for service being down if res.status_code >= 500: raise ApiNotAvailableException snax: list = res.json() final_snacks: dict = {} for snack in snax: new_snack = Snack(**snack) # add snacks to db. session = Session() new_snack = session.merge(new_snack) session.commit() session.refresh(new_snack) session.close() final_snacks[new_snack.id] = new_snack session.close() return final_snacks
def add_snack(name: str, location: str) -> Snack: """ Sends in a snack suggestion, to snack service Adds to snacks table if successful. returns the Snack object. Note: - create snack api supports latitude and longitude parameters but they aren't anywhere in the output so I'm not including the input Raises: - AuthorizationError if request to snacks API fails - BadRequestError if the snack already exists """ res: requests.Response = requests.post( properties.snacks_api_url, headers={ "Authorization": "ApiKey {}".format(properties.snacks_api_key), "Content-Type": "application/json" }, data=json.dumps({ "name": name, "location": location }) ) # check for auth errors if res.status_code == 401: raise AuthorizationError( {"error": "Unable to authorize to snacks api"} ) # check for conflicts elif res.status_code == 401: raise BadRequestError(res.json()) # check for service being down elif res.status_code >= 500: raise ApiNotAvailableException # 409 is returned when snack already exists. # However, I still want to update # the suggestionExpiry if its null or not the current month elif res.status_code == 409: session = Session() currentSnack = session.query( Snack ).filter( Snack.name == name ).filter(Snack.suggestionExpiry > datetime.datetime.now()).first() # snack exists in db with a valid suggestion month, it has already been # suggested if currentSnack: raise BadRequestError(res.json()) # snack exists in snack api but does not have a valid suggestion month # in db. Add suggestion month to db if optional snack. Otherwise raise # error else: extant_snack = get_snack_remote_by_name(name) if extant_snack.optional: extant_snack.suggestionExpiry = get_next_month() session.merge(extant_snack) session.commit() return extant_snack else: raise BadRequestError( "Cannot suggest a snack that is always purchased") else: # add to db session = Session() new_snack = Snack(**res.json()) new_snack.suggestionExpiry = get_next_month() session.merge(new_snack) session.commit() session.close() return new_snack