diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..93f040b --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: gunicorn app:app --log-file - \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..eea96f6 --- /dev/null +++ b/app.py @@ -0,0 +1,108 @@ +from flask import Flask, request, json, Response, redirect +from requests import HTTPError +from warnings import warn +from iron_cache import IronCache +from dairy_queen.theatre import Theatre +import requests +from json import loads, dumps + +#double_dip_cache = IronCache(name='double_dips') + +app = Flask(__name__) +@app.route('/') +def route_to_apiary(): + apiary_io = 'http://docs.dairyqueen1.apiary.io/' + return (redirect(apiary_io, code=302)) + +def hello_world(): + try: + cache_key = create_cache_key(near, days_from_now) + double_dips = json.loads(double_dip_cache.get(key=cache_key).value) + warn('Fetched results from cache with key: ' + cache_key) + except ValueError as ve: + warn(str(ve)) + showtimes = json.dumps({'error': '`date` must be a base-10 integer'}) + status = 400 + except HTTPError as e: + warn(str(e)) + return('hello world') + +@app.route('/double-dips', methods=['GET']) +def get_doubledips(): + location = request.args.get('location') + days_from_now = request.args.get('days_from_now') + max_waiting_time = request.args.get('max_wait_mins') + max_overlap_time = request.args.get('max_overlap_mins') + + status = None + msg = None + mimetype = 'application/json' + + if location is None or not isinstance(location, str): + status = 400 + msg = "'location' is mandatory and must be a string." + + if days_from_now is not None: + try: + days_from_now = int(days_from_now) + except Exception: + status = 400 + msg = "'days_from_now' must be a base-10 integer." + resp = Response(dumps({'msg': msg}), status=status, mimetype=mimetype) + return resp + else: + days_from_now = 0 + + if max_waiting_time is not None: + try: + max_waiting_time = int(max_waiting_time) + except Exception: + status = 400 + msg = "'max_waiting_time' must be a base-10 integer" + resp = Response(dumps({'msg': msg}), status=status, mimetype=mimetype) + return resp + else: + max_waiting_time = 45 + + if max_overlap_time is not None: + try: + max_overlap_time = int(max_overlap_time) + except Exception: + status = 400 + msg = "'max_overlap_time' must be a base-10 integer" + resp = Response(dumps({'msg': msg}), status=status, mimetype=mimetype) + return resp + else: + max_overlap_time = 5 + + + gms_url = 'http://google-movies-scraper.herokuapp.com/movies' + gms_params = { + 'near': location, + 'date': days_from_now + } + + # should definitely build some logic to handle response code of r... + r = requests.get(gms_url, params=gms_params) + theatres_json = loads(r.text) + output = [] + for theatre in theatres_json: + try: + tmp_theatre = Theatre(name=theatre.get('name'), + program=theatre.get('program'), + address=theatre.get('address')) + + tmp_json = tmp_theatre.to_json(max_waiting_time=max_waiting_time, + max_overlap_time=max_overlap_time) + + output.append(tmp_json) + except TypeError as e: + warn(str(e)) + + status = 200 + resp = Response(dumps(output), status=status, mimetype=mimetype) + return(resp) + + +if (__name__ == '__main__'): + app.run(debug=True, host='0.0.0.0', port=5000) \ No newline at end of file diff --git a/dairy_queen/__init__.py b/dairy_queen/dairy_queen/__init__.py similarity index 100% rename from dairy_queen/__init__.py rename to dairy_queen/dairy_queen/__init__.py diff --git a/dairy_queen/doubledip.py b/dairy_queen/dairy_queen/doubledip.py similarity index 92% rename from dairy_queen/doubledip.py rename to dairy_queen/dairy_queen/doubledip.py index 203db82..a56f34f 100644 --- a/dairy_queen/doubledip.py +++ b/dairy_queen/dairy_queen/doubledip.py @@ -1,5 +1,5 @@ import collections -from movie import Movie +from .movie import Movie # SO- flatten arbirtrary list: # http://stackoverflow.com/questions/2158395/flatten-an-irregular-list-of-lists-in-python#2158532 @@ -63,6 +63,10 @@ def __init__(self, movies): if throw_error: raise TypeError("DoubleDip can only be initialized from a movie or list of movies.") + def to_json(self, showtime_format='%H:%M'): + self.json = [movie.to_json(showtime_format) for movie in self] + return self.json + def __repr__(self): output = "DoubleDip([" + ", ".join([movie.__repr__() for movie in self]) + "])" return(output) diff --git a/dairy_queen/movie.py b/dairy_queen/dairy_queen/movie.py similarity index 71% rename from dairy_queen/movie.py rename to dairy_queen/dairy_queen/movie.py index 7276df7..37222fa 100644 --- a/dairy_queen/movie.py +++ b/dairy_queen/dairy_queen/movie.py @@ -1,4 +1,4 @@ -import datetime +from datetime import datetime, timedelta class Movie: """A Movie is an object that stores information for a particular movie (assumed @@ -20,13 +20,22 @@ def __init__(self, name, runtime, showtime, showtime_format='%H:%M'): if runtime <= 0: raise ValueError('runtime must be a positive integer') - self.start = datetime.datetime.strptime(showtime, showtime_format) + self.start = datetime.strptime(showtime, showtime_format) - self.runtime = datetime.timedelta(minutes=runtime) + self.runtime = timedelta(minutes=runtime) self.name = name self.end = self.start + self.runtime + def to_json(self, showtime_format='%H:%M'): + self.json = { + 'movie': self.name, + 'length': int(self.runtime.total_seconds()/60), + 'startTime': self.start.strftime(showtime_format), + 'endTime': self.end.strftime(showtime_format) + } + return self.json + def __str__(self): output = '%s (%s min): showing from %s to %s' % \ (self.name, str(self.runtime.total_seconds() / 60), self.start, self.end) diff --git a/dairy_queen/showtime.py b/dairy_queen/dairy_queen/showtime.py similarity index 100% rename from dairy_queen/showtime.py rename to dairy_queen/dairy_queen/showtime.py diff --git a/dairy_queen/test.json b/dairy_queen/dairy_queen/test.json similarity index 100% rename from dairy_queen/test.json rename to dairy_queen/dairy_queen/test.json diff --git a/dairy_queen/tests/example_theatre.json b/dairy_queen/dairy_queen/tests/example_theatre.json similarity index 100% rename from dairy_queen/tests/example_theatre.json rename to dairy_queen/dairy_queen/tests/example_theatre.json diff --git a/dairy_queen/dairy_queen/tests/test_doubledip.py b/dairy_queen/dairy_queen/tests/test_doubledip.py new file mode 100644 index 0000000..1d85908 --- /dev/null +++ b/dairy_queen/dairy_queen/tests/test_doubledip.py @@ -0,0 +1,27 @@ +from dairy_queen.doubledip import DoubleDip +from dairy_queen.movie import Movie + +class TestDoubleDip: + def test_to_json(self): + + doubledip = DoubleDip([ + Movie(name='a', runtime=60, showtime='12:45'), + Movie(name='b', runtime=120, showtime='14:00') + ]) + + json_output = [ + { + 'movie': 'a', + 'length': 60, + 'startTime': '12:45', + 'endTime': '13:45' + }, + { + 'movie': 'b', + 'length': 120, + 'startTime': '14:00', + 'endTime': '16:00' + } + ] + + assert doubledip.to_json() == json_output \ No newline at end of file diff --git a/dairy_queen/tests/test_flatlist.py b/dairy_queen/dairy_queen/tests/test_flatlist.py similarity index 95% rename from dairy_queen/tests/test_flatlist.py rename to dairy_queen/dairy_queen/tests/test_flatlist.py index 2b8976a..f6ea211 100644 --- a/dairy_queen/tests/test_flatlist.py +++ b/dairy_queen/dairy_queen/tests/test_flatlist.py @@ -1,4 +1,4 @@ -from doubledip import FlatList +from dairy_queen.doubledip import FlatList class TestFlatList: diff --git a/dairy_queen/tests/test_theatre.py b/dairy_queen/dairy_queen/tests/test_theatre.py similarity index 58% rename from dairy_queen/tests/test_theatre.py rename to dairy_queen/dairy_queen/tests/test_theatre.py index c9d4839..97c1502 100644 --- a/dairy_queen/tests/test_theatre.py +++ b/dairy_queen/dairy_queen/tests/test_theatre.py @@ -1,6 +1,6 @@ import pytest # for fixtures -from theatre import Theatre -from doubledip import DoubleDip +from dairy_queen.theatre import Theatre +from dairy_queen.doubledip import DoubleDip from operator import attrgetter # this is for sorting @@ -8,8 +8,7 @@ @pytest.fixture(scope = 'module') def theatre_json(): import json - return json.load(open('tests/example_theatre.json')) - + return json.load(open('dairy_queen/tests/example_theatre.json')) def create_theatre_and_calc_double_dips(theatre_json, max_waiting_mins = 20, max_overlap_mins = 6): theatre = Theatre(name = theatre_json['name'], program = theatre_json['program']) @@ -17,8 +16,8 @@ def create_theatre_and_calc_double_dips(theatre_json, max_waiting_mins = 20, max theatre.program.sort(key = attrgetter('name')) return (theatre) - class TestTheatre: + def test_calculate_double_dip_1(self, theatre_json): # we should expect to see a list of 1 double dip, connecting # movie 'a' to movie 'b' @@ -90,3 +89,61 @@ def test_calculate_double_dip_6(self, theatre_json): ] assert theatre.double_dips == expected_dips + + def test_to_json(self): + theatre_json = { + "name": "Test Theatre 5", + "description": "Test triplet when there's an unacceptable distance", + "program": [ + { + "name": "a", + "runtime": 60, + "showtimes": "16:00" + }, + { + "name": "b", + "runtime": 60, + "showtimes": "17:05" + }, + { + "name": "c", + "runtime": 60, + "showtimes": "19:05" + } + ] + } + + expected_output = { + 'name': theatre_json['name'], + 'address': theatre_json.get('address'), + 'doubleDips': [ + [ + { + 'movie': theatre_json['program'][0]['name'], + 'length': theatre_json['program'][0]['runtime'], + 'startTime': theatre_json['program'][0]['showtimes'], + 'endTime': "17:00" + }, + { + 'movie': theatre_json['program'][1]['name'], + 'length': theatre_json['program'][1]['runtime'], + 'startTime': theatre_json['program'][1]['showtimes'], + 'endTime': "18:05" + } + ], + [ + { + 'movie': theatre_json['program'][2]['name'], + 'length': theatre_json['program'][2]['runtime'], + 'startTime': theatre_json['program'][2]['showtimes'], + 'endTime': "20:05" + } + ] + ] + } + + theatre = Theatre(name=theatre_json.get('name'), + program=theatre_json.get('program'), + address=theatre_json.get('address')) + + assert theatre.to_json() == expected_output diff --git a/dairy_queen/theatre.py b/dairy_queen/dairy_queen/theatre.py similarity index 83% rename from dairy_queen/theatre.py rename to dairy_queen/dairy_queen/theatre.py index 8a71c75..cd285e7 100644 --- a/dairy_queen/theatre.py +++ b/dairy_queen/dairy_queen/theatre.py @@ -1,8 +1,8 @@ import warnings import datetime from operator import attrgetter # this is for sorting -from movie import Movie -from doubledip import DoubleDip +from .movie import Movie +from .doubledip import DoubleDip class Theatre: """ Theatre object @@ -19,14 +19,22 @@ class Theatre: """ double_dips = None + address = None + json = None - def __init__(self, name, program): + def __init__(self, name, program, address=None): if isinstance(name, str): self.name = name else: raise TypeError("'name' must be a string") + if address is not None: + if isinstance(address, str): + self.address = address + else: + raise TypeError("'address' must be a string") + self.program = [] # there's a subtle problem with program is a dictionary (or tuple) @@ -56,8 +64,7 @@ def __init__(self, name, program): showtime=showtime) ) except ValueError: - warnings.warn(movie.get('name') + "could not be coerced to a Movie due to bad runtime...") - + warnings.warn(movie.get('name') + " could not be coerced to a Movie due to bad runtime...") def __str__(self): output = self.name + "\n\nProgram:\n" @@ -68,7 +75,21 @@ def __repr__(self): output = "Theatre(name=%r, program=%r)" % (self.name, self.program) return(output) - def calculate_double_dips(self, max_waiting_time=45, max_overlap_time=30): + def to_json(self, max_waiting_time=45, max_overlap_time=30, showtime_format='%H:%M'): + if self.double_dips is None: + self.calculate_double_dips(max_waiting_time, max_overlap_time) + return self.to_json(max_waiting_time, max_overlap_time) + + self.json = { + 'name': self.name, + 'address': self.address, + 'doubleDips': [double_dip.to_json(showtime_format) for double_dip in self.double_dips] + } + + return self.json + + + def calculate_double_dips(self, max_waiting_time=45, max_overlap_time=5): max_waiting_time = datetime.timedelta(minutes = max_waiting_time) max_overlap_time = datetime.timedelta(minutes = max_overlap_time) diff --git a/dairy_queen/dist/dairy_queen-0.1.0.tar.gz b/dairy_queen/dist/dairy_queen-0.1.0.tar.gz new file mode 100644 index 0000000..85d5d1c Binary files /dev/null and b/dairy_queen/dist/dairy_queen-0.1.0.tar.gz differ diff --git a/dairy_queen/requirements.txt b/dairy_queen/requirements.txt new file mode 100644 index 0000000..f52210f --- /dev/null +++ b/dairy_queen/requirements.txt @@ -0,0 +1,3 @@ +py==1.4.31 +pytest==2.8.5 +wheel==0.29.0 diff --git a/setup.cfg b/dairy_queen/setup.cfg similarity index 100% rename from setup.cfg rename to dairy_queen/setup.cfg diff --git a/setup.py b/dairy_queen/setup.py similarity index 60% rename from setup.py rename to dairy_queen/setup.py index a9db15a..bc5c5c1 100644 --- a/setup.py +++ b/dairy_queen/setup.py @@ -1,13 +1,17 @@ from setuptools import setup setup(name='dairy_queen', - version='1.0.0', + version='0.1.0', description='package to calculate double dips in a given theatre', url='http://github.com/stevenpollack/dairy_queen', author='Steven Pollack', author_email='steven@gnobel.com', - setup_requires = ['pytest-runner'], - tests_require = ['pytest'], + setup_requires=['pytest-runner'], + tests_require=['pytest'], license='MIT', + classifiers=[ + "Programming Language :: Python", + "Programming Language :: Python :: 3" + ], packages=['dairy_queen'], zip_safe=False) diff --git a/requirements.txt b/requirements.txt index f52210f..2ce06e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,14 @@ +-e git+https://github.com/stevenpollack/dairy_queen.git@heroku_app#egg=dairy_queen&subdirectory=dairy_queen +Flask==0.10.1 +gunicorn==19.4.5 +iron-cache==0.3.2 +iron-core==1.2.0 +itsdangerous==0.24 +Jinja2==2.8 +MarkupSafe==0.23 py==1.4.31 pytest==2.8.5 -wheel==0.29.0 +python-dateutil==2.5.0 +requests==2.9.1 +six==1.10.0 +Werkzeug==0.11.4