From 1cb38139cffb32ffb2914dd414b6fe0bb8f639f5 Mon Sep 17 00:00:00 2001 From: Steven Pollack Date: Sun, 13 Mar 2016 22:27:07 +0100 Subject: [PATCH 1/5] moved around files to mimic python package structure --- .../{ => build/lib/dairy_queen}/__init__.py | 0 .../{ => build/lib/dairy_queen}/doubledip.py | 2 +- .../{ => build/lib/dairy_queen}/movie.py | 0 .../{ => build/lib/dairy_queen}/showtime.py | 0 .../{ => build/lib/dairy_queen}/theatre.py | 4 +- dairy_queen/dairy_queen/__init__.py | 0 dairy_queen/dairy_queen/doubledip.py | 76 ++++++++ dairy_queen/dairy_queen/movie.py | 46 +++++ dairy_queen/dairy_queen/showtime.py | 26 +++ dairy_queen/{ => dairy_queen}/test.json | 0 .../tests/example_theatre.json | 0 .../dairy_queen/tests/test_doubledip.py | 27 +++ .../{ => dairy_queen}/tests/test_flatlist.py | 2 +- .../{ => dairy_queen}/tests/test_theatre.py | 67 ++++++- dairy_queen/dairy_queen/theatre.py | 165 ++++++++++++++++++ dairy_queen/dist/dairy_queen-0.1.0.tar.gz | Bin 0 -> 4111 bytes .../requirements.txt | 0 setup.cfg => dairy_queen/setup.cfg | 0 setup.py => dairy_queen/setup.py | 10 +- 19 files changed, 413 insertions(+), 12 deletions(-) rename dairy_queen/{ => build/lib/dairy_queen}/__init__.py (100%) rename dairy_queen/{ => build/lib/dairy_queen}/doubledip.py (98%) rename dairy_queen/{ => build/lib/dairy_queen}/movie.py (100%) rename dairy_queen/{ => build/lib/dairy_queen}/showtime.py (100%) rename dairy_queen/{ => build/lib/dairy_queen}/theatre.py (98%) create mode 100644 dairy_queen/dairy_queen/__init__.py create mode 100644 dairy_queen/dairy_queen/doubledip.py create mode 100644 dairy_queen/dairy_queen/movie.py create mode 100644 dairy_queen/dairy_queen/showtime.py rename dairy_queen/{ => dairy_queen}/test.json (100%) rename dairy_queen/{ => dairy_queen}/tests/example_theatre.json (100%) create mode 100644 dairy_queen/dairy_queen/tests/test_doubledip.py rename dairy_queen/{ => dairy_queen}/tests/test_flatlist.py (95%) rename dairy_queen/{ => dairy_queen}/tests/test_theatre.py (58%) create mode 100644 dairy_queen/dairy_queen/theatre.py create mode 100644 dairy_queen/dist/dairy_queen-0.1.0.tar.gz rename requirements.txt => dairy_queen/requirements.txt (100%) rename setup.cfg => dairy_queen/setup.cfg (100%) rename setup.py => dairy_queen/setup.py (60%) diff --git a/dairy_queen/__init__.py b/dairy_queen/build/lib/dairy_queen/__init__.py similarity index 100% rename from dairy_queen/__init__.py rename to dairy_queen/build/lib/dairy_queen/__init__.py diff --git a/dairy_queen/doubledip.py b/dairy_queen/build/lib/dairy_queen/doubledip.py similarity index 98% rename from dairy_queen/doubledip.py rename to dairy_queen/build/lib/dairy_queen/doubledip.py index 203db82..bbc4866 100644 --- a/dairy_queen/doubledip.py +++ b/dairy_queen/build/lib/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 diff --git a/dairy_queen/movie.py b/dairy_queen/build/lib/dairy_queen/movie.py similarity index 100% rename from dairy_queen/movie.py rename to dairy_queen/build/lib/dairy_queen/movie.py diff --git a/dairy_queen/showtime.py b/dairy_queen/build/lib/dairy_queen/showtime.py similarity index 100% rename from dairy_queen/showtime.py rename to dairy_queen/build/lib/dairy_queen/showtime.py diff --git a/dairy_queen/theatre.py b/dairy_queen/build/lib/dairy_queen/theatre.py similarity index 98% rename from dairy_queen/theatre.py rename to dairy_queen/build/lib/dairy_queen/theatre.py index 8a71c75..b5b19fe 100644 --- a/dairy_queen/theatre.py +++ b/dairy_queen/build/lib/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 diff --git a/dairy_queen/dairy_queen/__init__.py b/dairy_queen/dairy_queen/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dairy_queen/dairy_queen/doubledip.py b/dairy_queen/dairy_queen/doubledip.py new file mode 100644 index 0000000..a56f34f --- /dev/null +++ b/dairy_queen/dairy_queen/doubledip.py @@ -0,0 +1,76 @@ +import collections +from .movie import Movie + +# SO- flatten arbirtrary list: +# http://stackoverflow.com/questions/2158395/flatten-an-irregular-list-of-lists-in-python#2158532 +# this yields a GENERATOR, not a list... +def flatten_list(self): + for el in self: + if isinstance(el, collections.Iterable) and not isinstance(el, (str, bytes, dict)): + for sub in flatten_list(el): + yield sub + else: + yield el + +class FlatList(list): + """ + This list is basically a semi-self-flattening list. + + It flattens iterables upon initialization and supports + the ability to compose append/preprend and flatten. + + Both the append and prepend methods return the modified object + which allow for .-chaining + """ + + def __init__(self, iterable = None): + self.append(iterable) + self.flatten() + + def flatten(self): + flattened_list = flatten_list(self.copy()) + self.clear() + for x in flattened_list: + self.append(x) + return(self) + + def append(self, p_object): + super().append(p_object) + return(self) + + def prepend(self, x): + self.insert(0, x) + return(self) + + def flat_prepend(self, x): + return(self.prepend(x).flatten()) + + def flat_append(self, x): + return(self.append(x).flatten()) + +class DoubleDip(FlatList): + def __init__(self, movies): + throw_error = False + if isinstance(movies, Movie): + self.append(movies) + elif isinstance(movies, list): + for movie in movies: + if not isinstance(movie, Movie): + throw_error = True + break + self.append(movie) + + 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) + + def __str__(self): + output = "Double Dip connecting:\n" + "\n".join([movie.__str__() for movie in self]) + print(output) \ No newline at end of file diff --git a/dairy_queen/dairy_queen/movie.py b/dairy_queen/dairy_queen/movie.py new file mode 100644 index 0000000..37222fa --- /dev/null +++ b/dairy_queen/dairy_queen/movie.py @@ -0,0 +1,46 @@ +from datetime import datetime, timedelta + +class Movie: + """A Movie is an object that stores information for a particular movie (assumed + to be played in a particular theatre). As in the real-world, a movie has "showtimes", + the times when its theatre is screening it, a "name", and a "runtime" (its duration in + minutes). + + Parameters + ---------- + name: a string. + runtime: a non-negative integer. Represents the duration of the movie, in minutes. + showtime: a string to be cast to a datetime object, representing when the movie starts. + + Returns + ------- + Movie: ... + """ + def __init__(self, name, runtime, showtime, showtime_format='%H:%M'): + if runtime <= 0: + raise ValueError('runtime must be a positive integer') + + self.start = datetime.strptime(showtime, showtime_format) + + 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) + return(output) + + def __repr__(self): + output = "Movie(name=%r, runtime=%r, start=%r, end=%r)" % (self.name, self.runtime, self.start, self.end) + return(output) diff --git a/dairy_queen/dairy_queen/showtime.py b/dairy_queen/dairy_queen/showtime.py new file mode 100644 index 0000000..59a5e8f --- /dev/null +++ b/dairy_queen/dairy_queen/showtime.py @@ -0,0 +1,26 @@ +class Showtime(datetime.datetime): + """ + to extend datetime.time we have to override the __new__ function. + Overriding the __init__ just yells at you. + See: http://stackoverflow.com/questions/27430269/python-how-to-extend-datetime-timedelta + """ + + def __new__(cls, time, date=None, time_format='%H:%M', date_format='%Y-%m-%d'): + if not isinstance(time, str): + raise TypeError("'time' must be a string") + + if date is not None: + if not isinstance(date, str): + raise TypeError("'date' must either be of None or string type") + ymd = datetime.datetime.strptime(date, date_format) + year, month, day = ymd.year, ymd.month, ymd.day + else: + ymd = gmtime() + year, month, day = ymd.tm_year, ymd.tm_mon, ymd.tm_mday + + ymd_date = datetime.date(year=year, month=month, day=day) + + showtime = datetime.datetime.strptime(time, time_format) + showtime_time = datetime.time(hour=showtime.hour, minute=showtime.minute) + + return(datetime.datetime.combine(ymd_date, showtime_time)) 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/dairy_queen/theatre.py b/dairy_queen/dairy_queen/theatre.py new file mode 100644 index 0000000..cd285e7 --- /dev/null +++ b/dairy_queen/dairy_queen/theatre.py @@ -0,0 +1,165 @@ +import warnings +import datetime +from operator import attrgetter # this is for sorting +from .movie import Movie +from .doubledip import DoubleDip + +class Theatre: + """ Theatre object + + Parameters + ------------ + name : a string indicating the name of the theatre. + program : an iterable containing entries which must have "name", "runtime", + and "showtimes" as keys that can be retrieved via a get()-method. + + Returns + ------- + Theatre : a Theatre object which has properties "name" (str) and "program" ([Movie]). + """ + + double_dips = None + address = None + json = None + + 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) + # that's not wrapped in [], so we have to check for that + if isinstance(program, (tuple, dict)): + program = [program] + else: + # we wrap in a list (since this is idempotent, we shouldn't + # have issues iterating over it. + program = list(program) + + for movie in program: + # movie['showtimes'] can either be a string or [string] + # so wrap in [] to be safe. + showtimes = movie.get('showtimes') + + if isinstance(showtimes, str): + showtimes = [showtimes] + elif not isinstance(showtimes, list): + raise TypeError("the 'showtimes' properties of a movie must be a string or list or strings") + + for showtime in showtimes: + try: + self.program.append( + Movie(name=movie.get('name'), + runtime=movie.get('runtime'), + showtime=showtime) + ) + except ValueError: + 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" + output += '\n'.join([movie.__str__() for movie in self.program]) + "\n" + return(output) + + def __repr__(self): + output = "Theatre(name=%r, program=%r)" % (self.name, self.program) + return(output) + + 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) + + def filter_program(movie): + output = [] + # filter out all movies that are too "far away" or too "close". + for x in self.program: + if movie.name != x.name and movie.end <= x.start + max_overlap_time and x.start <= movie.end + max_waiting_time: + output.append(x) + return(output) + + def find_all_dips_starting_from(movie): + + # label this movie as visited + if movie not in visited_movies: + visited_movies.append(movie) + else: + return([]) + + eligible_movies = filter_program(movie) + + # if no movies pass the filter, we're done + # return the terminal node as a list so that + # we can prepend future movies to this using `insert`. + if len(eligible_movies) == 0: + return(DoubleDip(movie)) + else: + # otherwise, we recursively branch out and explore + # all the potential paths + all_dips_from_movie = [] + for eligible_movie in eligible_movies: + + if eligible_movie in visited_movies: + break + + double_dips = find_all_dips_starting_from(eligible_movie) + + if isinstance(double_dips, DoubleDip): + double_dips.prepend(movie) + all_dips_from_movie.append(double_dips) + else: + # double dips should be a list + for double_dip in double_dips: + double_dip.flat_prepend(movie) + all_dips_from_movie.append(double_dip) + + return(all_dips_from_movie) + + # sort program in ASC start-time... + self.program.sort(key = attrgetter('start')) + + # create a stack to check if a movie has been visited in the DFS + visited_movies = [] + + self.double_dips = [] + + for movie in self.program: + + # dips can either be a single DoubleDip object, or a [DoubleDips]. + # if the latter, we can just + dips to self.double_dips. + # if the former, +'ing will cast the DoubleDip back down to a movie. + # This is undesirable for printing purposes later (when we want + # to convert to JSON). + dips = find_all_dips_starting_from(movie) + + # if a movie has already been visited, an [] will be returned + if isinstance(dips, DoubleDip): + self.double_dips.append(dips) + else: + self.double_dips += dips + + return(self.double_dips) 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 0000000000000000000000000000000000000000..85d5d1ce4bbd51aa68530e504597f49992cf1ba3 GIT binary patch literal 4111 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 N>3@8oxhVj6008(M72E&- literal 0 HcmV?d00001 diff --git a/requirements.txt b/dairy_queen/requirements.txt similarity index 100% rename from requirements.txt rename to dairy_queen/requirements.txt 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) From b4090432c40e30d1a7f391c610ff78edeaccc0a0 Mon Sep 17 00:00:00 2001 From: Steven Pollack Date: Sun, 13 Mar 2016 22:33:23 +0100 Subject: [PATCH 2/5] created quick and dirty v1 of heroku app - doesn't use caching, and requirements might be wrong --- Procfile | 1 + app.py | 108 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 14 ++++++ 3 files changed, 123 insertions(+) create mode 100644 Procfile create mode 100644 app.py create mode 100644 requirements.txt 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/requirements.txt b/requirements.txt new file mode 100644 index 0000000..087151e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,14 @@ +-e git+git@github.com:stevenpollack/dairy_queen.git@1cb38139cffb32ffb2914dd414b6fe0bb8f639f5#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 +python-dateutil==2.5.0 +requests==2.9.1 +six==1.10.0 +Werkzeug==0.11.4 From 0084d5451ca1e9105a4401c35860ef7675a041e0 Mon Sep 17 00:00:00 2001 From: Steven Pollack Date: Sun, 13 Mar 2016 22:40:02 +0100 Subject: [PATCH 3/5] remove dairy_queen as a pip dependency --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 087151e..9ea97c0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ --e git+git@github.com:stevenpollack/dairy_queen.git@1cb38139cffb32ffb2914dd414b6fe0bb8f639f5#egg=dairy_queen&subdirectory=dairy_queen Flask==0.10.1 gunicorn==19.4.5 iron-cache==0.3.2 From c5f34d6de56ab89573819b3f3041f0ee867e21c0 Mon Sep 17 00:00:00 2001 From: Steven Pollack Date: Sun, 13 Mar 2016 22:50:43 +0100 Subject: [PATCH 4/5] introduce dairy_queen dependency via http and not ssh --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 9ea97c0..2ce06e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +-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 From 7aef90294b3994b7c9e985de757bd957356f0a2d Mon Sep 17 00:00:00 2001 From: Steven Pollack Date: Sun, 13 Mar 2016 23:22:35 +0100 Subject: [PATCH 5/5] removed build artifacts --- dairy_queen/build/lib/dairy_queen/__init__.py | 0 .../build/lib/dairy_queen/doubledip.py | 72 --------- dairy_queen/build/lib/dairy_queen/movie.py | 37 ----- dairy_queen/build/lib/dairy_queen/showtime.py | 26 ---- dairy_queen/build/lib/dairy_queen/theatre.py | 144 ------------------ 5 files changed, 279 deletions(-) delete mode 100644 dairy_queen/build/lib/dairy_queen/__init__.py delete mode 100644 dairy_queen/build/lib/dairy_queen/doubledip.py delete mode 100644 dairy_queen/build/lib/dairy_queen/movie.py delete mode 100644 dairy_queen/build/lib/dairy_queen/showtime.py delete mode 100644 dairy_queen/build/lib/dairy_queen/theatre.py diff --git a/dairy_queen/build/lib/dairy_queen/__init__.py b/dairy_queen/build/lib/dairy_queen/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/dairy_queen/build/lib/dairy_queen/doubledip.py b/dairy_queen/build/lib/dairy_queen/doubledip.py deleted file mode 100644 index bbc4866..0000000 --- a/dairy_queen/build/lib/dairy_queen/doubledip.py +++ /dev/null @@ -1,72 +0,0 @@ -import collections -from .movie import Movie - -# SO- flatten arbirtrary list: -# http://stackoverflow.com/questions/2158395/flatten-an-irregular-list-of-lists-in-python#2158532 -# this yields a GENERATOR, not a list... -def flatten_list(self): - for el in self: - if isinstance(el, collections.Iterable) and not isinstance(el, (str, bytes, dict)): - for sub in flatten_list(el): - yield sub - else: - yield el - -class FlatList(list): - """ - This list is basically a semi-self-flattening list. - - It flattens iterables upon initialization and supports - the ability to compose append/preprend and flatten. - - Both the append and prepend methods return the modified object - which allow for .-chaining - """ - - def __init__(self, iterable = None): - self.append(iterable) - self.flatten() - - def flatten(self): - flattened_list = flatten_list(self.copy()) - self.clear() - for x in flattened_list: - self.append(x) - return(self) - - def append(self, p_object): - super().append(p_object) - return(self) - - def prepend(self, x): - self.insert(0, x) - return(self) - - def flat_prepend(self, x): - return(self.prepend(x).flatten()) - - def flat_append(self, x): - return(self.append(x).flatten()) - -class DoubleDip(FlatList): - def __init__(self, movies): - throw_error = False - if isinstance(movies, Movie): - self.append(movies) - elif isinstance(movies, list): - for movie in movies: - if not isinstance(movie, Movie): - throw_error = True - break - self.append(movie) - - if throw_error: - raise TypeError("DoubleDip can only be initialized from a movie or list of movies.") - - def __repr__(self): - output = "DoubleDip([" + ", ".join([movie.__repr__() for movie in self]) + "])" - return(output) - - def __str__(self): - output = "Double Dip connecting:\n" + "\n".join([movie.__str__() for movie in self]) - print(output) \ No newline at end of file diff --git a/dairy_queen/build/lib/dairy_queen/movie.py b/dairy_queen/build/lib/dairy_queen/movie.py deleted file mode 100644 index 7276df7..0000000 --- a/dairy_queen/build/lib/dairy_queen/movie.py +++ /dev/null @@ -1,37 +0,0 @@ -import datetime - -class Movie: - """A Movie is an object that stores information for a particular movie (assumed - to be played in a particular theatre). As in the real-world, a movie has "showtimes", - the times when its theatre is screening it, a "name", and a "runtime" (its duration in - minutes). - - Parameters - ---------- - name: a string. - runtime: a non-negative integer. Represents the duration of the movie, in minutes. - showtime: a string to be cast to a datetime object, representing when the movie starts. - - Returns - ------- - Movie: ... - """ - 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.runtime = datetime.timedelta(minutes=runtime) - self.name = name - - self.end = self.start + self.runtime - - def __str__(self): - output = '%s (%s min): showing from %s to %s' % \ - (self.name, str(self.runtime.total_seconds() / 60), self.start, self.end) - return(output) - - def __repr__(self): - output = "Movie(name=%r, runtime=%r, start=%r, end=%r)" % (self.name, self.runtime, self.start, self.end) - return(output) diff --git a/dairy_queen/build/lib/dairy_queen/showtime.py b/dairy_queen/build/lib/dairy_queen/showtime.py deleted file mode 100644 index 59a5e8f..0000000 --- a/dairy_queen/build/lib/dairy_queen/showtime.py +++ /dev/null @@ -1,26 +0,0 @@ -class Showtime(datetime.datetime): - """ - to extend datetime.time we have to override the __new__ function. - Overriding the __init__ just yells at you. - See: http://stackoverflow.com/questions/27430269/python-how-to-extend-datetime-timedelta - """ - - def __new__(cls, time, date=None, time_format='%H:%M', date_format='%Y-%m-%d'): - if not isinstance(time, str): - raise TypeError("'time' must be a string") - - if date is not None: - if not isinstance(date, str): - raise TypeError("'date' must either be of None or string type") - ymd = datetime.datetime.strptime(date, date_format) - year, month, day = ymd.year, ymd.month, ymd.day - else: - ymd = gmtime() - year, month, day = ymd.tm_year, ymd.tm_mon, ymd.tm_mday - - ymd_date = datetime.date(year=year, month=month, day=day) - - showtime = datetime.datetime.strptime(time, time_format) - showtime_time = datetime.time(hour=showtime.hour, minute=showtime.minute) - - return(datetime.datetime.combine(ymd_date, showtime_time)) diff --git a/dairy_queen/build/lib/dairy_queen/theatre.py b/dairy_queen/build/lib/dairy_queen/theatre.py deleted file mode 100644 index b5b19fe..0000000 --- a/dairy_queen/build/lib/dairy_queen/theatre.py +++ /dev/null @@ -1,144 +0,0 @@ -import warnings -import datetime -from operator import attrgetter # this is for sorting -from .movie import Movie -from .doubledip import DoubleDip - -class Theatre: - """ Theatre object - - Parameters - ------------ - name : a string indicating the name of the theatre. - program : an iterable containing entries which must have "name", "runtime", - and "showtimes" as keys that can be retrieved via a get()-method. - - Returns - ------- - Theatre : a Theatre object which has properties "name" (str) and "program" ([Movie]). - """ - - double_dips = None - - def __init__(self, name, program): - - if isinstance(name, str): - self.name = name - else: - raise TypeError("'name' must be a string") - - self.program = [] - - # there's a subtle problem with program is a dictionary (or tuple) - # that's not wrapped in [], so we have to check for that - if isinstance(program, (tuple, dict)): - program = [program] - else: - # we wrap in a list (since this is idempotent, we shouldn't - # have issues iterating over it. - program = list(program) - - for movie in program: - # movie['showtimes'] can either be a string or [string] - # so wrap in [] to be safe. - showtimes = movie.get('showtimes') - - if isinstance(showtimes, str): - showtimes = [showtimes] - elif not isinstance(showtimes, list): - raise TypeError("the 'showtimes' properties of a movie must be a string or list or strings") - - for showtime in showtimes: - try: - self.program.append( - Movie(name=movie.get('name'), - runtime=movie.get('runtime'), - showtime=showtime) - ) - except ValueError: - 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" - output += '\n'.join([movie.__str__() for movie in self.program]) + "\n" - return(output) - - 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): - - max_waiting_time = datetime.timedelta(minutes = max_waiting_time) - max_overlap_time = datetime.timedelta(minutes = max_overlap_time) - - def filter_program(movie): - output = [] - # filter out all movies that are too "far away" or too "close". - for x in self.program: - if movie.name != x.name and movie.end <= x.start + max_overlap_time and x.start <= movie.end + max_waiting_time: - output.append(x) - return(output) - - def find_all_dips_starting_from(movie): - - # label this movie as visited - if movie not in visited_movies: - visited_movies.append(movie) - else: - return([]) - - eligible_movies = filter_program(movie) - - # if no movies pass the filter, we're done - # return the terminal node as a list so that - # we can prepend future movies to this using `insert`. - if len(eligible_movies) == 0: - return(DoubleDip(movie)) - else: - # otherwise, we recursively branch out and explore - # all the potential paths - all_dips_from_movie = [] - for eligible_movie in eligible_movies: - - if eligible_movie in visited_movies: - break - - double_dips = find_all_dips_starting_from(eligible_movie) - - if isinstance(double_dips, DoubleDip): - double_dips.prepend(movie) - all_dips_from_movie.append(double_dips) - else: - # double dips should be a list - for double_dip in double_dips: - double_dip.flat_prepend(movie) - all_dips_from_movie.append(double_dip) - - return(all_dips_from_movie) - - # sort program in ASC start-time... - self.program.sort(key = attrgetter('start')) - - # create a stack to check if a movie has been visited in the DFS - visited_movies = [] - - self.double_dips = [] - - for movie in self.program: - - # dips can either be a single DoubleDip object, or a [DoubleDips]. - # if the latter, we can just + dips to self.double_dips. - # if the former, +'ing will cast the DoubleDip back down to a movie. - # This is undesirable for printing purposes later (when we want - # to convert to JSON). - dips = find_all_dips_starting_from(movie) - - # if a movie has already been visited, an [] will be returned - if isinstance(dips, DoubleDip): - self.double_dips.append(dips) - else: - self.double_dips += dips - - return(self.double_dips)