diff --git a/.gitignore b/.gitignore index 0652183..9a91afa 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,8 @@ __pycache__ .DS_Store *.log *.egg-info -build \ No newline at end of file +build + +# Experiment artifacts +*.png +*.pkl \ No newline at end of file diff --git a/deathstar/entities/user.py b/deathstar/entities/user.py deleted file mode 100644 index 0234e91..0000000 --- a/deathstar/entities/user.py +++ /dev/null @@ -1,79 +0,0 @@ -from typing import Any -from cascade.dataflow.dataflow import DataFlow, Edge, InvokeMethod, OpNode -from cascade.dataflow.operator import StatefulOperator -from deathstar.entities.flight import Flight, flight_op -from deathstar.entities.hotel import Hotel, hotel_op - - -class User(): - def __init__(self, user_id: str, password: str): - self.id = user_id - self.password = password - - def check(self, password): - return self.password == password - - def order(self, flight: Flight, hotel: Hotel): - if hotel.reserve() and flight.reserve(): - return True - else: - return False - -#### COMPILED FUNCTIONS (ORACLE) ##### - -def check_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.pop() - return state.password == variable_map["password"] - -def order_compiled_entry_0(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.append(variable_map["hotel"]) - -def order_compiled_entry_1(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.append(variable_map["flight"]) - -def order_compiled_if_cond(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - return variable_map["hotel_reserve"] and variable_map["flight_reserve"] - -def order_compiled_if_body(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.pop() - return True - -def order_compiled_else_body(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.pop() - return False - -user_op = StatefulOperator( - User, - { - "login": check_compiled, - "order_compiled_entry_0": order_compiled_entry_0, - "order_compiled_entry_1": order_compiled_entry_1, - "order_compiled_if_cond": order_compiled_if_cond, - "order_compiled_if_body": order_compiled_if_body, - "order_compiled_else_body": order_compiled_else_body - }, - {} -) - -# For now, the dataflow will be serial instead of parallel. Future optimizations -# will try to automatically parallelize this. -# There is also no user entry (this could also be an optimization) -df = DataFlow("user_order") -n0 = OpNode(user_op, InvokeMethod("order_compiled_entry_0")) -n1 = OpNode(hotel_op, InvokeMethod("reserve"), assign_result_to="hotel_reserve") -n2 = OpNode(user_op, InvokeMethod("order_compiled_entry_1")) -n3 = OpNode(flight_op, InvokeMethod("reserve"), assign_result_to="flight_reserve") -n4 = OpNode(user_op, InvokeMethod("order_compiled_if_cond"), is_conditional=True) -n5 = OpNode(user_op, InvokeMethod("order_compiled_if_body")) -n6 = OpNode(user_op, InvokeMethod("order_compiled_else_body")) - -df.add_edge(Edge(n0, n1)) -df.add_edge(Edge(n1, n2)) -df.add_edge(Edge(n2, n3)) -df.add_edge(Edge(n3, n4)) -df.add_edge(Edge(n4, n5, if_conditional=True)) -df.add_edge(Edge(n4, n6, if_conditional=False)) - -df.entry = n0 - -user_op.dataflows["order"] = df diff --git a/deathstar/__init__.py b/deathstar_hotel_reservation/__init__.py similarity index 100% rename from deathstar/__init__.py rename to deathstar_hotel_reservation/__init__.py diff --git a/deathstar/demo.py b/deathstar_hotel_reservation/demo.py similarity index 70% rename from deathstar/demo.py rename to deathstar_hotel_reservation/demo.py index a284ee7..b54d643 100644 --- a/deathstar/demo.py +++ b/deathstar_hotel_reservation/demo.py @@ -1,358 +1,375 @@ -import random -import sys -import os -import time -import csv -from timeit import default_timer as timer -from multiprocessing import Pool - -# import cascade -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) - -from cascade.dataflow.dataflow import Event, InitClass, InvokeMethod, OpNode -from cascade.runtime.flink_runtime import FlinkClientSync, FlinkOperator, FlinkRuntime, FlinkStatelessOperator -from deathstar.entities.flight import Flight, flight_op -from deathstar.entities.hotel import Geo, Hotel, Rate, hotel_op -from deathstar.entities.recommendation import Recommendation, recommend_op -from deathstar.entities.search import Search, search_op -from deathstar.entities.user import User, user_op - - -class DeathstarDemo(): - def __init__(self): - self.init_user = OpNode(user_op, InitClass()) - self.init_hotel = OpNode(hotel_op, InitClass()) - self.init_flight = OpNode(flight_op, InitClass()) - - def init_runtime(self, runtime, **kwargs): - self.runtime = runtime - self.runtime.init(**kwargs) - self.runtime.add_operator(hotel_op) - self.runtime.add_operator(flight_op) - self.runtime.add_operator(user_op) - self.runtime.add_stateless_operator(search_op) - self.runtime.add_stateless_operator(recommend_op) - - - def populate(self): - # Create locations & rates for hotels - geos = [] - geos.append(Geo(37.7867, 0)) - geos.append(Geo(37.7854, -122.4005)) - geos.append(Geo(37.7867, -122.4071)) - geos.append(Geo(37.7936, -122.3930)) - geos.append(Geo(37.7831, -122.4181)) - geos.append(Geo(37.7863, -122.4015)) - - for i in range(6, 100): - lat: float = 37.7835 + i / 500.0 * 3 - lon: float = -122.41 + i / 500.0 * 4 - geos.append(Geo(lat, lon)) - - rates = {} - rates[1] = Rate(1, "RACK", - "2015-04-09", - "2015-04-10", - { "BookableRate": 190.0, - "Code": "KNG", - "RoomDescription": "King sized bed", - "TotalRate": 109.0, - "TotalRateInclusive": 123.17}) - - rates[2] = Rate(2, "RACK", - "2015-04-09", - "2015-04-10", - { "BookableRate": 139.0, - "Code": "QN", - "RoomDescription": "Queen sized bed", - "TotalRate": 139.0, - "TotalRateInclusive": 153.09}) - - rates[3] = Rate(3, "RACK", - "2015-04-09", - "2015-04-10", - { "BookableRate": 109.0, - "Code": "KNG", - "RoomDescription": "King sized bed", - "TotalRate": 109.0, - "TotalRateInclusive": 123.17}) - - for i in range(4, 80): - if i % 3 == 0: - hotel_id = i - end_date = "2015-04-" - rate = 109.0 - rate_inc = 123.17 - if i % 2 == 0: - end_date += '17' - else: - end_date += '24' - if i % 5 == 1: - rate = 120.0 - rate_inc = 140.0 - elif i % 5 == 2: - rate = 124.0 - rate_inc = 144.0 - elif i % 5 == 3: - rate = 132.0 - rate_inc = 158.0 - elif i % 5 == 4: - rate = 232.0 - rate_inc = 258.0 - - rates[hotel_id] = Rate(i, "RACK", - "2015-04-09", - end_date, - { "BookableRate": rate, - "Code": "KNG", - "RoomDescription": "King sized bed", - "TotalRate": rate, - "TotalRateInclusive": rate_inc}) - - # we don't create recommendations, because it doesn't really - # correspond to an entity - prices = [] - - prices.append(150.00) - prices.append(120.00) - prices.append(190.00) - prices.append(160.00) - prices.append(140.00) - prices.append(200.00) - - for i in range(6, 100): - price = 179.00 - if i % 3 == 0: - if i % 5 == 0: - price = 123.17 - elif i % 5 == 1: - price = 140.00 - elif i % 5 == 2: - price = 144.00 - elif i % 5 == 3: - price = 158.00 - elif i % 5 == 4: - price = 258.00 - - prices.append(price) - - # populate users - self.users = [User(f"Cornell_{i}", str(i) * 10) for i in range(501)] - for user in self.users: - event = Event(self.init_user, [user.id], {"user_id": user.id, "password": user.password}, None) - self.runtime.send(event) - - # populate hotels - self.hotels: list[Hotel] = [] - for i in range(100): - geo = geos[i] - rate = rates[i] if i in rates else [] - price = prices[i] - hotel = Hotel(str(i), 10, geo, rate, price) - self.hotels.append(hotel) - event = Event(self.init_hotel, [hotel.key], - { - "key": hotel.key, - "cap": hotel.cap, - "geo": hotel.geo, - "rates": hotel.rates, - "price": hotel.price - }, None) - self.runtime.send(event) - - # populate flights - self.flights = [Flight(str(i), 10) for i in range(100)] - for flight in self.flights[:-1]: - event = Event(self.init_flight, [flight.id], { - "id": flight.id, - "cap": flight.cap - }, None) - self.runtime.send(event) - flight = self.flights[-1] - event = Event(self.init_flight, [flight.id], { - "id": flight.id, - "cap": flight.cap - }, None) - self.runtime.send(event, flush=True) - -def search_hotel(): - in_date = random.randint(9, 23) - out_date = random.randint(in_date + 1, 24) - - if in_date < 10: - in_date_str = f"2015-04-0{in_date}" - else: - in_date_str = f"2015-04-{in_date}" - if out_date < 10: - out_date_str = f"2015-04-0{out_date}" - else: - out_date_str = f"2015-04-{out_date}" - - lat = 38.0235 + (random.randint(0, 481) - 240.5) / 1000.0 - lon = -122.095 + (random.randint(0, 325) - 157.0) / 1000.0 - - # We don't really use the in_date, out_date information - return Event(search_op.dataflow.entry, ["tempkey"], {"lat": lat, "lon": lon}, search_op.dataflow) - -def recommend(req_param=None): - if req_param is None: - coin = random.random() - if coin < 0.5: - req_param = "distance" - else: - req_param = "price" - - lat = 38.0235 + (random.randint(0, 481) - 240.5) / 1000.0 - lon = -122.095 + (random.randint(0, 325) - 157.0) / 1000.0 - - return Event(recommend_op.dataflow.entry, ["tempkey"], {"requirement": req_param, "lat": lat, "lon": lon}, recommend_op.dataflow) - -def user_login(succesfull=True): - user_id = random.randint(0, 500) - username = f"Cornell_{user_id}" - password = str(user_id) * 10 if succesfull else "" - return Event(OpNode(user_op, InvokeMethod("login")), [username], {"password": password}, None) - -def reserve(): - hotel_id = random.randint(0, 99) - flight_id = random.randint(0, 99) - - # user = User("user1", "pass") - # user.order(flight, hotel) - user_id = "Cornell_" + str(random.randint(0, 500)) - - return Event(user_op.dataflows["order"].entry, [user_id], {"flight": str(flight_id), "hotel": str(hotel_id)}, user_op.dataflows["order"]) - -def deathstar_workload_generator(): - search_ratio = 0.6 - recommend_ratio = 0.39 - user_ratio = 0.005 - reserve_ratio = 0.005 - c = 0 - while True: - coin = random.random() - if coin < search_ratio: - yield search_hotel() - elif coin < search_ratio + recommend_ratio: - yield recommend() - elif coin < search_ratio + recommend_ratio + user_ratio: - yield user_login() - else: - yield reserve() - c += 1 - -threads = 1 -messages_per_second = 10 -sleeps_per_second = 10 -sleep_time = 0.0085 -seconds = 10 - - -def benchmark_runner(proc_num) -> dict[int, dict]: - print(f'Generator: {proc_num} starting') - client = FlinkClientSync("deathstar", "ds-out", "localhost:9092", True) - deathstar_generator = deathstar_workload_generator() - # futures: dict[int, dict] = {} - start = timer() - for _ in range(seconds): - sec_start = timer() - for i in range(messages_per_second): - if i % (messages_per_second // sleeps_per_second) == 0: - time.sleep(sleep_time) - event = next(deathstar_generator) - # func_name = event.dataflow.name if event.dataflow is not None else "login" # only login has no dataflow - key = event.key_stack[0] - # params = event.variable_map - client.send(event) - # futures[event._id] = {"event": f'{func_name} {key}->{params}'} - - client.flush() - sec_end = timer() - lps = sec_end - sec_start - if lps < 1: - time.sleep(1 - lps) - sec_end2 = timer() - print(f'Latency per second: {sec_end2 - sec_start}') - end = timer() - print(f'Average latency per second: {(end - start) / seconds}') - # styx.close() - # for key, metadata in styx.delivery_timestamps.items(): - # timestamp_futures[key]["timestamp"] = metadata - - done = False - while not done: - done = True - for event_id, fut in client._futures.items(): - result = fut["ret"] - if result is None: - done = False - time.sleep(0.5) - break - futures = client._futures - client.close() - return futures - - -def write_dict_to_csv(futures_dict, filename): - """ - Writes a dictionary of event data to a CSV file. - - Args: - futures_dict (dict): A dictionary where each key is an event ID and the value is another dict. - filename (str): The name of the CSV file to write to. - """ - # Define the column headers - headers = ["event_id", "sent", "sent_t", "ret", "ret_t", "latency"] - - # Open the file for writing - with open(filename, mode='w', newline='', encoding='utf-8') as csvfile: - writer = csv.DictWriter(csvfile, fieldnames=headers) - - # Write the headers - writer.writeheader() - - # Write the data rows - for event_id, event_data in futures_dict.items(): - # Prepare a row where the 'event_id' is the first column - row = { - "event_id": event_id, - "sent": event_data.get("sent"), - "sent_t": event_data.get("sent_t"), - "ret": event_data.get("ret"), - "ret_t": event_data.get("ret_t"), - "latency": event_data["ret_t"][1] - event_data["sent_t"][1] - } - writer.writerow(row) - -def main(): - ds = DeathstarDemo() - ds.init_runtime(FlinkRuntime("deathstar", "ds-out"), bundle_time=5, bundle_size=10) - ds.runtime.run(run_async=True) - ds.populate() - - - time.sleep(1) - input() - - # with Pool(threads) as p: - # results = p.map(benchmark_runner, range(threads)) - - # results = {k: v for d in results for k, v in d.items()} - results = benchmark_runner(0) - - # pd.DataFrame({"request_id": list(results.keys()), - # "timestamp": [res["timestamp"] for res in results.values()], - # "op": [res["op"] for res in results.values()] - # }).sort_values("timestamp").to_csv(f'{SAVE_DIR}/client_requests.csv', index=False) - print(results) - t = len(results) - r = 0 - for result in results.values(): - if result["ret"] is not None: - print(result) - r += 1 - print(f"{r}/{t} results recieved.") - write_dict_to_csv(results, "test2.csv") - -if __name__ == "__main__": +import random +import sys +import os +import time +import csv +from timeit import default_timer as timer +from multiprocessing import Pool + +# import cascade +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) + +from cascade.dataflow.dataflow import Event, EventResult, InitClass, InvokeMethod, OpNode +from cascade.runtime.flink_runtime import FlinkClientSync, FlinkOperator, FlinkRuntime, FlinkStatelessOperator +from deathstar_hotel_reservation.entities.flight import Flight, flight_op +from deathstar_hotel_reservation.entities.hotel import Geo, Hotel, Rate, hotel_op +from deathstar_hotel_reservation.entities.recommendation import Recommendation, recommend_op +from deathstar_hotel_reservation.entities.search import Search, search_op +from deathstar_hotel_reservation.entities.user import User, user_op +import pandas as pd + + +class DeathstarDemo(): + def __init__(self): + self.init_user = OpNode(User, InitClass(), read_key_from="user_id") + self.init_hotel = OpNode(Hotel, InitClass(), read_key_from="key") + self.init_flight = OpNode(Flight, InitClass(), read_key_from="id") + + def init_runtime(self, runtime, **kwargs): + self.runtime = runtime + self.runtime.init(**kwargs) + self.runtime.add_operator(hotel_op) + self.runtime.add_operator(flight_op) + self.runtime.add_operator(user_op) + # self.runtime.add_stateless_operator(search_op) + # self.runtime.add_stateless_operator(recommend_op) + + + def populate(self): + # Create locations & rates for hotels + geos = [] + geos.append(Geo(37.7867, 0)) + geos.append(Geo(37.7854, -122.4005)) + geos.append(Geo(37.7867, -122.4071)) + geos.append(Geo(37.7936, -122.3930)) + geos.append(Geo(37.7831, -122.4181)) + geos.append(Geo(37.7863, -122.4015)) + + for i in range(6, 100): + lat: float = 37.7835 + i / 500.0 * 3 + lon: float = -122.41 + i / 500.0 * 4 + geos.append(Geo(lat, lon)) + + rates = {} + rates[1] = Rate(1, "RACK", + "2015-04-09", + "2015-04-10", + { "BookableRate": 190.0, + "Code": "KNG", + "RoomDescription": "King sized bed", + "TotalRate": 109.0, + "TotalRateInclusive": 123.17}) + + rates[2] = Rate(2, "RACK", + "2015-04-09", + "2015-04-10", + { "BookableRate": 139.0, + "Code": "QN", + "RoomDescription": "Queen sized bed", + "TotalRate": 139.0, + "TotalRateInclusive": 153.09}) + + rates[3] = Rate(3, "RACK", + "2015-04-09", + "2015-04-10", + { "BookableRate": 109.0, + "Code": "KNG", + "RoomDescription": "King sized bed", + "TotalRate": 109.0, + "TotalRateInclusive": 123.17}) + + for i in range(4, 80): + if i % 3 == 0: + hotel_id = i + end_date = "2015-04-" + rate = 109.0 + rate_inc = 123.17 + if i % 2 == 0: + end_date += '17' + else: + end_date += '24' + if i % 5 == 1: + rate = 120.0 + rate_inc = 140.0 + elif i % 5 == 2: + rate = 124.0 + rate_inc = 144.0 + elif i % 5 == 3: + rate = 132.0 + rate_inc = 158.0 + elif i % 5 == 4: + rate = 232.0 + rate_inc = 258.0 + + rates[hotel_id] = Rate(i, "RACK", + "2015-04-09", + end_date, + { "BookableRate": rate, + "Code": "KNG", + "RoomDescription": "King sized bed", + "TotalRate": rate, + "TotalRateInclusive": rate_inc}) + + # we don't create recommendations, because it doesn't really + # correspond to an entity + prices = [] + + prices.append(150.00) + prices.append(120.00) + prices.append(190.00) + prices.append(160.00) + prices.append(140.00) + prices.append(200.00) + + for i in range(6, 100): + price = 179.00 + if i % 3 == 0: + if i % 5 == 0: + price = 123.17 + elif i % 5 == 1: + price = 140.00 + elif i % 5 == 2: + price = 144.00 + elif i % 5 == 3: + price = 158.00 + elif i % 5 == 4: + price = 258.00 + + prices.append(price) + + # populate users + self.users = [User(f"Cornell_{i}", str(i) * 10) for i in range(501)] + for user in self.users: + event = Event(self.init_user, {"user_id": user.id, "password": user.password}, None) + self.runtime.send(event) + + # populate hotels + self.hotels: list[Hotel] = [] + for i in range(100): + geo = geos[i] + rate = rates[i] if i in rates else [] + price = prices[i] + hotel = Hotel(str(i), 10, geo, rate, price) + self.hotels.append(hotel) + event = Event(self.init_hotel, + { + "key": hotel.key, + "cap": hotel.cap, + "geo": hotel.geo, + "rates": hotel.rates, + "price": hotel.price + }, None) + self.runtime.send(event) + + # populate flights + self.flights = [Flight(str(i), 10) for i in range(100)] + for flight in self.flights[:-1]: + event = Event(self.init_flight, { + "id": flight.id, + "cap": flight.cap + }, None) + self.runtime.send(event) + flight = self.flights[-1] + event = Event(self.init_flight, { + "id": flight.id, + "cap": flight.cap + }, None) + self.runtime.send(event, flush=True) + +def search_hotel(): + in_date = random.randint(9, 23) + out_date = random.randint(in_date + 1, 24) + + if in_date < 10: + in_date_str = f"2015-04-0{in_date}" + else: + in_date_str = f"2015-04-{in_date}" + if out_date < 10: + out_date_str = f"2015-04-0{out_date}" + else: + out_date_str = f"2015-04-{out_date}" + + lat = 38.0235 + (random.randint(0, 481) - 240.5) / 1000.0 + lon = -122.095 + (random.randint(0, 325) - 157.0) / 1000.0 + + # We don't really use the in_date, out_date information + return Event(search_op.dataflow.entry, {"lat": lat, "lon": lon}, search_op.dataflow) + +def recommend(req_param=None): + if req_param is None: + coin = random.random() + if coin < 0.5: + req_param = "distance" + else: + req_param = "price" + + lat = 38.0235 + (random.randint(0, 481) - 240.5) / 1000.0 + lon = -122.095 + (random.randint(0, 325) - 157.0) / 1000.0 + + + return Event(recommend_op.dataflow.entry, {"requirement": req_param, "lat": lat, "lon": lon}, recommend_op.dataflow) + +def user_login(succesfull=True): + user_id = random.randint(0, 500) + username = f"Cornell_{user_id}" + password = str(user_id) * 10 if succesfull else "" + return Event(OpNode(User, InvokeMethod("login"), read_key_from="user_key"), {"user_key": username, "password": password}, None) + + +def reserve(): + hotel_id = random.randint(0, 99) + flight_id = random.randint(0, 99) + + user_id = "Cornell_" + str(random.randint(0, 500)) + + return Event( + user_op.dataflows["order"].entry, + { + "user_key": user_id, + "flight_key": str(flight_id), + "hotel_key": str(hotel_id) + }, + user_op.dataflows["order"]) + +def deathstar_workload_generator(): + search_ratio = 0.6 + recommend_ratio = 0.39 + user_ratio = 0.005 + reserve_ratio = 0.005 + c = 0 + while True: + coin = random.random() + if coin < search_ratio: + yield search_hotel() + elif coin < search_ratio + recommend_ratio: + yield recommend() + elif coin < search_ratio + recommend_ratio + user_ratio: + yield user_login() + else: + yield reserve() + c += 1 + +def reserve_workload_generator(): + while True: + yield reserve() + +def user_login_workload_generator(): + while True: + yield user_login() + +threads = 1 +messages_per_burst = 10 +sleeps_per_burst = 1 +sleep_time = 0.0085 +seconds_per_burst = 1 +bursts = 50 + + +def benchmark_runner(proc_num) -> dict[int, dict]: + print(f'Generator: {proc_num} starting') + client = FlinkClientSync("deathstar", "ds-out", "localhost:9092", True) + deathstar_generator = user_login_workload_generator() + start = timer() + + for _ in range(bursts): + sec_start = timer() + + # send burst of messages + for i in range(messages_per_burst): + + # sleep sometimes between messages + if i % (messages_per_burst // sleeps_per_burst) == 0: + time.sleep(sleep_time) + event = next(deathstar_generator) + client.send(event) + + client.flush() + sec_end = timer() + + # wait out the second + lps = sec_end - sec_start + if lps < seconds_per_burst: + time.sleep(1 - lps) + sec_end2 = timer() + print(f'Latency per burst: {sec_end2 - sec_start} ({seconds_per_burst})') + + end = timer() + print(f'Average latency per burst: {(end - start) / bursts} ({seconds_per_burst})') + + done = False + while not done: + done = True + for event_id, fut in client._futures.items(): + result = fut["ret"] + if result is None: + done = False + time.sleep(0.5) + break + futures = client._futures + client.close() + return futures + + +def write_dict_to_pkl(futures_dict, filename): + """ + Writes a dictionary of event data to a pickle file. + + Args: + futures_dict (dict): A dictionary where each key is an event ID and the value is another dict. + filename (str): The name of the pickle file to write to. + """ + + # Prepare the data for the DataFrame + data = [] + for event_id, event_data in futures_dict.items(): + ret: EventResult = event_data.get("ret") + row = { + "event_id": event_id, + "sent": str(event_data.get("sent")), + "sent_t": event_data.get("sent_t"), + "ret": str(event_data.get("ret")), + "ret_t": event_data.get("ret_t"), + "roundtrip": ret.metadata["roundtrip"] if ret else None, + "flink_time": ret.metadata["flink_time"] if ret else None, + "deser_times": ret.metadata["deser_times"] if ret else None, + "loops": ret.metadata["loops"] if ret else None, + "latency": event_data["ret_t"][1] - event_data["sent_t"][1] if ret else None + } + data.append(row) + + # Create a DataFrame and save it as a pickle file + df = pd.DataFrame(data) + df.to_pickle(filename) + +def main(): + ds = DeathstarDemo() + ds.init_runtime(FlinkRuntime("deathstar", "ds-out", ui_port=8081), bundle_time=5, bundle_size=10) + ds.runtime.run(run_async=True) + ds.populate() + + + time.sleep(1) + input() + + # with Pool(threads) as p: + # results = p.map(benchmark_runner, range(threads)) + + # results = {k: v for d in results for k, v in d.items()} + results = benchmark_runner(0) + + # pd.DataFrame({"request_id": list(results.keys()), + # "timestamp": [res["timestamp"] for res in results.values()], + # "op": [res["op"] for res in results.values()] + # }).sort_values("timestamp").to_csv(f'{SAVE_DIR}/client_requests.csv', index=False) + print(results) + t = len(results) + r = 0 + for result in results.values(): + if result["ret"] is not None: + print(result) + r += 1 + print(f"{r}/{t} results recieved.") + write_dict_to_pkl(results, "test2.pkl") + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/deathstar/demo_python.py b/deathstar_hotel_reservation/demo_python.py similarity index 97% rename from deathstar/demo_python.py rename to deathstar_hotel_reservation/demo_python.py index 80e687c..1d44ac4 100644 --- a/deathstar/demo_python.py +++ b/deathstar_hotel_reservation/demo_python.py @@ -7,7 +7,7 @@ from cascade.runtime.python_runtime import PythonRuntime -from deathstar.demo import DeathstarDemo, deathstar_workload_generator +from deathstar_hotel_reservation.demo import DeathstarDemo, deathstar_workload_generator from timeit import default_timer as timer import csv diff --git a/deathstar/entities/__init__.py b/deathstar_hotel_reservation/entities/__init__.py similarity index 100% rename from deathstar/entities/__init__.py rename to deathstar_hotel_reservation/entities/__init__.py diff --git a/deathstar/entities/flight.py b/deathstar_hotel_reservation/entities/flight.py similarity index 89% rename from deathstar/entities/flight.py rename to deathstar_hotel_reservation/entities/flight.py index 60af68b..9a70415 100644 --- a/deathstar/entities/flight.py +++ b/deathstar_hotel_reservation/entities/flight.py @@ -1,33 +1,32 @@ -from typing import Any -from cascade.dataflow.dataflow import Operator -from cascade.dataflow.operator import StatefulOperator - - -class Flight(): - def __init__(self, id: str, cap: int): - self.id = id - self.cap = cap - # self.customers = [] - - # In order to be deterministic, we don't actually change the capacity - def reserve(self) -> bool: - if self.cap <= 0: - return False - return True - - -#### COMPILED FUNCTIONS (ORACLE) ##### - -def reserve_compiled(variable_map: dict[str, Any], state: Flight, key_stack: list[str]) -> Any: - key_stack.pop() - if state.cap <= 0: - return False - return True - -flight_op = StatefulOperator( - Flight, - { - "reserve": reserve_compiled - }, - {} # no dataflow? -) +from typing import Any +from cascade.dataflow.dataflow import Operator +from cascade.dataflow.operator import StatefulOperator + + +class Flight(): + def __init__(self, id: str, cap: int): + self.id = id + self.cap = cap + # self.customers = [] + + # In order to be deterministic, we don't actually change the capacity + def reserve(self) -> bool: + if self.cap <= 0: + return False + return True + + +#### COMPILED FUNCTIONS (ORACLE) ##### + +def reserve_compiled(variable_map: dict[str, Any], state: Flight) -> Any: + if state.cap <= 0: + return False + return True + +flight_op = StatefulOperator( + Flight, + { + "reserve": reserve_compiled + }, + {} # no dataflow? +) diff --git a/deathstar/entities/hotel.py b/deathstar_hotel_reservation/entities/hotel.py similarity index 80% rename from deathstar/entities/hotel.py rename to deathstar_hotel_reservation/entities/hotel.py index 6689168..923acb6 100644 --- a/deathstar/entities/hotel.py +++ b/deathstar_hotel_reservation/entities/hotel.py @@ -1,84 +1,81 @@ -from dataclasses import dataclass -from typing import Any, Optional -from cascade.dataflow.operator import StatefulOperator -from geopy.distance import distance - - -@dataclass -class Geo(): - lat: float - lon: float - - def distance_km(self, lat: float, lon: float): - return distance((lat, lon), (self.lat, self.lon)).km - -@dataclass -class Rate(): - key: int - code: str - in_date: str - out_date: str - room_type: dict - - def __key__(self): - return self.key - -# todo: add a linked entity -# e.g. reviews: list[Review] where Review is an entity -class Hotel(): - def __init__(self, - key: str, - cap: int, - geo: Geo, - rates: list[Rate], - price: float): - self.key = key - self.cap = cap - self.customers = [] - self.rates = rates - self.geo = geo - self.price = price - - # In order to be deterministic, we don't actually change the capacity - def reserve(self) -> bool: - if self.cap < 0: - return False - return True - - def get_geo(self) -> Geo: - return self.geo - - @staticmethod - def __all__() -> list['Hotel']: - pass - - def __key__(self) -> int: - return self.key - - - -#### COMPILED FUNCTIONS (ORACLE) ##### - -def reserve_compiled(variable_map: dict[str, Any], state: Hotel, key_stack: list[str]) -> Any: - key_stack.pop() - if state.cap <= 0: - return False - return True - -def get_geo_compiled(variable_map: dict[str, Any], state: Hotel, key_stack: list[str]) -> Any: - key_stack.pop() - return state.geo - -def get_price_compiled(variable_map: dict[str, Any], state: Hotel, key_stack: list[str]) -> Any: - key_stack.pop() - return state.price - -hotel_op = StatefulOperator( - Hotel, - { - "reserve": reserve_compiled, - "get_geo": get_geo_compiled, - "get_price": get_price_compiled - }, - {} # no dataflow? -) +from dataclasses import dataclass +from typing import Any +from cascade.dataflow.operator import StatefulOperator +from geopy.distance import distance + + +@dataclass +class Geo(): + lat: float + lon: float + + def distance_km(self, lat: float, lon: float): + return distance((lat, lon), (self.lat, self.lon)).km + +@dataclass +class Rate(): + key: int + code: str + in_date: str + out_date: str + room_type: dict + + def __key__(self): + return self.key + +# todo: add a linked entity +# e.g. reviews: list[Review] where Review is an entity +class Hotel(): + def __init__(self, + key: str, + cap: int, + geo: Geo, + rates: list[Rate], + price: float): + self.key = key + self.cap = cap + self.customers = [] + self.rates = rates + self.geo = geo + self.price = price + + # In order to be deterministic, we don't actually change the capacity + def reserve(self) -> bool: + if self.cap < 0: + return False + return True + + def get_geo(self) -> Geo: + return self.geo + + @staticmethod + def __all__() -> list['Hotel']: + pass + + def __key__(self) -> int: + return self.key + + + +#### COMPILED FUNCTIONS (ORACLE) ##### + +def reserve_compiled(variable_map: dict[str, Any], state: Hotel) -> Any: + if state.cap <= 0: + return False + return True + +def get_geo_compiled(variable_map: dict[str, Any], state: Hotel) -> Any: + return state.geo + +def get_price_compiled(variable_map: dict[str, Any], state: Hotel) -> Any: + return state.price + +hotel_op = StatefulOperator( + Hotel, + { + "reserve": reserve_compiled, + "get_geo": get_geo_compiled, + "get_price": get_price_compiled + }, + {} # no dataflow? +) diff --git a/deathstar/entities/recommendation.py b/deathstar_hotel_reservation/entities/recommendation.py similarity index 57% rename from deathstar/entities/recommendation.py rename to deathstar_hotel_reservation/entities/recommendation.py index 7667210..576da60 100644 --- a/deathstar/entities/recommendation.py +++ b/deathstar_hotel_reservation/entities/recommendation.py @@ -1,137 +1,129 @@ -from typing import Any, Literal -from cascade.dataflow.dataflow import CollectNode, DataFlow, Edge, InvokeMethod, OpNode, SelectAllNode -from cascade.dataflow.operator import StatelessOperator -from deathstar.entities.hotel import Geo, Hotel, hotel_op - -# Stateless -class Recommendation(): - @staticmethod - def get_recommendations(requirement: Literal["distance", "price"], lat: float, lon: float) -> list[Hotel]: - if requirement == "distance": - distances = [(hotel.geo.distance_km(lat, lon), hotel) - for hotel in Hotel.__all__()] - min_dist = min(distances, key=lambda x: x[0]) - res = [hotel for dist, hotel in distances if dist == min_dist] - elif requirement == "price": - prices = [(hotel.price, hotel) - for hotel in Hotel.__all__()] - min_price = min(prices, key=lambda x: x[0]) - res = [hotel for rate, hotel in prices if rate == min_price] - - # todo: raise error on else ...? - return res - -#### COMPILED FUNCTIONS (ORACLE) #### - -def get_recs_if_cond(variable_map: dict[str, Any], key_stack: list[str]): - return variable_map["requirement"] == "distance" - -# list comprehension entry -def get_recs_if_body_0(variable_map: dict[str, Any], key_stack: list[str]): - hotel_key = key_stack[-1] - # The body will need the hotel key (actually, couldn't we just take the top of the key stack again?) - variable_map["hotel_key"] = hotel_key - # The next node (Hotel.get_geo) will need the hotel key - key_stack.append(hotel_key) - - -# list comprehension body -def get_recs_if_body_1(variable_map: dict[str, Any], key_stack: list[str]): - hotel_geo: Geo = variable_map["hotel_geo"] - lat, lon = variable_map["lat"], variable_map["lon"] - dist = hotel_geo.distance_km(lat, lon) - return (dist, variable_map["hotel_key"]) - -# after list comprehension -def get_recs_if_body_2(variable_map: dict[str, Any], key_stack: list[str]): - distances = variable_map["distances"] - min_dist = min(distances, key=lambda x: x[0])[0] - variable_map["res"] = [hotel for dist, hotel in distances if dist == min_dist] - - -def get_recs_elif_cond(variable_map: dict[str, Any], key_stack: list[str]): - return variable_map["requirement"] == "price" - - -# list comprehension entry -def get_recs_elif_body_0(variable_map: dict[str, Any], key_stack: list[str]): - hotel_key = key_stack[-1] - # The body will need the hotel key (actually, couldn't we just take the top of the key stack again?) - variable_map["hotel_key"] = hotel_key - # The next node (Hotel.get_geo) will need the hotel key - key_stack.append(hotel_key) - - -# list comprehension body -def get_recs_elif_body_1(variable_map: dict[str, Any], key_stack: list[str]): - return (variable_map["hotel_price"], variable_map["hotel_key"]) - -# after list comprehension -def get_recs_elif_body_2(variable_map: dict[str, Any], key_stack: list[str]): - prices = variable_map["prices"] - min_price = min(prices, key=lambda x: x[0])[0] - variable_map["res"] = [hotel for price, hotel in prices if price == min_price] - - - -# a future optimization might instead duplicate this piece of code over the two -# branches, in order to reduce the number of splits by one -def get_recs_final(variable_map: dict[str, Any], key_stack: list[str]): - return variable_map["res"] - - -recommend_op = StatelessOperator({ - "get_recs_if_cond": get_recs_if_cond, - "get_recs_if_body_0": get_recs_if_body_0, - "get_recs_if_body_1": get_recs_if_body_1, - "get_recs_if_body_2": get_recs_if_body_2, - "get_recs_elif_cond": get_recs_elif_cond, - "get_recs_elif_body_0": get_recs_elif_body_0, - "get_recs_elif_body_1": get_recs_elif_body_1, - "get_recs_elif_body_2": get_recs_elif_body_2, - "get_recs_final": get_recs_final, -}, None) - -df = DataFlow("get_recommendations") -n1 = OpNode(recommend_op, InvokeMethod("get_recs_if_cond"), is_conditional=True) -n2 = OpNode(recommend_op, InvokeMethod("get_recs_if_body_0")) -n3 = OpNode(hotel_op, InvokeMethod("get_geo"), assign_result_to="hotel_geo") -n4 = OpNode(recommend_op, InvokeMethod("get_recs_if_body_1"), assign_result_to="distance") -n5 = CollectNode("distances", "distance") -n6 = OpNode(recommend_op, InvokeMethod("get_recs_if_body_2")) -ns1 = SelectAllNode(Hotel, n5) - -n7 = OpNode(recommend_op, InvokeMethod("get_recs_elif_cond"), is_conditional=True) -n8 = OpNode(recommend_op, InvokeMethod("get_recs_elif_body_0")) -n9 = OpNode(hotel_op, InvokeMethod("get_price"), assign_result_to="hotel_price") -n10 = OpNode(recommend_op, InvokeMethod("get_recs_elif_body_1"), assign_result_to="price") -n11 = CollectNode("prices", "price") -n12 = OpNode(recommend_op, InvokeMethod("get_recs_elif_body_2")) -ns2 = SelectAllNode(Hotel, n11) - - -n13 = OpNode(recommend_op, InvokeMethod("get_recs_final")) - -df.add_edge(Edge(n1, ns1, if_conditional=True)) -df.add_edge(Edge(n1, n7, if_conditional=False)) -df.add_edge(Edge(n7, ns2, if_conditional=True)) -df.add_edge(Edge(n7, n13, if_conditional=False)) - -# if branch -df.add_edge(Edge(ns1, n2)) -df.add_edge(Edge(n2, n3)) -df.add_edge(Edge(n3, n4)) -df.add_edge(Edge(n4, n5)) -df.add_edge(Edge(n5, n6)) -df.add_edge(Edge(n6, n13)) - -# elif branch -df.add_edge(Edge(ns2, n8)) -df.add_edge(Edge(n8, n9)) -df.add_edge(Edge(n9, n10)) -df.add_edge(Edge(n10, n11)) -df.add_edge(Edge(n11, n12)) -df.add_edge(Edge(n12, n13)) - -df.entry = n1 +from typing import Any, Literal +from cascade.dataflow.dataflow import CollectNode, DataFlow, Edge, InvokeMethod, OpNode, SelectAllNode, StatelessOpNode +from cascade.dataflow.operator import StatelessOperator +from deathstar_hotel_reservation.entities.hotel import Geo, Hotel + +# Stateless +class Recommendation(): + @staticmethod + def get_recommendations(requirement: Literal["distance", "price"], lat: float, lon: float) -> list[Hotel]: + if requirement == "distance": + distances = [(hotel.geo.distance_km(lat, lon), hotel) + for hotel in Hotel.__all__()] + min_dist = min(distances, key=lambda x: x[0]) + res = [hotel for dist, hotel in distances if dist == min_dist] + elif requirement == "price": + prices = [(hotel.price, hotel) + for hotel in Hotel.__all__()] + min_price = min(prices, key=lambda x: x[0]) + res = [hotel for rate, hotel in prices if rate == min_price] + + # todo: raise error on else ...? + return res + +#### COMPILED FUNCTIONS (ORACLE) #### + +def get_recs_if_cond(variable_map: dict[str, Any]): + return variable_map["requirement"] == "distance" + +# list comprehension entry +def get_recs_if_body_0(variable_map: dict[str, Any]): + pass + + +# list comprehension body +def get_recs_if_body_1(variable_map: dict[str, Any]): + hotel_geo: Geo = variable_map["hotel_geo"] + lat, lon = variable_map["lat"], variable_map["lon"] + dist = hotel_geo.distance_km(lat, lon) + return (dist, variable_map["hotel_key"]) + +# after list comprehension +def get_recs_if_body_2(variable_map: dict[str, Any]): + distances = variable_map["distances"] + min_dist = min(distances, key=lambda x: x[0])[0] + variable_map["res"] = [hotel for dist, hotel in distances if dist == min_dist] + + +def get_recs_elif_cond(variable_map: dict[str, Any]): + return variable_map["requirement"] == "price" + + +# list comprehension entry +def get_recs_elif_body_0(variable_map: dict[str, Any]): + pass + + +# list comprehension body +def get_recs_elif_body_1(variable_map: dict[str, Any]): + return (variable_map["hotel_price"], variable_map["hotel_key"]) + +# after list comprehension +def get_recs_elif_body_2(variable_map: dict[str, Any]): + prices = variable_map["prices"] + min_price = min(prices, key=lambda x: x[0])[0] + variable_map["res"] = [hotel for price, hotel in prices if price == min_price] + + + +# a future optimization might instead duplicate this piece of code over the two +# branches, in order to reduce the number of splits by one +def get_recs_final(variable_map: dict[str, Any]): + return variable_map["res"] + + +recommend_op = StatelessOperator({ + "get_recs_if_cond": get_recs_if_cond, + "get_recs_if_body_0": get_recs_if_body_0, + "get_recs_if_body_1": get_recs_if_body_1, + "get_recs_if_body_2": get_recs_if_body_2, + "get_recs_elif_cond": get_recs_elif_cond, + "get_recs_elif_body_0": get_recs_elif_body_0, + "get_recs_elif_body_1": get_recs_elif_body_1, + "get_recs_elif_body_2": get_recs_elif_body_2, + "get_recs_final": get_recs_final, +}, None) + +df = DataFlow("get_recommendations") +n1 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_if_cond"), is_conditional=True) +n2 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_if_body_0")) +n3 = OpNode(Hotel, InvokeMethod("get_geo"), assign_result_to="hotel_geo", read_key_from="hotel_key") +n4 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_if_body_1"), assign_result_to="distance") +n5 = CollectNode("distances", "distance") +n6 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_if_body_2")) +ns1 = SelectAllNode(Hotel, n5, assign_key_to="hotel_key") + +n7 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_elif_cond"), is_conditional=True) +n8 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_elif_body_0")) +n9 = OpNode(Hotel, InvokeMethod("get_price"), assign_result_to="hotel_price", read_key_from="hotel_key") +n10 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_elif_body_1"), assign_result_to="price") +n11 = CollectNode("prices", "price") +n12 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_elif_body_2")) +ns2 = SelectAllNode(Hotel, n11, assign_key_to="hotel_key") + + +n13 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_final")) + +df.add_edge(Edge(n1, ns1, if_conditional=True)) +df.add_edge(Edge(n1, n7, if_conditional=False)) +df.add_edge(Edge(n7, ns2, if_conditional=True)) +df.add_edge(Edge(n7, n13, if_conditional=False)) + +# if branch +df.add_edge(Edge(ns1, n2)) +df.add_edge(Edge(n2, n3)) +df.add_edge(Edge(n3, n4)) +df.add_edge(Edge(n4, n5)) +df.add_edge(Edge(n5, n6)) +df.add_edge(Edge(n6, n13)) + +# elif branch +df.add_edge(Edge(ns2, n8)) +df.add_edge(Edge(n8, n9)) +df.add_edge(Edge(n9, n10)) +df.add_edge(Edge(n10, n11)) +df.add_edge(Edge(n11, n12)) +df.add_edge(Edge(n12, n13)) + +df.entry = n1 recommend_op.dataflow = df \ No newline at end of file diff --git a/deathstar/entities/search.py b/deathstar_hotel_reservation/entities/search.py similarity index 55% rename from deathstar/entities/search.py rename to deathstar_hotel_reservation/entities/search.py index a2782d2..abc5895 100644 --- a/deathstar/entities/search.py +++ b/deathstar_hotel_reservation/entities/search.py @@ -1,85 +1,77 @@ -from typing import Any -from cascade.dataflow.dataflow import CollectNode, DataFlow, Edge, InvokeMethod, OpNode, SelectAllNode -from cascade.dataflow.operator import StatelessOperator -from deathstar.entities.hotel import Geo, Hotel, hotel_op - -# Stateless -class Search(): - # Get the 5 nearest hotels - @staticmethod - def nearby(lat: float, lon: float, in_date: int, out_date: int): - distances = [ - (dist, hotel) - for hotel in Hotel.__all__() - if (dist := hotel.geo.distance_km(lat, lon)) < 10] - hotels = [hotel for dist, hotel in sorted(distances)[:5]] - return hotels - - -#### COMPILED FUNCTIONS (ORACLE) ##### - - - -# predicate 1 -def search_nearby_compiled_0(variable_map: dict[str, Any], key_stack: list[str]): - # We assume that the top of the key stack is the hotel key. - # This assumption holds if the node before this one is a correctly - # configure SelectAllNode. - - hotel_key = key_stack[-1] - # The body will need the hotel key (actually, couldn't we just take the top of the key stack again?) - variable_map["hotel_key"] = hotel_key - # The next node (Hotel.get_geo) will need the hotel key - key_stack.append(hotel_key) - -# predicate 2 -def search_nearby_compiled_1(variable_map: dict[str, Any], key_stack: list[str]): - hotel_geo: Geo = variable_map["hotel_geo"] - lat, lon = variable_map["lat"], variable_map["lon"] - dist = hotel_geo.distance_km(lat, lon) - variable_map["dist"] = dist - return dist < 10 - - -# body -def search_nearby_compiled_2(variable_map: dict[str, Any], key_stack: list[str]): - return (variable_map["dist"], variable_map["hotel_key"]) - -# next line -def search_nearby_compiled_3(variable_map: dict[str, Any], key_stack: list[str]): - distances = variable_map["distances"] - hotels = [hotel for dist, hotel in sorted(distances)[:5]] - return hotels - - -search_op = StatelessOperator({ - "search_nearby_compiled_0": search_nearby_compiled_0, - "search_nearby_compiled_1": search_nearby_compiled_1, - "search_nearby_compiled_2": search_nearby_compiled_2, - "search_nearby_compiled_3": search_nearby_compiled_3, -}, None) - -df = DataFlow("search_nearby") -n1 = OpNode(search_op, InvokeMethod("search_nearby_compiled_0")) -n2 = OpNode(hotel_op, InvokeMethod("get_geo"), assign_result_to="hotel_geo") -n3 = OpNode(search_op, InvokeMethod("search_nearby_compiled_1"), is_conditional=True) -n4 = OpNode(search_op, InvokeMethod("search_nearby_compiled_2"), assign_result_to="search_body") -n5 = CollectNode("distances", "search_body") -n0 = SelectAllNode(Hotel, n5) - -n6 = OpNode(search_op, InvokeMethod("search_nearby_compiled_3")) - -df.add_edge(Edge(n0, n1)) -df.add_edge(Edge(n1, n2)) -df.add_edge(Edge(n2, n3)) - -# if true make the body -df.add_edge(Edge(n3, n4, if_conditional=True)) -df.add_edge(Edge(n4, n5)) -# if false skip past -df.add_edge(Edge(n3, n5, if_conditional=False)) - -df.add_edge(Edge(n5, n6)) - -df.entry = n0 +from typing import Any +from cascade.dataflow.dataflow import CollectNode, DataFlow, Edge, InvokeMethod, OpNode, SelectAllNode, StatelessOpNode +from cascade.dataflow.operator import StatelessOperator +from deathstar_hotel_reservation.entities.hotel import Geo, Hotel, hotel_op + +# Stateless +class Search(): + # Get the 5 nearest hotels + @staticmethod + def nearby(lat: float, lon: float, in_date: int, out_date: int): + distances = [ + (dist, hotel) + for hotel in Hotel.__all__() + if (dist := hotel.geo.distance_km(lat, lon)) < 10] + hotels = [hotel for dist, hotel in sorted(distances)[:5]] + return hotels + + +#### COMPILED FUNCTIONS (ORACLE) ##### + + + +# predicate 1 +def search_nearby_compiled_0(variable_map: dict[str, Any]): + pass + +# predicate 2 +def search_nearby_compiled_1(variable_map: dict[str, Any]): + hotel_geo: Geo = variable_map["hotel_geo"] + lat, lon = variable_map["lat"], variable_map["lon"] + dist = hotel_geo.distance_km(lat, lon) + variable_map["dist"] = dist + return dist < 10 + + +# body +def search_nearby_compiled_2(variable_map: dict[str, Any]): + return (variable_map["dist"], variable_map["hotel_key"]) + +# next line +def search_nearby_compiled_3(variable_map: dict[str, Any]): + distances = variable_map["distances"] + hotels = [hotel for dist, hotel in sorted(distances)[:5]] + return hotels + + +search_op = StatelessOperator({ + "search_nearby_compiled_0": search_nearby_compiled_0, + "search_nearby_compiled_1": search_nearby_compiled_1, + "search_nearby_compiled_2": search_nearby_compiled_2, + "search_nearby_compiled_3": search_nearby_compiled_3, +}, None) + +df = DataFlow("search_nearby") +n1 = StatelessOpNode(search_op, InvokeMethod("search_nearby_compiled_0")) +n2 = OpNode(Hotel, InvokeMethod("get_geo"), assign_result_to="hotel_geo", read_key_from="hotel_key") +n3 = StatelessOpNode(search_op, InvokeMethod("search_nearby_compiled_1"), is_conditional=True) +n4 = StatelessOpNode(search_op, InvokeMethod("search_nearby_compiled_2"), assign_result_to="search_body") +n5 = CollectNode("distances", "search_body") +n0 = SelectAllNode(Hotel, n5, assign_key_to="hotel_key") + +n6 = StatelessOpNode(search_op, InvokeMethod("search_nearby_compiled_3")) + +df.add_edge(Edge(n0, n1)) +df.add_edge(Edge(n1, n2)) +df.add_edge(Edge(n2, n3)) + +# if true make the body +df.add_edge(Edge(n3, n4, if_conditional=True)) +df.add_edge(Edge(n4, n5)) +# if false skip past +df.add_edge(Edge(n3, n5, if_conditional=False)) + +df.add_edge(Edge(n5, n6)) + +df.entry = n0 search_op.dataflow = df \ No newline at end of file diff --git a/deathstar_hotel_reservation/entities/user.py b/deathstar_hotel_reservation/entities/user.py new file mode 100644 index 0000000..dba7567 --- /dev/null +++ b/deathstar_hotel_reservation/entities/user.py @@ -0,0 +1,105 @@ +from typing import Any +from cascade.dataflow.dataflow import CollectNode, CollectTarget, DataFlow, Edge, InvokeMethod, OpNode +from cascade.dataflow.operator import StatefulOperator +from deathstar_hotel_reservation.entities.flight import Flight, flight_op +from deathstar_hotel_reservation.entities.hotel import Hotel, hotel_op + + +class User(): + def __init__(self, user_id: str, password: str): + self.id = user_id + self.password = password + + def check(self, password): + return self.password == password + + def order(self, flight: Flight, hotel: Hotel): + if hotel.reserve() and flight.reserve(): + return True + else: + return False + +#### COMPILED FUNCTIONS (ORACLE) ##### + +def check_compiled(variable_map: dict[str, Any], state: User) -> Any: + return state.password == variable_map["password"] + +def order_compiled_entry_0(variable_map: dict[str, Any], state: User) -> Any: + pass + +def order_compiled_entry_1(variable_map: dict[str, Any], state: User) -> Any: + pass + +def order_compiled_if_cond(variable_map: dict[str, Any], state: User) -> Any: + return variable_map["hotel_reserve"] and variable_map["flight_reserve"] + +def order_compiled_if_cond_parallel(variable_map: dict[str, Any], state: User) -> Any: + return variable_map["reserves"][0] and variable_map["reserves"][1] + +def order_compiled_if_body(variable_map: dict[str, Any], state: User) -> Any: + return True + +def order_compiled_else_body(variable_map: dict[str, Any], state: User) -> Any: + return False + +user_op = StatefulOperator( + User, + { + "login": check_compiled, + "order_compiled_entry_0": order_compiled_entry_0, + "order_compiled_entry_1": order_compiled_entry_1, + # "order_compiled_if_cond": order_compiled_if_cond, + "order_compiled_if_cond": order_compiled_if_cond_parallel, + "order_compiled_if_body": order_compiled_if_body, + "order_compiled_else_body": order_compiled_else_body + }, + {} +) + +# For now, the dataflow will be serial instead of parallel. Future optimizations +# will try to automatically parallelize this. +# There is also no user entry (this could also be an optimization) +def df_serial(): + df = DataFlow("user_order") + n0 = OpNode(User, InvokeMethod("order_compiled_entry_0"), read_key_from="user_key") + n1 = OpNode(Hotel, InvokeMethod("reserve"), assign_result_to="hotel_reserve", read_key_from="hotel_key") + n2 = OpNode(User, InvokeMethod("order_compiled_entry_1"), read_key_from="user_key") + n3 = OpNode(Flight, InvokeMethod("reserve"), assign_result_to="flight_reserve", read_key_from="flight_key") + n4 = OpNode(User, InvokeMethod("order_compiled_if_cond"), is_conditional=True, read_key_from="user_key") + n5 = OpNode(User, InvokeMethod("order_compiled_if_body"), read_key_from="user_key") + n6 = OpNode(User, InvokeMethod("order_compiled_else_body"), read_key_from="user_key") + + df.add_edge(Edge(n0, n1)) + df.add_edge(Edge(n1, n2)) + df.add_edge(Edge(n2, n3)) + df.add_edge(Edge(n3, n4)) + df.add_edge(Edge(n4, n5, if_conditional=True)) + df.add_edge(Edge(n4, n6, if_conditional=False)) + + df.entry = n0 + return df + + +# PARALLEL DATAFLOW +def df_parallel(): + df = DataFlow("user_order") + n0 = OpNode(User, InvokeMethod("order_compiled_entry_0"), read_key_from="user_key") + ct = CollectNode(assign_result_to="reserves", read_results_from="reserve") + n1 = OpNode(Hotel, InvokeMethod("reserve"), assign_result_to="reserve", read_key_from="hotel_key", collect_target=CollectTarget(ct, 2, 0)) + n3 = OpNode(Flight, InvokeMethod("reserve"), assign_result_to="reserve", read_key_from="flight_key", collect_target=CollectTarget(ct, 2, 1)) + n4 = OpNode(User, InvokeMethod("order_compiled_if_cond"), is_conditional=True, read_key_from="user_key") + n5 = OpNode(User, InvokeMethod("order_compiled_if_body"), read_key_from="user_key") + n6 = OpNode(User, InvokeMethod("order_compiled_else_body"), read_key_from="user_key") + + df.add_edge(Edge(n0, n1)) + df.add_edge(Edge(n0, n3)) + df.add_edge(Edge(n1, ct)) + df.add_edge(Edge(n3, ct)) + df.add_edge(Edge(ct, n4)) + df.add_edge(Edge(n4, n5, if_conditional=True)) + df.add_edge(Edge(n4, n6, if_conditional=False)) + + df.entry = n0 + return df + +user_op.dataflows["order"] = df_serial() diff --git a/deathstar/test_demo.py b/deathstar_hotel_reservation/test_demo.py similarity index 91% rename from deathstar/test_demo.py rename to deathstar_hotel_reservation/test_demo.py index a3afd4a..dea227f 100644 --- a/deathstar/test_demo.py +++ b/deathstar_hotel_reservation/test_demo.py @@ -1,100 +1,100 @@ - -import os -import sys - -# import cascade -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) - -from cascade.runtime.python_runtime import PythonClientSync, PythonRuntime -from cascade.runtime.flink_runtime import FlinkClientSync, FlinkRuntime -from deathstar.demo import DeathstarDemo, recommend, reserve, search_hotel, user_login -import time -import pytest - -@pytest.mark.integration -def test_deathstar_demo(): - ds = DeathstarDemo() - ds.init_runtime(FlinkRuntime("deathstardemo-test", "dsd-out")) - ds.runtime.run(run_async=True) - print("Populating, press enter to go to the next step when done") - ds.populate() - - client = FlinkClientSync("deathstardemo-test", "dsd-out") - input() - print("testing user login") - event = user_login() - client.send(event) - - input() - print("testing reserve") - event = reserve() - client.send(event) - - input() - print("testing search") - event = search_hotel() - client.send(event) - - input() - print("testing recommend (distance)") - time.sleep(0.5) - event = recommend(req_param="distance") - client.send(event) - - input() - print("testing recommend (price)") - time.sleep(0.5) - event = recommend(req_param="price") - client.send(event) - - print(client._futures) - input() - print("done!") - print(client._futures) - -def test_deathstar_demo_python(): - ds = DeathstarDemo() - ds.init_runtime(PythonRuntime()) - ds.runtime.run() - print("Populating, press enter to go to the next step when done") - ds.populate() - - time.sleep(2) - - client = PythonClientSync(ds.runtime) - print("testing user login") - event = user_login() - result = client.send(event) - assert result == True - event = user_login(succesfull=False) - result = client.send(event) - assert result == False - - print("testing reserve") - event = reserve() - result = client.send(event) - assert result == True - - return - print("testing search") - event = search_hotel() - result = client.send(event) - print(result) - - print("testing recommend (distance)") - time.sleep(0.5) - event = recommend(req_param="distance") - result = client.send(event) - print(result) - - print("testing recommend (price)") - time.sleep(0.5) - event = recommend(req_param="price") - result = client.send(event) - print(result) - - print("done!") - - -if __name__ == "__main__": + +import os +import sys + +# import cascade +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) + +from cascade.runtime.python_runtime import PythonClientSync, PythonRuntime +from cascade.runtime.flink_runtime import FlinkClientSync, FlinkRuntime +from deathstar_hotel_reservation.demo import DeathstarDemo, recommend, reserve, search_hotel, user_login +import time +import pytest + +@pytest.mark.integration +def test_deathstar_demo(): + ds = DeathstarDemo() + ds.init_runtime(FlinkRuntime("deathstardemo-test", "dsd-out")) + ds.runtime.run(run_async=True) + print("Populating, press enter to go to the next step when done") + ds.populate() + + client = FlinkClientSync("deathstardemo-test", "dsd-out") + input() + print("testing user login") + event = user_login() + client.send(event) + + input() + print("testing reserve") + event = reserve() + client.send(event) + + input() + print("testing search") + event = search_hotel() + client.send(event) + + input() + print("testing recommend (distance)") + time.sleep(0.5) + event = recommend(req_param="distance") + client.send(event) + + input() + print("testing recommend (price)") + time.sleep(0.5) + event = recommend(req_param="price") + client.send(event) + + print(client._futures) + input() + print("done!") + print(client._futures) + +def test_deathstar_demo_python(): + ds = DeathstarDemo() + ds.init_runtime(PythonRuntime()) + ds.runtime.run() + print("Populating, press enter to go to the next step when done") + ds.populate() + + time.sleep(0.1) + + client = PythonClientSync(ds.runtime) + print("testing user login") + event = user_login() + result = client.send(event) + assert result == True + event = user_login(succesfull=False) + result = client.send(event) + assert result == False + + print("testing reserve") + event = reserve() + result = client.send(event) + assert result == True + + return + print("testing search") + event = search_hotel() + result = client.send(event) + print(result) + + print("testing recommend (distance)") + time.sleep(0.5) + event = recommend(req_param="distance") + result = client.send(event) + print(result) + + print("testing recommend (price)") + time.sleep(0.5) + event = recommend(req_param="price") + result = client.send(event) + print(result) + + print("done!") + + +if __name__ == "__main__": test_deathstar_demo() \ No newline at end of file diff --git a/deathstar_movie_review/__init__.py b/deathstar_movie_review/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deathstar_movie_review/demo.py b/deathstar_movie_review/demo.py new file mode 100644 index 0000000..6e546e7 --- /dev/null +++ b/deathstar_movie_review/demo.py @@ -0,0 +1,224 @@ +import hashlib +import uuid + +from .movie_data import movie_data +from .workload_data import movie_titles, charset +import random +from timeit import default_timer as timer +import sys +import os + +# import cascade +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) + +from cascade.dataflow.dataflow import Event, EventResult, InitClass, OpNode +from cascade.runtime.flink_runtime import FlinkClientSync, FlinkRuntime +from cascade.dataflow.optimization.dead_node_elim import dead_node_elimination + +from .entities.user import user_op, User +from .entities.compose_review import compose_review_op +from .entities.frontend import frontend_op, text_op, unique_id_op +from .entities.movie import MovieInfo, movie_id_op, movie_info_op, plot_op, Plot, MovieId + +import time +import pandas as pd + +def populate_user(client: FlinkRuntime): + init_user = OpNode(User, InitClass(), read_key_from="username") + for i in range(1000): + user_id = f'user{i}' + username = f'username_{i}' + password = f'password_{i}' + hasher = hashlib.new('sha512') + salt = uuid.uuid1().bytes + hasher.update(password.encode()) + hasher.update(salt) + + password_hash = hasher.hexdigest() + + user_data = { + "userId": user_id, + "FirstName": "firstname", + "LastName": "lastname", + "Username": username, + "Password": password_hash, + "Salt": salt + } + event = Event(init_user, {"username": username, "user_data": user_data}, None) + client.send(event) + + +def populate_movie(client: FlinkRuntime): + init_movie_info = OpNode(MovieInfo, InitClass(), read_key_from="movie_id") + init_plot = OpNode(Plot, InitClass(), read_key_from="movie_id") + init_movie_id = OpNode(MovieId, InitClass(), read_key_from="title") + + for movie in movie_data: + movie_id = movie["MovieId"] + + # movie info -> write `movie` + event = Event(init_movie_info, {"movie_id": movie_id, "info": movie}, None) + client.send(event) + + # plot -> write "plot" + event = Event(init_plot, {"movie_id": movie_id, "plot": "plot"}, None) + client.send(event) + + # movie_id_op -> register movie id + event = Event(init_movie_id, {"title": movie["Title"], "movie_id": movie_id}, None) + client.send(event) + + +def compose_review(req_id): + user_index = random.randint(0, 999) + username = f"username_{user_index}" + password = f"password_{user_index}" + title = random.choice(movie_titles) + rating = random.randint(0, 10) + text = ''.join(random.choice(charset) for _ in range(256)) + + return frontend_op.dataflow.generate_event({ + "review": req_id, + "user": username, + "title": title, + "rating": rating, + "text": text + }) + +def deathstar_workload_generator(): + c = 1 + while True: + yield compose_review(c) + c += 1 + +threads = 1 +messages_per_burst = 10 +sleeps_per_burst = 10 +sleep_time = 0.08 #0.0085 +seconds_per_burst = 1 +bursts = 100 + + +def benchmark_runner(proc_num) -> dict[int, dict]: + print(f'Generator: {proc_num} starting') + client = FlinkClientSync("ds-movie-in", "ds-movie-out") + deathstar_generator = deathstar_workload_generator() + start = timer() + + for _ in range(bursts): + sec_start = timer() + + # send burst of messages + for i in range(messages_per_burst): + + # sleep sometimes between messages + if i % (messages_per_burst // sleeps_per_burst) == 0: + time.sleep(sleep_time) + event = next(deathstar_generator) + client.send(event) + + client.flush() + sec_end = timer() + + # wait out the second + lps = sec_end - sec_start + if lps < seconds_per_burst: + time.sleep(1 - lps) + sec_end2 = timer() + print(f'Latency per burst: {sec_end2 - sec_start} ({seconds_per_burst})') + + end = timer() + print(f'Average latency per burst: {(end - start) / bursts} ({seconds_per_burst})') + + done = False + while not done: + done = True + for event_id, fut in client._futures.items(): + result = fut["ret"] + if result is None: + done = False + time.sleep(0.5) + break + futures = client._futures + client.close() + return futures + + +def write_dict_to_pkl(futures_dict, filename): + """ + Writes a dictionary of event data to a pickle file. + + Args: + futures_dict (dict): A dictionary where each key is an event ID and the value is another dict. + filename (str): The name of the pickle file to write to. + """ + + # Prepare the data for the DataFrame + data = [] + for event_id, event_data in futures_dict.items(): + ret: EventResult = event_data.get("ret") + row = { + "event_id": event_id, + "sent": str(event_data.get("sent")), + "sent_t": event_data.get("sent_t"), + "ret": str(event_data.get("ret")), + "ret_t": event_data.get("ret_t"), + "roundtrip": ret.metadata["roundtrip"] if ret else None, + "flink_time": ret.metadata["flink_time"] if ret else None, + "deser_times": ret.metadata["deser_times"] if ret else None, + "loops": ret.metadata["loops"] if ret else None, + "latency": event_data["ret_t"][1] - event_data["sent_t"][1] if ret else None + } + data.append(row) + + # Create a DataFrame and save it as a pickle file + df = pd.DataFrame(data) + df.to_pickle(filename) + +def main(): + runtime = FlinkRuntime("ds-movie-in", "ds-movie-out", 8081) + runtime.init(bundle_time=5, bundle_size=10) + + print(frontend_op.dataflow.to_dot()) + # dead_node_elimination([], [frontend_op]) + print(frontend_op.dataflow.to_dot()) + input() + + + runtime.add_operator(compose_review_op) + runtime.add_operator(user_op) + runtime.add_operator(movie_info_op) + runtime.add_operator(movie_id_op) + runtime.add_operator(plot_op) + runtime.add_stateless_operator(frontend_op) + runtime.add_stateless_operator(unique_id_op) + runtime.add_stateless_operator(text_op) + + runtime.run(run_async=True) + populate_user(runtime) + populate_movie(runtime) + runtime.producer.flush() + time.sleep(1) + + input() + + # with Pool(threads) as p: + # results = p.map(benchmark_runner, range(threads)) + + # results = {k: v for d in results for k, v in d.items()} + results = benchmark_runner(0) + + print("last result:") + print(list(results.values())[-1]) + t = len(results) + r = 0 + for result in results.values(): + if result["ret"] is not None: + print(result) + r += 1 + print(f"{r}/{t} results recieved.") + write_dict_to_pkl(results, "test2.pkl") + +if __name__ == "__main__": + main() + diff --git a/deathstar_movie_review/entities/__init__.py b/deathstar_movie_review/entities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deathstar_movie_review/entities/compose_review.py b/deathstar_movie_review/entities/compose_review.py new file mode 100644 index 0000000..7423c4b --- /dev/null +++ b/deathstar_movie_review/entities/compose_review.py @@ -0,0 +1,59 @@ +from typing import Any + +from src.cascade.dataflow.operator import StatefulOperator + + +class ComposeReview: + def __init__(self, req_id: str, *args): # *args is a temporary hack to allow for creation of composereview on the fly + self.req_id = req_id + self.review_data = {} + + def upload_unique_id(self, review_id: int): + self.review_data["review_id"] = review_id + + # could use the User class instead? + def upload_user_id(self, user_id: str): + self.review_data["userId"] = user_id + + def upload_movie_id(self, movie_id: str): + self.review_data["movieId"] = movie_id + + def upload_rating(self, rating: int): + self.review_data["rating"] = rating + + def upload_text(self, text: str): + self.review_data["text"] = text + + def get_data(self): + return self.review_data + +def upload_unique_id_compiled(variable_map: dict[str, Any], state: ComposeReview) -> Any: + state.review_data["review_id"] = variable_map["review_id"] + +def upload_user_id_compiled(variable_map: dict[str, Any], state: ComposeReview) -> Any: + state.review_data["userId"] = variable_map["user_id"] + +def upload_movie_id_compiled(variable_map: dict[str, Any], state: ComposeReview) -> Any: + state.review_data["movieId"] = variable_map["movie_id"] + +def upload_rating_compiled(variable_map: dict[str, Any], state: ComposeReview) -> Any: + state.review_data["rating"] = variable_map["rating"] + +def upload_text_compiled(variable_map: dict[str, Any], state: ComposeReview) -> Any: + state.review_data["text"] = variable_map["text"] + +def get_data_compiled(variable_map: dict[str, Any], state: ComposeReview) -> Any: + return state.review_data + +compose_review_op = StatefulOperator( + ComposeReview, + { + "upload_unique_id": upload_unique_id_compiled, + "upload_user_id": upload_user_id_compiled, + "upload_movie_id": upload_movie_id_compiled, + "upload_rating": upload_rating_compiled, + "upload_text": upload_text_compiled, + "get_data": get_data_compiled, + }, + {} +) \ No newline at end of file diff --git a/deathstar_movie_review/entities/frontend.py b/deathstar_movie_review/entities/frontend.py new file mode 100644 index 0000000..db75bc2 --- /dev/null +++ b/deathstar_movie_review/entities/frontend.py @@ -0,0 +1,188 @@ +from typing import Any +import uuid + +from cascade.dataflow.dataflow import CollectNode, CollectTarget, DataFlow, Edge, InvokeMethod, OpNode, StatelessOpNode +from cascade.dataflow.operator import StatelessOperator +from deathstar_movie_review.entities.compose_review import ComposeReview +from deathstar_movie_review.entities.movie import MovieId +from deathstar_movie_review.entities.user import User + + +# unique_id is stateless +class UniqueId(): + @staticmethod + def upload_unique_id_2(review: ComposeReview): + review_id = uuid.uuid1().int >> 64 + review.upload_unique_id(review_id) + +# text is stateless +class Text(): + @staticmethod + def upload_text_2(review: ComposeReview, text: str): + review.upload_text(text) + +CHAR_LIMIT = 50 + +# frontend is made stateless +class Frontend(): + @staticmethod + def compose(review: ComposeReview, user: User, title: MovieId, rating: int, text: str): + + # dead node elimination will remove "returning back" to the original function + # + # cascade could theoritically allow for more advanced analysis, + # that would enable all these to run in parallel. However, this is only + # possible because + # 1. the individual functions don't depend on each other + # 2. the ordering of side-effects does not matter + UniqueId.upload_unique_id_2(review) + user.upload_user(review) + title.upload_movie(review, rating) + + text = text[:CHAR_LIMIT] # an operation like this could be reorderd for better efficiency! + Text.upload_text_2(review, text) + +###### COMPILED FUNCTIONS ###### + +### UPLOAD UNIQUE ### + +def upload_unique_compiled_0(variable_map: dict[str, Any]): + variable_map["review_id"] = uuid.uuid1().int >> 64 + +unique_id_op = StatelessOperator( + { + "upload_unique": upload_unique_compiled_0, + }, + None +) + +df = DataFlow("upload_unique_id") +n0 = StatelessOpNode(unique_id_op, InvokeMethod("upload_unique")) +n1 = OpNode(ComposeReview, InvokeMethod("upload_unique_id"), read_key_from="review") +df.entry = n0 +unique_id_op.dataflow = df + +### TEXT ### + +text_op = StatelessOperator( + {}, + None +) + +df = DataFlow("upload_text") +n0 = OpNode(ComposeReview, InvokeMethod("upload_text"), read_key_from="review") +df.entry = n0 +text_op.dataflow = df + +### FRONTEND ### + +def compose_compiled_0(variable_map: dict[str, Any]): + pass + + +frontend_op = StatelessOperator( + { + "empty": compose_compiled_0, + }, + None +) + +def frontend_df_serial(): + # This dataflow calls many other dataflows. + # It could be more useful to have a "Dataflow" node + df = DataFlow("compose") + n0 = StatelessOpNode(frontend_op, InvokeMethod("empty")) + + # Upload Unique DF + n1_a = StatelessOpNode(unique_id_op, InvokeMethod("upload_unique")) + n1_b = OpNode(ComposeReview, InvokeMethod("upload_unique_id"), read_key_from="review") + + n2 = StatelessOpNode(frontend_op, InvokeMethod("empty")) + + # Upload User DF + n3_a = OpNode(User, InvokeMethod("upload_user_compiled_0"), read_key_from="user") + n3_b = OpNode(ComposeReview, InvokeMethod("upload_user_id"), read_key_from="review") + + n4 = StatelessOpNode(frontend_op, InvokeMethod("empty")) + + # Upload Movie DF + n5_a = OpNode(MovieId, InvokeMethod("upload_movie_cond"), read_key_from="title", is_conditional=True) + n5_b = OpNode(ComposeReview, InvokeMethod("upload_movie_id"), read_key_from="review") + n5_c = OpNode(ComposeReview, InvokeMethod("upload_rating"), read_key_from="review") + + n6 = StatelessOpNode(frontend_op, InvokeMethod("empty")) + + # Upload Text DF + n7 = OpNode(ComposeReview, InvokeMethod("upload_text"), read_key_from="review") + + n8 = StatelessOpNode(frontend_op, InvokeMethod("empty")) + + df.add_edge(Edge(n0, n1_a)) + df.add_edge(Edge(n1_a, n1_b)) + df.add_edge(Edge(n1_b, n2)) + + df.add_edge(Edge(n2, n3_a)) + df.add_edge(Edge(n3_a, n3_b)) + df.add_edge(Edge(n3_b, n4)) + + df.add_edge(Edge(n4, n5_a)) + df.add_edge(Edge(n5_a, n5_b, if_conditional=True)) + df.add_edge(Edge(n5_a, n5_c, if_conditional=False)) + df.add_edge(Edge(n5_b, n6)) + df.add_edge(Edge(n5_c, n6)) + + df.add_edge(Edge(n6, n7)) + df.add_edge(Edge(n7, n8)) + + df.entry = n0 + return df + +def frontend_df_parallel(): + # This dataflow calls many other dataflows. + # It could be more useful to have a "Dataflow" node + df = DataFlow("compose") + # n0 = StatelessOpNode(frontend_op, InvokeMethod("empty")) + ct = CollectNode(assign_result_to="results", read_results_from="dummy") + + # Upload Unique DF + n1_a = StatelessOpNode(unique_id_op, InvokeMethod("upload_unique")) + n1_b = OpNode(ComposeReview, InvokeMethod("upload_unique_id"), read_key_from="review", collect_target=CollectTarget(ct, 4, 0)) + + + # Upload User DF + n3_a = OpNode(User, InvokeMethod("upload_user_compiled_0"), read_key_from="user") + n3_b = OpNode(ComposeReview, InvokeMethod("upload_user_id"), read_key_from="review", collect_target=CollectTarget(ct, 4, 1)) + + + # Upload Movie DF + n5_a = OpNode(MovieId, InvokeMethod("upload_movie_cond"), read_key_from="title", is_conditional=True) + n5_b = OpNode(ComposeReview, InvokeMethod("upload_movie_id"), read_key_from="review", collect_target=CollectTarget(ct, 4, 2)) + n5_c = OpNode(ComposeReview, InvokeMethod("upload_rating"), read_key_from="review", collect_target=CollectTarget(ct, 4, 2)) + + + # Upload Text DF + n7 = OpNode(ComposeReview, InvokeMethod("upload_text"), read_key_from="review",collect_target=CollectTarget(ct, 4, 3)) + + + # df.add_edge(Edge(n0, n1_a)) + df.add_edge(Edge(n1_a, n1_b)) + df.add_edge(Edge(n1_b, ct)) + + # df.add_edge(Edge(n0, n3_a)) + df.add_edge(Edge(n3_a, n3_b)) + df.add_edge(Edge(n3_b, ct)) + + # df.add_edge(Edge(n0, n5_a)) + df.add_edge(Edge(n5_a, n5_b, if_conditional=True)) + df.add_edge(Edge(n5_a, n5_c, if_conditional=False)) + df.add_edge(Edge(n5_b, ct)) + df.add_edge(Edge(n5_c, ct)) + + # df.add_edge(Edge(n0, n7)) + df.add_edge(Edge(n7, ct)) + + df.entry = [n1_a, n3_a, n5_a, n7] + return df + +frontend_op.dataflow = frontend_df_parallel() + diff --git a/deathstar_movie_review/entities/movie.py b/deathstar_movie_review/entities/movie.py new file mode 100644 index 0000000..ade4d19 --- /dev/null +++ b/deathstar_movie_review/entities/movie.py @@ -0,0 +1,72 @@ +from typing import Any +from cascade.dataflow.dataflow import DataFlow, Edge, InvokeMethod, OpNode +from cascade.dataflow.operator import StatefulOperator +from deathstar_movie_review.entities.compose_review import ComposeReview +from deathstar_movie_review.entities.user import User + + +class MovieId: + # key: 'title' + def __init__(self, title: str, movie_id: str): + self.title = title + self.movie_id = movie_id + + def upload_movie(self, review: ComposeReview, rating: int): + if self.movie_id is not None: + review.upload_movie_id(self.movie_id) + else: + review.upload_rating(rating) + + +def upload_movie_compiled_cond_0(variable_map: dict[str, Any], state: MovieId) -> Any: + variable_map["movie_id"] = state.movie_id # SSA + return variable_map["movie_id"] is not None + +movie_id_op = StatefulOperator( + MovieId, + { + "upload_movie_cond": upload_movie_compiled_cond_0 + }, + {} +) + +def upload_movie_df(): + df = DataFlow("movieId_upload_movie") + n0 = OpNode(MovieId, InvokeMethod("upload_movie_cond"), read_key_from="title", is_conditional=True) + n1 = OpNode(ComposeReview, InvokeMethod("upload_movie_id"), read_key_from="review") + n2 = OpNode(ComposeReview, InvokeMethod("upload_rating"), read_key_from="review") + + df.add_edge(Edge(n0, n1, if_conditional=True)) + df.add_edge(Edge(n0, n2, if_conditional=False)) + df.entry = n0 + return df + +movie_id_op.dataflows["upload_movie"] = upload_movie_df() + + + +### Other movie-related operators + +# key: movie_id + +class Plot: + def __init__(self, movie_id: str, plot: str): + self.movie_id = movie_id + self.plot = plot + +class MovieInfo: + def __init__(self, movie_id: str, info: dict): + self.movie_id = movie_id + self.info = info + +movie_info_op = StatefulOperator( + MovieInfo, + {}, + {} +) + +plot_op = StatefulOperator( + Plot, + {}, + {} +) \ No newline at end of file diff --git a/deathstar_movie_review/entities/user.py b/deathstar_movie_review/entities/user.py new file mode 100644 index 0000000..2b244a9 --- /dev/null +++ b/deathstar_movie_review/entities/user.py @@ -0,0 +1,36 @@ +from typing import Any +from deathstar_movie_review.entities.compose_review import ComposeReview +from src.cascade.dataflow.dataflow import DataFlow, Edge, InvokeMethod, OpNode +from src.cascade.dataflow.operator import StatefulOperator + + +class User: + def __init__(self, username: str, user_data: dict): + self.username = username + self.user_data = user_data + + def upload_user(self, review: ComposeReview): + review.upload_user_id(self.user_data["userId"]) + + +def upload_user_compiled_0(variable_map: dict[str, Any], state: User) -> Any: + variable_map["user_id"] = state.user_data["userId"] + +user_op = StatefulOperator( + User, + { + "upload_user_compiled_0": upload_user_compiled_0, + }, + {} +) + +def upload_df(): + df = DataFlow("user_upload_user") + n0 = OpNode(User, InvokeMethod("upload_user_compiled_0"), read_key_from="username") + n1 = OpNode(ComposeReview, InvokeMethod("upload_user_id"), read_key_from="review") + + df.add_edge(Edge(n0, n1)) + df.entry = n0 + return df + +user_op.dataflows["upload_user"] = upload_df() \ No newline at end of file diff --git a/deathstar_movie_review/movie_data.py b/deathstar_movie_review/movie_data.py new file mode 100644 index 0000000..a25f23f --- /dev/null +++ b/deathstar_movie_review/movie_data.py @@ -0,0 +1,2571 @@ +movie_data = [{"MovieId": "299534", "Title": "Avengers: Endgame", "Casts": [], "PlotId": "299534", + "ThumbnailIds": ["/or06FN3Dka5tukK1e9sl16pB3iy.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.6, + "NumRating": 4789}, + {"MovieId": "543103", "Title": "Kamen Rider Heisei Generations FOREVER", "Casts": [], "PlotId": "543103", + "ThumbnailIds": ["/6sOFQDlkY6El1B2P5gklzJfVdsT.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.8, + "NumRating": 12}, {"MovieId": "299537", "Title": "Captain Marvel", "Casts": [], "PlotId": "299537", + "ThumbnailIds": ["/AtsgWhDnHTq68L0lLsUrCnM7TjG.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 4726}, + {"MovieId": "447404", "Title": "Pok\u00e9mon Detective Pikachu", "Casts": [], "PlotId": "447404", + "ThumbnailIds": ["/wgQ7APnFpf1TuviKHXeEe3KnsTV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 120}, {"MovieId": "456740", "Title": "Hellboy", "Casts": [], "PlotId": "456740", + "ThumbnailIds": ["/bk8LyaMqUtaQ9hUShuvFznQYQKR.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.2, "NumRating": 320}, + {"MovieId": "537915", "Title": "After", "Casts": [], "PlotId": "537915", + "ThumbnailIds": ["/u3B2YKUjWABcxXZ6Nm9h10hLUbh.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 819}, + {"MovieId": "299536", "Title": "Avengers: Infinity War", "Casts": [], "PlotId": "299536", + "ThumbnailIds": ["/7WsyChQLEftFiDOVTGkv3hFpyyt.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.3, + "NumRating": 13379}, + {"MovieId": "495925", "Title": "Doraemon the Movie: Nobita's Treasure Island", "Casts": [], + "PlotId": "495925", "ThumbnailIds": ["/cmJ71gdZxCqkMUvGwWgSg3MK7pC.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.3, "NumRating": 16}, + {"MovieId": "438650", "Title": "Cold Pursuit", "Casts": [], "PlotId": "438650", + "ThumbnailIds": ["/otK0H9H1w3JVGJjad5Kzx3Z9kt2.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.2, + "NumRating": 445}, {"MovieId": "287947", "Title": "Shazam!", "Casts": [], "PlotId": "287947", + "ThumbnailIds": ["/xnopI5Xtky18MPhK40cZAGAOVeV.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.2, "NumRating": 1442}, + {"MovieId": "487297", "Title": "What Men Want", "Casts": [], "PlotId": "487297", + "ThumbnailIds": ["/30IiwvIRqPGjUV0bxJkZfnSiCL.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 222}, {"MovieId": "450465", "Title": "Glass", "Casts": [], "PlotId": "450465", + "ThumbnailIds": ["/svIDTNUoajS8dLEo7EosxvyAsgJ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.5, "NumRating": 2695}, + {"MovieId": "24428", "Title": "The Avengers", "Casts": [], "PlotId": "24428", + "ThumbnailIds": ["/cezWGskPY5x7GaglTTRN4Fugfb8.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 19269}, {"MovieId": "284054", "Title": "Black Panther", "Casts": [], "PlotId": "284054", + "ThumbnailIds": ["/uxzzxijgPIY7slzFvMotPv8wjKA.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.4, "NumRating": 11967}, + {"MovieId": "166428", "Title": "How to Train Your Dragon: The Hidden World", "Casts": [], + "PlotId": "166428", "ThumbnailIds": ["/xvx4Yhf0DVH8G4LzNISpMfFBDy2.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.6, "NumRating": 1718}, + {"MovieId": "468224", "Title": "Tolkien", "Casts": [], "PlotId": "468224", + "ThumbnailIds": ["/5ElXRKi773koW0mAnRjpNTpgZXZ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.9, + "NumRating": 8}, + {"MovieId": "445629", "Title": "Fighting with My Family", "Casts": [], "PlotId": "445629", + "ThumbnailIds": ["/3TZCBAdKQiz0cGKGEjZiyZUA01O.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 174}, + {"MovieId": "283995", "Title": "Guardians of the Galaxy Vol. 2", "Casts": [], "PlotId": "283995", + "ThumbnailIds": ["/y4MBh0EjBlMuOzv9axM4qJlmhzz.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.7, + "NumRating": 12212}, + {"MovieId": "99861", "Title": "Avengers: Age of Ultron", "Casts": [], "PlotId": "99861", + "ThumbnailIds": ["/t90Y3G8UGQp0f0DrP60wRu9gfrH.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 13126}, {"MovieId": "532671", "Title": "The Prodigy", "Casts": [], "PlotId": "532671", + "ThumbnailIds": ["/yyejodyk3lWncVjVhhrEkPctY9o.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.9, "NumRating": 161}, + {"MovieId": "920", "Title": "Cars", "Casts": [], "PlotId": "920", + "ThumbnailIds": ["/5damnMcRFKSjhCirgX3CMa88MBj.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 7366}, {"MovieId": "284053", "Title": "Thor: Ragnarok", "Casts": [], "PlotId": "284053", + "ThumbnailIds": ["/rzRwTcFvttcN1ZpX2xv4j3tSdJu.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.5, "NumRating": 11089}, + {"MovieId": "535167", "Title": "The Wandering Earth", "Casts": [], "PlotId": "535167", + "ThumbnailIds": ["/yzqpJcJT79CLTNdZVU7HHee6L3a.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 101}, + {"MovieId": "457799", "Title": "Extremely Wicked, Shockingly Evil and Vile", "Casts": [], + "PlotId": "457799", "ThumbnailIds": ["/zSuJ3r5zr5T26tTxyygHhgkUAIM.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 272}, + {"MovieId": "157433", "Title": "Pet Sematary", "Casts": [], "PlotId": "157433", + "ThumbnailIds": ["/7SPhr7Qj39vbnfF9O2qHRYaKHAL.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 383}, {"MovieId": "263115", "Title": "Logan", "Casts": [], "PlotId": "263115", + "ThumbnailIds": ["/gGBu0hKw9BGddG8RkRAMX7B6NDB.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.8, "NumRating": 12092}, + {"MovieId": "118340", "Title": "Guardians of the Galaxy", "Casts": [], "PlotId": "118340", + "ThumbnailIds": ["/y31QB9kn3XSudA15tV7UWQ9XLuW.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.9, + "NumRating": 17578}, + {"MovieId": "576393", "Title": "Fall in Love at First Kiss", "Casts": [], "PlotId": "576393", + "ThumbnailIds": ["/wtaSH8MfJSCEIrrEX9SQuHdU5sl.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 20}, + {"MovieId": "324857", "Title": "Spider-Man: Into the Spider-Verse", "Casts": [], "PlotId": "324857", + "ThumbnailIds": ["/iiZZdoQBEYBv6id8su7ImL0oCbD.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.4, + "NumRating": 3649}, {"MovieId": "424783", "Title": "Bumblebee", "Casts": [], "PlotId": "424783", + "ThumbnailIds": ["/fw02ONlDhrYjTSZV8XO6hhU3ds3.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.4, "NumRating": 2221}, + {"MovieId": "10195", "Title": "Thor", "Casts": [], "PlotId": "10195", + "ThumbnailIds": ["/9zDwvsISU8bR15R2yN3kh1lfqve.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 12476}, {"MovieId": "297802", "Title": "Aquaman", "Casts": [], "PlotId": "297802", + "ThumbnailIds": ["/5Kg76ldv7VxeX9YlcQXiowHgdX6.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.8, "NumRating": 5783}, + {"MovieId": "363088", "Title": "Ant-Man and the Wasp", "Casts": [], "PlotId": "363088", + "ThumbnailIds": ["/eivQmS3wqzqnQWILHLc4FsEfcXP.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 5977}, + {"MovieId": "280217", "Title": "The Lego Movie 2: The Second Part", "Casts": [], "PlotId": "280217", + "ThumbnailIds": ["/QTESAsBVZwjtGJNDP7utiGV37z.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 459}, {"MovieId": "137113", "Title": "Edge of Tomorrow", "Casts": [], "PlotId": "137113", + "ThumbnailIds": ["/tpoVEYvm6qcXueZrQYJNRLXL88s.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.6, "NumRating": 7924}, + {"MovieId": "449562", "Title": "The Hustle", "Casts": [], "PlotId": "449562", + "ThumbnailIds": ["/qibqW5Dnvqp4hcEnoTARbQgxwJy.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 11}, {"MovieId": "329996", "Title": "Dumbo", "Casts": [], "PlotId": "329996", + "ThumbnailIds": ["/279PwJAcelI4VuBtdzrZASqDPQr.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 1102}, + {"MovieId": "485811", "Title": "Redcon-1", "Casts": [], "PlotId": "485811", + "ThumbnailIds": ["/jOYUbe61DQiY628inVkR1KERS30.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.2, + "NumRating": 31}, {"MovieId": "491418", "Title": "Instant Family", "Casts": [], "PlotId": "491418", + "ThumbnailIds": ["/dic3GdmMpxxfkCQfvZnasb5ZkSG.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.5, "NumRating": 690}, + {"MovieId": "315635", "Title": "Spider-Man: Homecoming", "Casts": [], "PlotId": "315635", + "ThumbnailIds": ["/kY2c7wKgOfQjvbqe7yVzLTYkxJO.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 11238}, + {"MovieId": "122917", "Title": "The Hobbit: The Battle of the Five Armies", "Casts": [], + "PlotId": "122917", "ThumbnailIds": ["/9zRzFJuaj0CHIOhAkcCcFTvyu2X.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.3, "NumRating": 8241}, + {"MovieId": "526050", "Title": "Little", "Casts": [], "PlotId": "526050", + "ThumbnailIds": ["/4MDB6jJl3U7xK1Gw64zIqt9pQA4.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 38}, + {"MovieId": "480414", "Title": "The Curse of La Llorona", "Casts": [], "PlotId": "480414", + "ThumbnailIds": ["/jhZlXSnFUpNiLAek9EkPrtLEWQI.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.5, + "NumRating": 243}, {"MovieId": "522681", "Title": "Escape Room", "Casts": [], "PlotId": "522681", + "ThumbnailIds": ["/8Ls1tZ6qjGzfGHjBB7ihOnf7f0b.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.3, "NumRating": 1069}, + {"MovieId": "500852", "Title": "Miss Bala", "Casts": [], "PlotId": "500852", + "ThumbnailIds": ["/ae9yrSAS7nLZPbbkOm61pSuIqeo.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 52}, {"MovieId": "376865", "Title": "High Life", "Casts": [], "PlotId": "376865", + "ThumbnailIds": ["/wElOvH7H6sLElsTOLu1MY6oWRUx.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.8, "NumRating": 105}, + {"MovieId": "500682", "Title": "The Highwaymen", "Casts": [], "PlotId": "500682", + "ThumbnailIds": ["/4bRYg4l12yDuJvAfqvUOPnBrxno.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 514}, + {"MovieId": "337339", "Title": "The Fate of the Furious", "Casts": [], "PlotId": "337339", + "ThumbnailIds": ["/dImWM7GJqryWJO9LHa3XQ8DD5NH.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 6236}, {"MovieId": "1726", "Title": "Iron Man", "Casts": [], "PlotId": "1726", + "ThumbnailIds": ["/848chlIWVT41VtAAgyh9bWymAYb.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.5, "NumRating": 15386}, + {"MovieId": "1771", "Title": "Captain America: The First Avenger", "Casts": [], "PlotId": "1771", + "ThumbnailIds": ["/vSNxAJTlD0r02V9sPYpOjqDZXUK.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 12515}, + {"MovieId": "271110", "Title": "Captain America: Civil War", "Casts": [], "PlotId": "271110", + "ThumbnailIds": ["/kSBXou5Ac7vEqKd97wotJumyJvU.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 13696}, {"MovieId": "458723", "Title": "Us", "Casts": [], "PlotId": "458723", + "ThumbnailIds": ["/ux2dU1jQ2ACIMShzB3yP93Udpzc.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.2, "NumRating": 1268}, + {"MovieId": "566555", "Title": "Detective Conan: The Fist of Blue Sapphire", "Casts": [], + "PlotId": "566555", "ThumbnailIds": ["/1GyvpwvgswOrHvxjnw2FBLNkTyo.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.0, "NumRating": 2}, + {"MovieId": "390634", "Title": "Fate/stay night: Heaven\u2019s Feel II. lost butterfly", "Casts": [], + "PlotId": "390634", "ThumbnailIds": ["/4tS0iyKQBDFqVpVcH21MSJwXZdq.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 4.7, "NumRating": 46}, + {"MovieId": "375588", "Title": "Robin Hood", "Casts": [], "PlotId": "375588", + "ThumbnailIds": ["/AiRfixFcfTkNbn2A73qVJPlpkUo.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.7, + "NumRating": 1095}, + {"MovieId": "338952", "Title": "Fantastic Beasts: The Crimes of Grindelwald", "Casts": [], + "PlotId": "338952", "ThumbnailIds": ["/fMMrl8fD9gRCFJvsx0SuFwkEOop.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.9, "NumRating": 4636}, + {"MovieId": "76338", "Title": "Thor: The Dark World", "Casts": [], "PlotId": "76338", + "ThumbnailIds": ["/bnX5PqAdQZRXSw3aX3DutDcdso5.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 9932}, + {"MovieId": "399579", "Title": "Alita: Battle Angel", "Casts": [], "PlotId": "399579", + "ThumbnailIds": ["/xRWht48C2V8XNfzvPehyClOvDni.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 1951}, {"MovieId": "512196", "Title": "Happy Death Day 2U", "Casts": [], "PlotId": "512196", + "ThumbnailIds": ["/4tdnePOkOOzwuGPEOAHp8UA4vqx.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.1, "NumRating": 714}, + {"MovieId": "245891", "Title": "John Wick", "Casts": [], "PlotId": "245891", + "ThumbnailIds": ["/b9uYMMbm87IBFOq59pppvkkkgNg.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 9589}, {"MovieId": "347375", "Title": "Mile 22", "Casts": [], "PlotId": "347375", + "ThumbnailIds": ["/2L8ehd95eSW9x7KINYtZmRkAlrZ.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.0, "NumRating": 882}, + {"MovieId": "10138", "Title": "Iron Man 2", "Casts": [], "PlotId": "10138", + "ThumbnailIds": ["/ArqpkNYGfcTIA6umWt6xihfIZZv.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 12070}, {"MovieId": "287424", "Title": "Maggie", "Casts": [], "PlotId": "287424", + "ThumbnailIds": ["/mFt7Oo3pf8f1BZAdWLyUpYM63aT.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.3, "NumRating": 947}, + {"MovieId": "335983", "Title": "Venom", "Casts": [], "PlotId": "335983", + "ThumbnailIds": ["/2uNW4WbgBXL25BAbXGLnLqX71Sw.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 5909}, + {"MovieId": "671", "Title": "Harry Potter and the Philosopher's Stone", "Casts": [], "PlotId": "671", + "ThumbnailIds": ["/dCtFvscYcXQKTNvyyaQr2g2UacJ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.8, + "NumRating": 13675}, + {"MovieId": "454294", "Title": "The Kid Who Would Be King", "Casts": [], "PlotId": "454294", + "ThumbnailIds": ["/kBuvLX6zynQP0sjyqbXV4jNaZ4E.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 160}, + {"MovieId": "22", "Title": "Pirates of the Caribbean: The Curse of the Black Pearl", "Casts": [], + "PlotId": "22", "ThumbnailIds": ["/tkt9xR1kNX5R9rCebASKck44si2.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.7, "NumRating": 12157}, + {"MovieId": "424694", "Title": "Bohemian Rhapsody", "Casts": [], "PlotId": "424694", + "ThumbnailIds": ["/lHu1wtNaczFPGFDTrjCSzeLPTKN.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.1, + "NumRating": 7236}, {"MovieId": "504172", "Title": "The Mule", "Casts": [], "PlotId": "504172", + "ThumbnailIds": ["/oeZh7yEz3PMnZLgBPhrafFHRbVz.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.5, "NumRating": 1390}, + {"MovieId": "404368", "Title": "Ralph Breaks the Internet", "Casts": [], "PlotId": "404368", + "ThumbnailIds": ["/lvfIaThG5HA8THf76nghKinjjji.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 2406}, + {"MovieId": "120", "Title": "The Lord of the Rings: The Fellowship of the Ring", "Casts": [], + "PlotId": "120", "ThumbnailIds": ["/56zTpe2xvaA4alU51sRWPoKPYZy.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.3, "NumRating": 14253}, + {"MovieId": "395990", "Title": "Death Wish", "Casts": [], "PlotId": "395990", + "ThumbnailIds": ["/7FG13lLQcV9DC2Bhn0hjxc6AFXV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 1158}, + {"MovieId": "458156", "Title": "John Wick: Chapter 3 \u2013 Parabellum", "Casts": [], "PlotId": "458156", + "ThumbnailIds": ["/ziEuG1essDuWuC5lpWUaw1uXY2O.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "440161", "Title": "The Sisters Brothers", "Casts": [], "PlotId": "440161", + "ThumbnailIds": ["/7Tl2nZ6uvmxwK14Skbf9VFHEHpX.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.0, "NumRating": 517}, + {"MovieId": "301351", "Title": "We Are Your Friends", "Casts": [], "PlotId": "301351", + "ThumbnailIds": ["/accc6f6h3Xi8kURvYpPoATOsm2Z.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 1218}, + {"MovieId": "579598", "Title": "Vaarikkuzhiyile Kolapathakam", "Casts": [], "PlotId": "579598", + "ThumbnailIds": ["/qwDA7qSSQLwQ7JgDmHrflHFyQZf.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "442249", "Title": "The First Purge", "Casts": [], "PlotId": "442249", + "ThumbnailIds": ["/litjsBoiydO6JlO70uOX4N3WnNL.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.8, "NumRating": 1778}, + {"MovieId": "218043", "Title": "Left Behind", "Casts": [], "PlotId": "218043", + "ThumbnailIds": ["/lWf8po2lGleVuzRB4lpHavVT1Lv.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 3.9, + "NumRating": 694}, {"MovieId": "383498", "Title": "Deadpool 2", "Casts": [], "PlotId": "383498", + "ThumbnailIds": ["/to0spRl1CMDvyUbOnbb4fTk3VAd.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.5, "NumRating": 8451}, + {"MovieId": "487558", "Title": "BlacKkKlansman", "Casts": [], "PlotId": "487558", + "ThumbnailIds": ["/3ntR66u2SHZ2UA3r3DjF2Dl6Kwx.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 2892}, {"MovieId": "298250", "Title": "Jigsaw", "Casts": [], "PlotId": "298250", + "ThumbnailIds": ["/2mUqHJG7ZiGwZYIylczFCsRPbXM.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.1, "NumRating": 1837}, + {"MovieId": "454983", "Title": "The Kissing Booth", "Casts": [], "PlotId": "454983", + "ThumbnailIds": ["/7Dktk2ST6aL8h9Oe5rpk903VLhx.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 3269}, + {"MovieId": "263109", "Title": "Shaun the Sheep Movie", "Casts": [], "PlotId": "263109", + "ThumbnailIds": ["/aOHsNN1p2nuiF9WaMaCNXy0T80J.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 697}, + {"MovieId": "3512", "Title": "Under Siege 2: Dark Territory", "Casts": [], "PlotId": "3512", + "ThumbnailIds": ["/6Z1p71nkm45cYuIZWOx5JSCYc0o.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.6, + "NumRating": 358}, {"MovieId": "460019", "Title": "Truth or Dare", "Casts": [], "PlotId": "460019", + "ThumbnailIds": ["/kdkNaQYZ7dhM80LsnAGKpH8ca2g.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 1908}, + {"MovieId": "284052", "Title": "Doctor Strange", "Casts": [], "PlotId": "284052", + "ThumbnailIds": ["/4PiiNGXj1KENTmCBHeN6Mskj2Fq.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 12292}, + {"MovieId": "122", "Title": "The Lord of the Rings: The Return of the King", "Casts": [], "PlotId": "122", + "ThumbnailIds": ["/rCzpDGLbOoPwLjy3OAm5NUPOTrC.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.4, + "NumRating": 12967}, {"MovieId": "1585", "Title": "It's a Wonderful Life", "Casts": [], "PlotId": "1585", + "ThumbnailIds": ["/rgj6QjdyCeDrO9KGt1kusGyhvb2.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 8.2, "NumRating": 1986}, + {"MovieId": "157336", "Title": "Interstellar", "Casts": [], "PlotId": "157336", + "ThumbnailIds": ["/nBNZadXqJSdt05SHLqgT0HuC5Gm.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.2, + "NumRating": 18294}, + {"MovieId": "446021", "Title": "Bad Times at the El Royale", "Casts": [], "PlotId": "446021", + "ThumbnailIds": ["/iNtFgXqXPRMkm1QO8CHn5sHfUgE.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 1261}, + {"MovieId": "464504", "Title": "A Madea Family Funeral", "Casts": [], "PlotId": "464504", + "ThumbnailIds": ["/sFvOTUlZrIxCLdmz1fC16wK0lme.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.5, + "NumRating": 305}, {"MovieId": "459992", "Title": "Long Shot", "Casts": [], "PlotId": "459992", + "ThumbnailIds": ["/m2ttWZ8rMRwIMT7zA48Jo6mTkDS.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.9, "NumRating": 35}, + {"MovieId": "361292", "Title": "Suspiria", "Casts": [], "PlotId": "361292", + "ThumbnailIds": ["/dzWTnkert9EoiPWldWJ15dnfAFl.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.1, + "NumRating": 668}, {"MovieId": "155", "Title": "The Dark Knight", "Casts": [], "PlotId": "155", + "ThumbnailIds": ["/1hRoyzDtpgMU7Dz4JF22RANzQO7.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.4, "NumRating": 18634}, + {"MovieId": "627", "Title": "Trainspotting", "Casts": [], "PlotId": "627", + "ThumbnailIds": ["/p1O3eFsdb0GEIYu87xlwV7P4jM1.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.0, + "NumRating": 5228}, {"MovieId": "11", "Title": "Star Wars", "Casts": [], "PlotId": "11", + "ThumbnailIds": ["/btTdmkgIvOi0FFip1sPuZI2oQG6.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 8.2, "NumRating": 11282}, + {"MovieId": "428078", "Title": "Mortal Engines", "Casts": [], "PlotId": "428078", + "ThumbnailIds": ["/iteUvQKCW0EqNQrIVzZGJntYq9s.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 1597}, {"MovieId": "339877", "Title": "Loving Vincent", "Casts": [], "PlotId": "339877", + "ThumbnailIds": ["/56sq57kDm7XgyXBYrgJLumo0Jac.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 8.2, "NumRating": 1178}, + {"MovieId": "351286", "Title": "Jurassic World: Fallen Kingdom", "Casts": [], "PlotId": "351286", + "ThumbnailIds": ["/c9XxwwhPHdaImA2f1WEfEsbhaFB.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 5800}, {"MovieId": "463684", "Title": "Velvet Buzzsaw", "Casts": [], "PlotId": "463684", + "ThumbnailIds": ["/3rViQPcrWthMNecp5XnkKev6BzW.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.3, "NumRating": 806}, + {"MovieId": "407451", "Title": "A Wrinkle in Time", "Casts": [], "PlotId": "407451", + "ThumbnailIds": ["/yAcb58vipewa1BfNit2RjE6boXA.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.0, + "NumRating": 1046}, + {"MovieId": "11430", "Title": "The Lion King 1\u00bd", "Casts": [], "PlotId": "11430", + "ThumbnailIds": ["/vDfaIoXTaQLUD5HVAv2hLIFKAcq.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 1547}, + {"MovieId": "429617", "Title": "Spider-Man: Far from Home", "Casts": [], "PlotId": "429617", + "ThumbnailIds": ["/q1ZcgXatgXo58tUO3vEsrJhYSbu.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, + {"MovieId": "672", "Title": "Harry Potter and the Chamber of Secrets", "Casts": [], "PlotId": "672", + "ThumbnailIds": ["/sdEOH0992YZ0QSxgXNIGLq1ToUi.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.7, + "NumRating": 11563}, + {"MovieId": "348350", "Title": "Solo: A Star Wars Story", "Casts": [], "PlotId": "348350", + "ThumbnailIds": ["/3IGbjc5ZC5yxim5W0sFING2kdcz.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 3689}, {"MovieId": "502292", "Title": "The Last Summer", "Casts": [], "PlotId": "502292", + "ThumbnailIds": ["/roxH78GESToUjfd6Tc973jV0Wu7.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.1, "NumRating": 259}, + {"MovieId": "68721", "Title": "Iron Man 3", "Casts": [], "PlotId": "68721", + "ThumbnailIds": ["/7XiGqZE8meUv7L4720L0tIDd7gO.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 13891}, {"MovieId": "489925", "Title": "Eighth Grade", "Casts": [], "PlotId": "489925", + "ThumbnailIds": ["/xTa9cLhGHfQ7084UvoPQ2bBXKqd.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.4, "NumRating": 480}, + {"MovieId": "460885", "Title": "Mandy", "Casts": [], "PlotId": "460885", + "ThumbnailIds": ["/m0yf7J7HsKeK6E81SMRcX8vx6mH.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 591}, {"MovieId": "562", "Title": "Die Hard", "Casts": [], "PlotId": "562", + "ThumbnailIds": ["/mc7MubOLcIw3MDvnuQFrO9psfCa.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.7, "NumRating": 6137}, + {"MovieId": "12536", "Title": "Home Alone 4", "Casts": [], "PlotId": "12536", + "ThumbnailIds": ["/359bHILkiD6ZVCq6WoHSD0UuJQV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.2, + "NumRating": 513}, {"MovieId": "429300", "Title": "Adrift", "Casts": [], "PlotId": "429300", + "ThumbnailIds": ["/5gLDeADaETvwQlQow5szlyuhLbj.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 1207}, + {"MovieId": "346910", "Title": "The Predator", "Casts": [], "PlotId": "346910", + "ThumbnailIds": ["/wMq9kQXTeQCHUZOG4fAe5cAxyUA.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.2, + "NumRating": 1934}, {"MovieId": "375262", "Title": "The Favourite", "Casts": [], "PlotId": "375262", + "ThumbnailIds": ["/cwBq0onfmeilU5xgqNNjJAMPfpw.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.7, "NumRating": 1925}, + {"MovieId": "438799", "Title": "Overlord", "Casts": [], "PlotId": "438799", + "ThumbnailIds": ["/l76Rgp32z2UxjULApxGXAPpYdAP.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 965}, {"MovieId": "244506", "Title": "Camp X-Ray", "Casts": [], "PlotId": "244506", + "ThumbnailIds": ["/oGcmIqOAbV8npgY57u7tqzwPgc.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.8, "NumRating": 608}, + {"MovieId": "400650", "Title": "Mary Poppins Returns", "Casts": [], "PlotId": "400650", + "ThumbnailIds": ["/uTVGku4LibMGyKgQvjBtv3OYfAX.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 1341}, + {"MovieId": "450001", "Title": "Master Z: Ip Man Legacy", "Casts": [], "PlotId": "450001", + "ThumbnailIds": ["/6VxEvOF7QDovsG6ro9OVyjH07LF.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.1, + "NumRating": 133}, + {"MovieId": "532321", "Title": "Re: Zero kara Hajimeru Isekai Seikatsu - Memory Snow", "Casts": [], + "PlotId": "532321", "ThumbnailIds": ["/xqR4ABkFTFYe8NDJi3knwWX7zfn.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 4.9, "NumRating": 7}, + {"MovieId": "11024", "Title": "Scooby-Doo 2: Monsters Unleashed", "Casts": [], "PlotId": "11024", + "ThumbnailIds": ["/vEp6qw25qY2n03O9EaeiWNv89vb.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.6, + "NumRating": 1223}, + {"MovieId": "287948", "Title": "The Transporter Refueled", "Casts": [], "PlotId": "287948", + "ThumbnailIds": ["/zW7oC3tucYLzu77xNbPbYjXUN4o.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.4, + "NumRating": 824}, {"MovieId": "340676", "Title": "Personal Shopper", "Casts": [], "PlotId": "340676", + "ThumbnailIds": ["/jDOWYDmJvGkVkVHf7Ru66gy6ry8.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.8, "NumRating": 584}, + {"MovieId": "226857", "Title": "Endless Love", "Casts": [], "PlotId": "226857", + "ThumbnailIds": ["/wWkTnQosziIXEePOyunr53el8fe.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 977}, {"MovieId": "102899", "Title": "Ant-Man", "Casts": [], "PlotId": "102899", + "ThumbnailIds": ["/D6e8RJf2qUstnfkTslTXNTUAlT.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 11671}, + {"MovieId": "483906", "Title": "Polar", "Casts": [], "PlotId": "483906", + "ThumbnailIds": ["/qOBEpKVLl8Q9CZScbOcRRVISezV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 781}, {"MovieId": "527261", "Title": "The Silence", "Casts": [], "PlotId": "527261", + "ThumbnailIds": ["/lTVOquzxw2DPF3MKuYd1ynz9F6H.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 526}, + {"MovieId": "411728", "Title": "The Professor and the Madman", "Casts": [], "PlotId": "411728", + "ThumbnailIds": ["/gtGCDLhfjW96qVarwctnuTpGOtD.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 88}, {"MovieId": "429351", "Title": "12 Strong", "Casts": [], "PlotId": "429351", + "ThumbnailIds": ["/j18021qCeRi3yUBtqd2UFj1c0RQ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.6, "NumRating": 1278}, + {"MovieId": "396806", "Title": "Anon", "Casts": [], "PlotId": "396806", + "ThumbnailIds": ["/xhBTO9n3fxy3HJt7WlR9h9vvVmk.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.9, + "NumRating": 659}, {"MovieId": "531309", "Title": "Brightburn", "Casts": [], "PlotId": "531309", + "ThumbnailIds": ["/roslEbKdY0WSgYaB5KXvPKY0bXS.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 0.0, "NumRating": 1}, + {"MovieId": "454227", "Title": "Outlaw King", "Casts": [], "PlotId": "454227", + "ThumbnailIds": ["/rT49ousKUWN3dV7UlhaC9onTNdl.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 592}, {"MovieId": "76617", "Title": "Texas Chainsaw 3D", "Casts": [], "PlotId": "76617", + "ThumbnailIds": ["/p0kJOoNvwnWgmvKvL4GqlV3OPUV.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.3, "NumRating": 765}, + {"MovieId": "440472", "Title": "The Upside", "Casts": [], "PlotId": "440472", + "ThumbnailIds": ["/hPZ2caow1PCND6qnerfgn6RTXdm.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 200}, {"MovieId": "514439", "Title": "Breakthrough", "Casts": [], "PlotId": "514439", + "ThumbnailIds": ["/t58dx7JIgchr9If5uxn3NmHaHoS.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 34}, + {"MovieId": "378236", "Title": "The Emoji Movie", "Casts": [], "PlotId": "378236", + "ThumbnailIds": ["/f5pF4OYzh4wb1dYL2ARQNdqUsEZ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.4, + "NumRating": 1411}, {"MovieId": "490132", "Title": "Green Book", "Casts": [], "PlotId": "490132", + "ThumbnailIds": ["/7BsvSuDQuoqhWmU2fL7W2GOcZHU.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 8.3, "NumRating": 3120}, + {"MovieId": "431530", "Title": "A Bad Moms Christmas", "Casts": [], "PlotId": "431530", + "ThumbnailIds": ["/gPNHolu7AGnrB7r5kvJRRTfwMFR.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 1013}, {"MovieId": "293660", "Title": "Deadpool", "Casts": [], "PlotId": "293660", + "ThumbnailIds": ["/inVq3FRqcYIRl2la8iZikYYxFNR.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.6, "NumRating": 19875}, + {"MovieId": "11451", "Title": "Herbie Fully Loaded", "Casts": [], "PlotId": "11451", + "ThumbnailIds": ["/7lTfTZ8CDfXw09eAv3OOvsbCVgs.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.3, + "NumRating": 1168}, + {"MovieId": "209112", "Title": "Batman v Superman: Dawn of Justice", "Casts": [], "PlotId": "209112", + "ThumbnailIds": ["/cGOPbv9wA5gEejkUN892JrveARt.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 11827}, {"MovieId": "86467", "Title": "Bel Ami", "Casts": [], "PlotId": "86467", + "ThumbnailIds": ["/59bgVtpX6MPy2d5zberJiRNkDDx.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.2, "NumRating": 308}, + {"MovieId": "270010", "Title": "A Hologram for the King", "Casts": [], "PlotId": "270010", + "ThumbnailIds": ["/dDHJBd2iv7KTDzI7ybNLs31vvkM.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.9, + "NumRating": 574}, + {"MovieId": "131631", "Title": "The Hunger Games: Mockingjay - Part 1", "Casts": [], "PlotId": "131631", + "ThumbnailIds": ["/gj282Pniaa78ZJfbaixyLXnXEDI.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 9938}, {"MovieId": "151960", "Title": "Planes", "Casts": [], "PlotId": "151960", + "ThumbnailIds": ["/c8ALtmmmtofVYOxSJG0S5ZcP2pF.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.8, "NumRating": 860}, + {"MovieId": "13186", "Title": "Wrong Turn 2: Dead End", "Casts": [], "PlotId": "13186", + "ThumbnailIds": ["/ftmiyfrTrqTi5Qod62nGRP9BGPn.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.6, + "NumRating": 528}, {"MovieId": "1949", "Title": "Zodiac", "Casts": [], "PlotId": "1949", + "ThumbnailIds": ["/bgLyOROfFQI3FqYL7jQbiaV8lkN.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 4148}, + {"MovieId": "4922", "Title": "The Curious Case of Benjamin Button", "Casts": [], "PlotId": "4922", + "ThumbnailIds": ["/gjMR102u5hPdIAWX7O2rim8ZWgA.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 6474}, {"MovieId": "297762", "Title": "Wonder Woman", "Casts": [], "PlotId": "297762", + "ThumbnailIds": ["/imekS7f1OuHyUP2LAiTEM0zBzUz.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.3, "NumRating": 12322}, + {"MovieId": "15789", "Title": "A Goofy Movie", "Casts": [], "PlotId": "15789", + "ThumbnailIds": ["/bycmMhO3iIoEDzP768sUjq2RV4T.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 796}, {"MovieId": "1724", "Title": "The Incredible Hulk", "Casts": [], "PlotId": "1724", + "ThumbnailIds": ["/gCQ4e8klADtzoa6bL7XLPnjiUIg.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.2, "NumRating": 6127}, + {"MovieId": "254128", "Title": "San Andreas", "Casts": [], "PlotId": "254128", + "ThumbnailIds": ["/qey0tdcOp9kCDdEZuJ87yE3crSe.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 5035}, {"MovieId": "480530", "Title": "Creed II", "Casts": [], "PlotId": "480530", + "ThumbnailIds": ["/v3QyboWRoA4O9RbcsqH8tJMe8EB.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.7, "NumRating": 2107}, + {"MovieId": "127585", "Title": "X-Men: Days of Future Past", "Casts": [], "PlotId": "127585", + "ThumbnailIds": ["/pb1IURTkK5rImP9ZV83lxJO2us7.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 9884}, {"MovieId": "2320", "Title": "Executive Decision", "Casts": [], "PlotId": "2320", + "ThumbnailIds": ["/wmgkFuEr8bHRAgs1HS5U46Rzo0I.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.1, "NumRating": 387}, + {"MovieId": "574", "Title": "The Man Who Knew Too Much", "Casts": [], "PlotId": "574", + "ThumbnailIds": ["/vhUOukoJTWPfVpZOiKwrjdEV4OX.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 541}, {"MovieId": "68817", "Title": "Footloose", "Casts": [], "PlotId": "68817", + "ThumbnailIds": ["/kDpo6G7rYRHQ1bFhyLiJEW9ESPO.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 910}, + {"MovieId": "141052", "Title": "Justice League", "Casts": [], "PlotId": "141052", + "ThumbnailIds": ["/eifGNCSDuxJeS1loAXil5bIGgvC.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 7362}, {"MovieId": "403119", "Title": "47 Meters Down", "Casts": [], "PlotId": "403119", + "ThumbnailIds": ["/2IgdRUTdHyoI3nFORcnnYEKOGIH.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.6, "NumRating": 1484}, + {"MovieId": "332562", "Title": "A Star Is Born", "Casts": [], "PlotId": "332562", + "ThumbnailIds": ["/wrFpXMNBRj2PBiN4Z5kix51XaIZ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 5321}, {"MovieId": "260513", "Title": "Incredibles 2", "Casts": [], "PlotId": "260513", + "ThumbnailIds": ["/9lFKBtaVIhP7E2Pk0IY1CwTKTMZ.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.6, "NumRating": 6089}, + {"MovieId": "401246", "Title": "The Square", "Casts": [], "PlotId": "401246", + "ThumbnailIds": ["/qYb8hzGGX7uBAVMYW1YFtcFeZhp.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 594}, {"MovieId": "393519", "Title": "Raw", "Casts": [], "PlotId": "393519", + "ThumbnailIds": ["/6kXW9b1FZXvB3l0mLMDbKwGgL3P.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.0, "NumRating": 1421}, + {"MovieId": "241257", "Title": "Regression", "Casts": [], "PlotId": "241257", + "ThumbnailIds": ["/d1dtQQfJJWU31cGSFbfhyeLtsTm.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.5, + "NumRating": 1047}, {"MovieId": "12289", "Title": "Red Cliff", "Casts": [], "PlotId": "12289", + "ThumbnailIds": ["/uMiA2c1wrySRTI3f2ij5i2aCCya.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.2, "NumRating": 354}, + {"MovieId": "181808", "Title": "Star Wars: The Last Jedi", "Casts": [], "PlotId": "181808", + "ThumbnailIds": ["/kOVEVeg59E0wsnXmF9nrh6OmWII.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 8361}, {"MovieId": "399402", "Title": "Hunter Killer", "Casts": [], "PlotId": "399402", + "ThumbnailIds": ["/a0j18XNVhP4RcW3wXwsqT0kVoQm.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.2, "NumRating": 623}, + {"MovieId": "12123", "Title": "Chain Reaction", "Casts": [], "PlotId": "12123", + "ThumbnailIds": ["/62jE7wHYXLMBdYrdpTHpHsAu7NL.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.5, + "NumRating": 444}, + {"MovieId": "1116", "Title": "The Wind That Shakes the Barley", "Casts": [], "PlotId": "1116", + "ThumbnailIds": ["/uLt7SpgPjfwkWqJpUk08xONISFZ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 304}, {"MovieId": "308504", "Title": "Last Knights", "Casts": [], "PlotId": "308504", + "ThumbnailIds": ["/jKcbKy4C9bbwcBWGkMQR70vBNXJ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.3, "NumRating": 523}, + {"MovieId": "19995", "Title": "Avatar", "Casts": [], "PlotId": "19995", + "ThumbnailIds": ["/kmcqlZGaSh20zpTbuoF0Cdn07dT.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 18408}, {"MovieId": "603", "Title": "The Matrix", "Casts": [], "PlotId": "603", + "ThumbnailIds": ["/hEpWvX6Bp79eLxY1kX5ZZJcme5U.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 8.1, "NumRating": 14155}, + {"MovieId": "207768", "Title": "I Spit on Your Grave 2", "Casts": [], "PlotId": "207768", + "ThumbnailIds": ["/pHratkL4ETydC0Kcwgc3tOyvLK0.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 487}, + {"MovieId": "145247", "Title": "The 100 Year-Old Man Who Climbed Out the Window and Disappeared", + "Casts": [], "PlotId": "145247", "ThumbnailIds": ["/cE51BdbaxpOWnfSKlSPNy5rzzFV.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.8, "NumRating": 491}, + {"MovieId": "844", "Title": "2046", "Casts": [], "PlotId": "844", + "ThumbnailIds": ["/orslhtqVBbI1n7RECP7Fkhuodph.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 339}, {"MovieId": "77931", "Title": "The Smurfs 2", "Casts": [], "PlotId": "77931", + "ThumbnailIds": ["/mF2FXzZ3EktWoagU4fzoabNsK6J.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.7, "NumRating": 1186}, + {"MovieId": "320007", "Title": "Victoria", "Casts": [], "PlotId": "320007", + "ThumbnailIds": ["/t66eAg7xBLGoSSoA8JtJtX3NKs1.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.8, + "NumRating": 609}, {"MovieId": "266396", "Title": "The Gunman", "Casts": [], "PlotId": "266396", + "ThumbnailIds": ["/lnUozDnDANTsDYEdsNsHC6b8IiS.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.7, "NumRating": 575}, + {"MovieId": "272", "Title": "Batman Begins", "Casts": [], "PlotId": "272", + "ThumbnailIds": ["/dr6x4GyyegBWtinPBzipY02J2lV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 11969}, {"MovieId": "374473", "Title": "I, Daniel Blake", "Casts": [], "PlotId": "374473", + "ThumbnailIds": ["/jJhqXTsAXCS46NhkYXBM7HDs8z8.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.7, "NumRating": 598}, + {"MovieId": "290751", "Title": "Secret in Their Eyes", "Casts": [], "PlotId": "290751", + "ThumbnailIds": ["/jHaJkzzmxjwXnvNHBFqXd8l4UE4.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 682}, + {"MovieId": "353326", "Title": "The Man Who Knew Infinity", "Casts": [], "PlotId": "353326", + "ThumbnailIds": ["/stXjVMZlY8khhjiJYrq4DdQ5uyV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 685}, {"MovieId": "152780", "Title": "The Past", "Casts": [], "PlotId": "152780", + "ThumbnailIds": ["/kjerwTkZJev6Ve5DS0hRbaOuVJb.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.3, "NumRating": 247}, + {"MovieId": "341006", "Title": "The Belko Experiment", "Casts": [], "PlotId": "341006", + "ThumbnailIds": ["/faJK0dP3S92kQoKtO4LZMjy41kf.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 743}, {"MovieId": "9703", "Title": "The Last Legion", "Casts": [], "PlotId": "9703", + "ThumbnailIds": ["/8K4WWwFew1CzCGVkgmKdamCA6kz.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.4, "NumRating": 393}, + {"MovieId": "316152", "Title": "Free State of Jones", "Casts": [], "PlotId": "316152", + "ThumbnailIds": ["/tQcwB5nXpN4vH5ewP79tyXJcA1I.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 773}, {"MovieId": "97614", "Title": "Deadfall", "Casts": [], "PlotId": "97614", + "ThumbnailIds": ["/kt3bqW8pgbIxJY7aOqcQfUpB8dA.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 352}, + {"MovieId": "278", "Title": "The Shawshank Redemption", "Casts": [], "PlotId": "278", + "ThumbnailIds": ["/9O7gLzmreU0nGkIB6K3BsJbzvNv.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.7, + "NumRating": 12976}, {"MovieId": "351339", "Title": "Anthropoid", "Casts": [], "PlotId": "351339", + "ThumbnailIds": ["/5hUghRVVkBYufftfNQkGevY5AmE.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.0, "NumRating": 504}, + {"MovieId": "353616", "Title": "Pitch Perfect 3", "Casts": [], "PlotId": "353616", + "ThumbnailIds": ["/fchHLsLjFvzAFSQykiMwdF1051.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 1834}, + {"MovieId": "77948", "Title": "The Cold Light of Day", "Casts": [], "PlotId": "77948", + "ThumbnailIds": ["/zXhphNKS56VQbVJXqk3OMrjtNNc.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.1, + "NumRating": 454}, {"MovieId": "1667", "Title": "March of the Penguins", "Casts": [], "PlotId": "1667", + "ThumbnailIds": ["/yjAayG6nY9VO8IJoTLrSLUyu5kD.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.0, "NumRating": 593}, + {"MovieId": "341012", "Title": "Popstar: Never Stop Never Stopping", "Casts": [], "PlotId": "341012", + "ThumbnailIds": ["/m9k2MCvZJWkV5DfJY9fueeeqNxb.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 662}, {"MovieId": "15370", "Title": "The Cat Returns", "Casts": [], "PlotId": "15370", + "ThumbnailIds": ["/57rS5DXN8YfRHgh609RgCKjKbel.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.2, "NumRating": 698}, + {"MovieId": "289222", "Title": "The Zookeeper's Wife", "Casts": [], "PlotId": "289222", + "ThumbnailIds": ["/50KGpMiIvSkF4WHOgp0gM6r6sMU.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 705}, {"MovieId": "420817", "Title": "Aladdin", "Casts": [], "PlotId": "420817", + "ThumbnailIds": ["/3iYQTLGoy7QnjcUYRJy4YrAgGvp.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 0.0, "NumRating": 0}, + {"MovieId": "395458", "Title": "Suburbicon", "Casts": [], "PlotId": "395458", + "ThumbnailIds": ["/a3IHgSwO5jWPLcGjKqbQ7pxVGkq.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 859}, {"MovieId": "54318", "Title": "The Water Horse", "Casts": [], "PlotId": "54318", + "ThumbnailIds": ["/i1DU0Nux6CozY1uEjyz5uCWBxhc.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.4, "NumRating": 518}, + {"MovieId": "27205", "Title": "Inception", "Casts": [], "PlotId": "27205", + "ThumbnailIds": ["/qmDpIHrmpJINaRKAfWQfftjCdyi.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.3, + "NumRating": 21844}, + {"MovieId": "10925", "Title": "The Return of the Living Dead", "Casts": [], "PlotId": "10925", + "ThumbnailIds": ["/1vydUitzC8W8W5oNevEArCltydJ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 465}, {"MovieId": "179826", "Title": "Odd Thomas", "Casts": [], "PlotId": "179826", + "ThumbnailIds": ["/b8FUEQnuig1BpBiWVfybOk3VozN.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 539}, + {"MovieId": "390062", "Title": "Jungle", "Casts": [], "PlotId": "390062", + "ThumbnailIds": ["/tDgxknTVwrScxpCYyGUjXSn5NRk.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 800}, {"MovieId": "680", "Title": "Pulp Fiction", "Casts": [], "PlotId": "680", + "ThumbnailIds": ["/dM2w364MScsjFf8pfMbaWUcWrR.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.4, "NumRating": 14963}, + {"MovieId": "2019", "Title": "Hard Target", "Casts": [], "PlotId": "2019", + "ThumbnailIds": ["/6WEu60V7EzncuFJSVmGJzhFvs4I.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 407}, + {"MovieId": "302349", "Title": "Iron Sky: The Coming Race", "Casts": [], "PlotId": "302349", + "ThumbnailIds": ["/l5t2Nf1F7iQUKTrODg93xmQzZLj.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.8, + "NumRating": 16}, {"MovieId": "280", "Title": "Terminator 2: Judgment Day", "Casts": [], "PlotId": "280", + "ThumbnailIds": ["/2y4dmgWYRMYXdD1UyJVcn2HSd1D.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.9, "NumRating": 6589}, + {"MovieId": "11976", "Title": "Legend", "Casts": [], "PlotId": "11976", + "ThumbnailIds": ["/xNWs9LM46vJWwf1q0VGPOccTKg4.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 496}, {"MovieId": "8966", "Title": "Twilight", "Casts": [], "PlotId": "8966", + "ThumbnailIds": ["/lcMp3AONdNhjYE9MmTtMMTOiRDP.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 7194}, + {"MovieId": "10048", "Title": "Stealth", "Casts": [], "PlotId": "10048", + "ThumbnailIds": ["/tiezxIq6TJO4B09c13Z8a675dDy.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.1, + "NumRating": 492}, {"MovieId": "369192", "Title": "Battle of the Sexes", "Casts": [], "PlotId": "369192", + "ThumbnailIds": ["/fWy0A3VojTCb0S2MKtEJjpquubF.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.9, "NumRating": 1022}, + {"MovieId": "381288", "Title": "Split", "Casts": [], "PlotId": "381288", + "ThumbnailIds": ["/rXMWOZiCt6eMX22jWuTOSdQ98bY.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 10125}, {"MovieId": "111190", "Title": "Adore", "Casts": [], "PlotId": "111190", + "ThumbnailIds": ["/l2OTtTGfSfBRKArvzn14sk3xiNL.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.1, "NumRating": 547}, + {"MovieId": "12103", "Title": "Don't Say a Word", "Casts": [], "PlotId": "12103", + "ThumbnailIds": ["/qx3hgW9MqxsEEFjx4eSbpp1Fe2l.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 402}, {"MovieId": "88273", "Title": "A Royal Affair", "Casts": [], "PlotId": "88273", + "ThumbnailIds": ["/f7bfKSEasjNLJ7I8rS5YJmzSDGr.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 442}, + {"MovieId": "5336", "Title": "Sal\u00f2, or the 120 Days of Sodom", "Casts": [], "PlotId": "5336", + "ThumbnailIds": ["/xnaDdiRfZlJaTf6JRc4in40eaeI.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 658}, {"MovieId": "399361", "Title": "Triple Frontier", "Casts": [], "PlotId": "399361", + "ThumbnailIds": ["/aBw8zYuAljVM1FeK5bZKITPH8ZD.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.2, "NumRating": 992}, + {"MovieId": "11319", "Title": "The Rescuers", "Casts": [], "PlotId": "11319", + "ThumbnailIds": ["/49rGpB2x6AFB83SC4IBl9foRIGp.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 1264}, {"MovieId": "335984", "Title": "Blade Runner 2049", "Casts": [], "PlotId": "335984", + "ThumbnailIds": ["/gajva2L0rPYkEWjzgFlBXCAVBE5.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.4, "NumRating": 6549}, + {"MovieId": "314405", "Title": "Tale of Tales", "Casts": [], "PlotId": "314405", + "ThumbnailIds": ["/9HKUonyvlNvW74rxX9XwbiWCjQV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 827}, {"MovieId": "260346", "Title": "Taken 3", "Casts": [], "PlotId": "260346", + "ThumbnailIds": ["/ikDwR3i2bczqnRf1urJTy77YTFf.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.1, "NumRating": 3360}, + {"MovieId": "10199", "Title": "The Tale of Despereaux", "Casts": [], "PlotId": "10199", + "ThumbnailIds": ["/8Nge4rXAQzU5w9U8OvnXuuJltL9.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 454}, + {"MovieId": "338189", "Title": "It's Only the End of the World", "Casts": [], "PlotId": "338189", + "ThumbnailIds": ["/xTqy97V5EYtnMWWo2wVpScanpDS.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 845}, {"MovieId": "182560", "Title": "Dark Places", "Casts": [], "PlotId": "182560", + "ThumbnailIds": ["/1z7Bxnxi1lgO0ksc6peI4UssEPf.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 747}, + {"MovieId": "574241", "Title": "Poms", "Casts": [], "PlotId": "574241", + "ThumbnailIds": ["/m2ksKdmWIoUh3GJfu9oEBLorjAJ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.5, + "NumRating": 1}, + {"MovieId": "438674", "Title": "Dragged Across Concrete", "Casts": [], "PlotId": "438674", + "ThumbnailIds": ["/fVG4a27ImyPS4vvNMjCtan3QhDl.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 95}, {"MovieId": "467660", "Title": "Unsane", "Casts": [], "PlotId": "467660", + "ThumbnailIds": ["/jvDBfavZASdKsJunu9VCAtXjLS2.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.1, "NumRating": 632}, + {"MovieId": "2749", "Title": "15 Minutes", "Casts": [], "PlotId": "2749", + "ThumbnailIds": ["/qqqleHV8y7NLKt7isSZAvOwVPH6.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.7, + "NumRating": 321}, + {"MovieId": "123025", "Title": "Batman: The Dark Knight Returns, Part 1", "Casts": [], "PlotId": "123025", + "ThumbnailIds": ["/mFPD2YsdaWAzjuxF7ItGmsEFpdY.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.7, + "NumRating": 691}, {"MovieId": "38543", "Title": "Ironclad", "Casts": [], "PlotId": "38543", + "ThumbnailIds": ["/kxAkA3cL0pSiGlW1JeiGHJr5Jv2.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.1, "NumRating": 366}, + {"MovieId": "11045", "Title": "Taxi", "Casts": [], "PlotId": "11045", + "ThumbnailIds": ["/7Ly55SeRBozH3ZBeSOodcgKGYlF.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.9, + "NumRating": 434}, {"MovieId": "3093", "Title": "Basic Instinct 2", "Casts": [], "PlotId": "3093", + "ThumbnailIds": ["/gXzrKzHnMmfWCF5PMYQfNOzCxYl.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 4.7, "NumRating": 319}, + {"MovieId": "315664", "Title": "Florence Foster Jenkins", "Casts": [], "PlotId": "315664", + "ThumbnailIds": ["/aBjQeNw7JBpXWGFaPYcGNlecRyr.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 837}, {"MovieId": "210577", "Title": "Gone Girl", "Casts": [], "PlotId": "210577", + "ThumbnailIds": ["/gdiLTof3rbPDAmPaCf4g6op46bj.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.9, "NumRating": 10253}, + {"MovieId": "397422", "Title": "Rough Night", "Casts": [], "PlotId": "397422", + "ThumbnailIds": ["/i66xbL1C6FEWDm2KoX11DHmP4Rz.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.5, + "NumRating": 1053}, {"MovieId": "424", "Title": "Schindler's List", "Casts": [], "PlotId": "424", + "ThumbnailIds": ["/yPisjyLweCl1tbgwgtzBCNCBle.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.5, "NumRating": 7885}, + {"MovieId": "218778", "Title": "Alexander and the Terrible, Horrible, No Good, Very Bad Day", "Casts": [], + "PlotId": "218778", "ThumbnailIds": ["/f29NQJaWyzXldOGT7F5CDKbwAH9.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.1, "NumRating": 801}, + {"MovieId": "9994", "Title": "The Great Mouse Detective", "Casts": [], "PlotId": "9994", + "ThumbnailIds": ["/9uDr7vfjCFr39KGCcqrk44Cg7fQ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 746}, {"MovieId": "10320", "Title": "The Ring Two", "Casts": [], "PlotId": "10320", + "ThumbnailIds": ["/mYZdcd5XeZkye72oQejkLuAGI7q.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.6, "NumRating": 1219}, + {"MovieId": "374475", "Title": "Toni Erdmann", "Casts": [], "PlotId": "374475", + "ThumbnailIds": ["/gJNjVE8WGUjiSKUtMDEvNzxR5zq.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.1, + "NumRating": 479}, {"MovieId": "830", "Title": "Forbidden Planet", "Casts": [], "PlotId": "830", + "ThumbnailIds": ["/qdH1Fm5vl1DAjXNHPmRZ4yEiUgM.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.3, "NumRating": 376}, + {"MovieId": "243940", "Title": "The Lazarus Effect", "Casts": [], "PlotId": "243940", + "ThumbnailIds": ["/3oqjCN38pb7MI5i8lUlDQd4IAeA.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.1, + "NumRating": 809}, + {"MovieId": "9662", "Title": "The Triplets of Belleville", "Casts": [], "PlotId": "9662", + "ThumbnailIds": ["/A8aSp63Xi4cnip3wAhk7Jml1uWG.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 437}, {"MovieId": "41211", "Title": "Heartbreaker", "Casts": [], "PlotId": "41211", + "ThumbnailIds": ["/fECaEIjjvozG1dGoi6cgTevIae5.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 743}, + {"MovieId": "515248", "Title": "Someone Great", "Casts": [], "PlotId": "515248", + "ThumbnailIds": ["/h0nz5lIBXeUZChBNfwL08bLWQaU.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 328}, {"MovieId": "177494", "Title": "Veronica Mars", "Casts": [], "PlotId": "177494", + "ThumbnailIds": ["/nS3L07mQfcNJcisLEKgi8fWoBS1.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 695}, + {"MovieId": "102780", "Title": "Byzantium", "Casts": [], "PlotId": "102780", + "ThumbnailIds": ["/pSnlpdh27luQ6GcchSkPomZ5fyr.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 465}, {"MovieId": "155084", "Title": "13 Sins", "Casts": [], "PlotId": "155084", + "ThumbnailIds": ["/uYPy89cVgvE1ic0bN39YJrumxwo.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.3, "NumRating": 561}, + {"MovieId": "353081", "Title": "Mission: Impossible - Fallout", "Casts": [], "PlotId": "353081", + "ThumbnailIds": ["/AkJQpZp9WoNdj7pLYSj1L0RcMMN.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 3664}, + {"MovieId": "250734", "Title": "Far from the Madding Crowd", "Casts": [], "PlotId": "250734", + "ThumbnailIds": ["/6v1DEaO2TZzrqcaGohUf1K7BXkU.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 578}, {"MovieId": "471859", "Title": "Charlie Says", "Casts": [], "PlotId": "471859", + "ThumbnailIds": ["/nwYDXLQpPULdUbwkTc4gWpMJufn.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 4.0, "NumRating": 5}, + {"MovieId": "252178", "Title": "'71", "Casts": [], "PlotId": "252178", + "ThumbnailIds": ["/b8dmfG84peFdouN2N8wOsiI9WHt.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 617}, {"MovieId": "16608", "Title": "The Proposition", "Casts": [], "PlotId": "16608", + "ThumbnailIds": ["/e4j7s9SzaJTfjkorMX1iY38IzZi.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.2, "NumRating": 327}, + {"MovieId": "127560", "Title": "The Railway Man", "Casts": [], "PlotId": "127560", + "ThumbnailIds": ["/3iU9KLIhvc3pdl1SkeD0gv4BO0f.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 400}, {"MovieId": "428449", "Title": "A Ghost Story", "Casts": [], "PlotId": "428449", + "ThumbnailIds": ["/rp5JPIyZi9sMob15l46zNQLe5cO.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.0, "NumRating": 879}, + {"MovieId": "330483", "Title": "The Choice", "Casts": [], "PlotId": "330483", + "ThumbnailIds": ["/7EenqQdtZjOeBAAlxNtnG1eLHnf.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 1022}, + {"MovieId": "10131", "Title": "A Nightmare on Elm Street 4: The Dream Master", "Casts": [], + "PlotId": "10131", "ThumbnailIds": ["/gERdob60xVv0tJ1Kr3XEqqj0KFl.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.8, "NumRating": 556}, + {"MovieId": "10497", "Title": "Bitter Moon", "Casts": [], "PlotId": "10497", + "ThumbnailIds": ["/sUiqWj0k5oPcThd1WaXvt9Vymzr.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 231}, {"MovieId": "329865", "Title": "Arrival", "Casts": [], "PlotId": "329865", + "ThumbnailIds": ["/hLudzvGfpi6JlwUnsNhXwKKg4j.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 10225}, + {"MovieId": "55301", "Title": "Alvin and the Chipmunks: Chipwrecked", "Casts": [], "PlotId": "55301", + "ThumbnailIds": ["/s1nHXSJTq45RUHn7RmW6fO7kHu9.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.5, + "NumRating": 1028}, {"MovieId": "375315", "Title": "The Salesman", "Casts": [], "PlotId": "375315", + "ThumbnailIds": ["/sLqw0NXQJY8S8Na94rlefavBnRX.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.3, "NumRating": 380}, + {"MovieId": "9425", "Title": "Soldier", "Casts": [], "PlotId": "9425", + "ThumbnailIds": ["/lpByPXb2xJvsmeEBxO9YPADLLdi.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 326}, + {"MovieId": "121", "Title": "The Lord of the Rings: The Two Towers", "Casts": [], "PlotId": "121", + "ThumbnailIds": ["/5VTN0pR8gcqV3EPUHHfMGnJYN9L.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.3, + "NumRating": 12288}, + {"MovieId": "400535", "Title": "Sicario: Day of the Soldado", "Casts": [], "PlotId": "400535", + "ThumbnailIds": ["/msqWSQkU403cQKjQHnWLnugv7EY.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 1261}, + {"MovieId": "498248", "Title": "Mia and the White Lion", "Casts": [], "PlotId": "498248", + "ThumbnailIds": ["/zXfwBeYe0RnqsJ0dnBoFTB4SSrG.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.7, + "NumRating": 134}, {"MovieId": "429197", "Title": "Vice", "Casts": [], "PlotId": "429197", + "ThumbnailIds": ["/1gCab6rNv1r6V64cwsU4oEr649Y.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 1095}, + {"MovieId": "12444", "Title": "Harry Potter and the Deathly Hallows: Part 1", "Casts": [], + "PlotId": "12444", "ThumbnailIds": ["/maP4MTfPCeVD2FZbKTLUgriOW4R.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.7, "NumRating": 10591}, + {"MovieId": "269149", "Title": "Zootopia", "Casts": [], "PlotId": "269149", + "ThumbnailIds": ["/sM33SANp9z6rXW8Itn7NnG1GOEs.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.7, + "NumRating": 9815}, {"MovieId": "22787", "Title": "Whiteout", "Casts": [], "PlotId": "22787", + "ThumbnailIds": ["/oC28DSfylqtkf8AWkr0cVj7TcjS.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.4, "NumRating": 348}, + {"MovieId": "1930", "Title": "The Amazing Spider-Man", "Casts": [], "PlotId": "1930", + "ThumbnailIds": ["/eA2D86Y6VPWuUzZyatiLBwpTilQ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 10242}, {"MovieId": "9021", "Title": "The Santa Clause 2", "Casts": [], "PlotId": "9021", + "ThumbnailIds": ["/i7tbiDPIaa4VsQh1wWmbkY4zTRX.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.6, "NumRating": 609}, + {"MovieId": "527641", "Title": "Five Feet Apart", "Casts": [], "PlotId": "527641", + "ThumbnailIds": ["/kreTuJBkUjVWePRfhHZuYfhNE1T.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.2, + "NumRating": 470}, + {"MovieId": "140607", "Title": "Star Wars: The Force Awakens", "Casts": [], "PlotId": "140607", + "ThumbnailIds": ["/weUSwMdQIa3NaXVzwUoIIcAi85d.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 12473}, {"MovieId": "8342", "Title": "No Man's Land", "Casts": [], "PlotId": "8342", + "ThumbnailIds": ["/8DvLzDG9OeDzKdXZjRkCoWfXUcc.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.7, "NumRating": 184}, + {"MovieId": "244786", "Title": "Whiplash", "Casts": [], "PlotId": "244786", + "ThumbnailIds": ["/lIv1QinFqz4dlp5U4lQ6HaiskOZ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.4, + "NumRating": 7750}, + {"MovieId": "243938", "Title": "Hot Tub Time Machine 2", "Casts": [], "PlotId": "243938", + "ThumbnailIds": ["/tQtWuwvMf0hCc2QR2tkolwl7c3c.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.1, + "NumRating": 498}, {"MovieId": "419479", "Title": "The Babysitter", "Casts": [], "PlotId": "419479", + "ThumbnailIds": ["/86a7GRVRCwfl7wdI4QadyvKa6Zu.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 1519}, + {"MovieId": "82390", "Title": "The Paperboy", "Casts": [], "PlotId": "82390", + "ThumbnailIds": ["/foYlHgTIT8PFOeHOFxBICDZmYrm.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.5, + "NumRating": 398}, {"MovieId": "198663", "Title": "The Maze Runner", "Casts": [], "PlotId": "198663", + "ThumbnailIds": ["/coss7RgL0NH6g4fC2s5atvf3dFO.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 10483}, + {"MovieId": "11470", "Title": "Jason X", "Casts": [], "PlotId": "11470", + "ThumbnailIds": ["/zdfRa0FLVxfRwWhwitIZhjEIepI.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.5, + "NumRating": 525}, + {"MovieId": "675", "Title": "Harry Potter and the Order of the Phoenix", "Casts": [], "PlotId": "675", + "ThumbnailIds": ["/4YnLxYLHhT4UQ8i9jxAXWy46Xuw.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 10605}, {"MovieId": "11050", "Title": "Terms of Endearment", "Casts": [], "PlotId": "11050", + "ThumbnailIds": ["/iZiYzBb6nyv2anbpVTV5ba6GSwq.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.1, "NumRating": 320}, + {"MovieId": "109445", "Title": "Frozen", "Casts": [], "PlotId": "109445", + "ThumbnailIds": ["/eFnGmj63QPUpK7QUWSOUhypIQOT.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 9577}, + {"MovieId": "11549", "Title": "Invasion of the Body Snatchers", "Casts": [], "PlotId": "11549", + "ThumbnailIds": ["/4oj0G2Zr6tQKBDOzKaLuefdeBsG.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 406}, {"MovieId": "38541", "Title": "The Divide", "Casts": [], "PlotId": "38541", + "ThumbnailIds": ["/3AdJMWYMjZBED2DLfBiOq7Rfw03.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 300}, + {"MovieId": "398175", "Title": "Brawl in Cell Block 99", "Casts": [], "PlotId": "398175", + "ThumbnailIds": ["/bfB1J6jsjdGWKjXxKQ5hNd1OyAs.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 489}, {"MovieId": "77663", "Title": "Killing Season", "Casts": [], "PlotId": "77663", + "ThumbnailIds": ["/ftZVLKyr3RDGtVelU06GAbtOFQa.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.5, "NumRating": 571}, + {"MovieId": "673", "Title": "Harry Potter and the Prisoner of Azkaban", "Casts": [], "PlotId": "673", + "ThumbnailIds": ["/jUFjMoLh8T2CWzHUSjKCojI5SHu.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.9, + "NumRating": 11378}, {"MovieId": "332340", "Title": "Man Up", "Casts": [], "PlotId": "332340", + "ThumbnailIds": ["/y7C1EQ9zxJ3mlaQeRztw3NVw41P.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.6, "NumRating": 534}, + {"MovieId": "458253", "Title": "Missing Link", "Casts": [], "PlotId": "458253", + "ThumbnailIds": ["/gEkKHiiQRVUSX15Iwo8VFydXrtu.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 39}, {"MovieId": "9725", "Title": "Friday the 13th Part 2", "Casts": [], "PlotId": "9725", + "ThumbnailIds": ["/92rGctBMTv4uaSlIBVnhz01kRWL.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 618}, + {"MovieId": "265189", "Title": "Force Majeure", "Casts": [], "PlotId": "265189", + "ThumbnailIds": ["/rGMtc9AtZsnWSSL5VnLaGvx1PI6.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 409}, + {"MovieId": "101", "Title": "L\u00e9on: The Professional", "Casts": [], "PlotId": "101", + "ThumbnailIds": ["/gE8S02QUOhVnAmYu4tcrBlMTujz.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.3, + "NumRating": 7509}, {"MovieId": "233063", "Title": "Suck Me Shakespeer", "Casts": [], "PlotId": "233063", + "ThumbnailIds": ["/9GSGg4McYID6Ld0OMwimLr1Zx4J.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.1, "NumRating": 1051}, + {"MovieId": "1497", "Title": "Teenage Mutant Ninja Turtles II: The Secret of the Ooze", "Casts": [], + "PlotId": "1497", "ThumbnailIds": ["/HD4LtUw4Ono9tzgwWOhHpHcjkj.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 460}, + {"MovieId": "309886", "Title": "Blood Father", "Casts": [], "PlotId": "309886", + "ThumbnailIds": ["/rQ3CKv33u2Z4keC2AYZqi3RGIdX.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 820}, {"MovieId": "31442", "Title": "Ivan's Childhood", "Casts": [], "PlotId": "31442", + "ThumbnailIds": ["/vmRWSLP1DE9WTta0hfzIafJ0dID.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.0, "NumRating": 221}, + {"MovieId": "198375", "Title": "The Garden of Words", "Casts": [], "PlotId": "198375", + "ThumbnailIds": ["/f1Mhgu0sxvXEcUYDH4yVWdNh10j.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 830}, {"MovieId": "11497", "Title": "All Dogs Go to Heaven", "Casts": [], "PlotId": "11497", + "ThumbnailIds": ["/nmWh1NglDinfkHD9zCNqGWyhl7Q.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 468}, + {"MovieId": "49051", "Title": "The Hobbit: An Unexpected Journey", "Casts": [], "PlotId": "49051", + "ThumbnailIds": ["/ysX7vDmSh5O19vFjAi56WL7l4nk.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 11926}, {"MovieId": "243683", "Title": "Step Up All In", "Casts": [], "PlotId": "243683", + "ThumbnailIds": ["/jFkvtmhE4NqENI4e0sZxmI8vrSS.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.8, "NumRating": 1187}, + {"MovieId": "11596", "Title": "New Nightmare", "Casts": [], "PlotId": "11596", + "ThumbnailIds": ["/oLEZibnraixTE68rTaYv4FEmvYd.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 554}, {"MovieId": "223485", "Title": "Slow West", "Casts": [], "PlotId": "223485", + "ThumbnailIds": ["/uZ9dBBn0GaEw0YIBvQd01Io3pho.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 529}, + {"MovieId": "524247", "Title": "The Intruder", "Casts": [], "PlotId": "524247", + "ThumbnailIds": ["/p9xKyetr0ihJ2K6HJMeXzc4IwEv.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.0, + "NumRating": 7}, {"MovieId": "453755", "Title": "Arctic", "Casts": [], "PlotId": "453755", + "ThumbnailIds": ["/dxRc0Y4IjSRLC2fYaguXQtJCI4e.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.2, "NumRating": 159}, + {"MovieId": "6687", "Title": "Transsiberian", "Casts": [], "PlotId": "6687", + "ThumbnailIds": ["/izYDkKxj9sI8KNKK0PR54dpuiFu.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 320}, {"MovieId": "10523", "Title": "W.", "Casts": [], "PlotId": "10523", + "ThumbnailIds": ["/upFQuRzN23kLvzO1brSFClLqZvV.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 238}, + {"MovieId": "12246", "Title": "Mongol: The Rise of Genghis Khan", "Casts": [], "PlotId": "12246", + "ThumbnailIds": ["/8dlyyuwSJtS0VOLM9xi7SA7xbfU.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 271}, {"MovieId": "76544", "Title": "Man of Tai Chi", "Casts": [], "PlotId": "76544", + "ThumbnailIds": ["/42MutxsgeJS5pYLNWUGv8d6B9wa.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 422}, + {"MovieId": "93828", "Title": "Welcome to the Punch", "Casts": [], "PlotId": "93828", + "ThumbnailIds": ["/eJwSM618opISwPkzia9fNeFvW4Z.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 362}, + {"MovieId": "227348", "Title": "Paranormal Activity: The Marked Ones", "Casts": [], "PlotId": "227348", + "ThumbnailIds": ["/fMC6CqCRDzQDxAibydeZF9XBAJ4.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.2, + "NumRating": 737}, {"MovieId": "205596", "Title": "The Imitation Game", "Casts": [], "PlotId": "205596", + "ThumbnailIds": ["/noUp0XOqIcmgefRnRZa1nhtRvWO.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.1, "NumRating": 10218}, + {"MovieId": "10587", "Title": "Police Academy 4: Citizens on Patrol", "Casts": [], "PlotId": "10587", + "ThumbnailIds": ["/AcvfIPLXGxSucYGSmfkRU2SqLi4.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.3, + "NumRating": 486}, {"MovieId": "228150", "Title": "Fury", "Casts": [], "PlotId": "228150", + "ThumbnailIds": ["/pfte7wdMobMF4CVHuOxyu6oqeeA.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 6541}, + {"MovieId": "5902", "Title": "A Bridge Too Far", "Casts": [], "PlotId": "5902", + "ThumbnailIds": ["/ormdFwSHGVgsWdrzP5pRPaA6nme.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 329}, {"MovieId": "293310", "Title": "Citizenfour", "Casts": [], "PlotId": "293310", + "ThumbnailIds": ["/yNuLhb2y6I5cO7BfiJ7bdfllnIG.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.9, "NumRating": 700}, + {"MovieId": "110420", "Title": "Wolf Children", "Casts": [], "PlotId": "110420", + "ThumbnailIds": ["/rDMxjCYEVnvLC4nsBpB6wjL0LDy.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.2, + "NumRating": 1024}, {"MovieId": "9962", "Title": "The Good Girl", "Casts": [], "PlotId": "9962", + "ThumbnailIds": ["/21AacCArwM5hQ8fYPRA0WKZviiN.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.0, "NumRating": 285}, + {"MovieId": "466282", "Title": "To All the Boys I've Loved Before", "Casts": [], "PlotId": "466282", + "ThumbnailIds": ["/hKHZhUbIyUAjcSrqJThFGYIR6kI.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.9, + "NumRating": 4317}, {"MovieId": "439079", "Title": "The Nun", "Casts": [], "PlotId": "439079", + "ThumbnailIds": ["/sFC1ElvoKGdHJIWRpNB3xWJ9lJA.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.6, "NumRating": 2695}, + {"MovieId": "246080", "Title": "Black Sea", "Casts": [], "PlotId": "246080", + "ThumbnailIds": ["/ghac5aKzS0tsmBhXXAWURcEEVk0.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 615}, {"MovieId": "535", "Title": "Flashdance", "Casts": [], "PlotId": "535", + "ThumbnailIds": ["/jwsnPftPybX2pTDbvWbyqviKO7S.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.4, "NumRating": 626}, + {"MovieId": "11046", "Title": "Where Eagles Dare", "Casts": [], "PlotId": "11046", + "ThumbnailIds": ["/991u0tSWGTVXCYkgC6ftaGE4bkQ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 372}, {"MovieId": "10479", "Title": "Rules of Engagement", "Casts": [], "PlotId": "10479", + "ThumbnailIds": ["/1UlrMyCza2xINltCyYgmAwQAx07.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.1, "NumRating": 276}, + {"MovieId": "526", "Title": "Ladyhawke", "Casts": [], "PlotId": "526", + "ThumbnailIds": ["/51RFCKLFuEbvLQsFzXcupQnkoRD.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 535}, {"MovieId": "345940", "Title": "The Meg", "Casts": [], "PlotId": "345940", + "ThumbnailIds": ["/eyWICPcxOuTcDDDbTMOZawoOn8d.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 2764}, + {"MovieId": "82702", "Title": "How to Train Your Dragon 2", "Casts": [], "PlotId": "82702", + "ThumbnailIds": ["/lRjOR4uclMQijUav4OjeZprlehu.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.7, + "NumRating": 5471}, + {"MovieId": "767", "Title": "Harry Potter and the Half-Blood Prince", "Casts": [], "PlotId": "767", + "ThumbnailIds": ["/bFXys2nhALwDvpkF3dP3Vvdfn8b.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 10346}, {"MovieId": "8881", "Title": "Che: Part One", "Casts": [], "PlotId": "8881", + "ThumbnailIds": ["/eayI8CIR8miw3rDVLEwoqTL0TPt.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.8, "NumRating": 356}, + {"MovieId": "157845", "Title": "The Rover", "Casts": [], "PlotId": "157845", + "ThumbnailIds": ["/ugVbrbicJHfOw8e24RUYISKqgvf.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 427}, + {"MovieId": "503129", "Title": "Student of the Year 2", "Casts": [], "PlotId": "503129", + "ThumbnailIds": ["/ij8au3Sw1iw6AhroZ0AYYsCra51.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 3.0, + "NumRating": 3}, {"MovieId": "471507", "Title": "Destroyer", "Casts": [], "PlotId": "471507", + "ThumbnailIds": ["/sHw9gTdo43nJL82py0oaROkXXNr.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.8, "NumRating": 131}, + {"MovieId": "150117", "Title": "I Give It a Year", "Casts": [], "PlotId": "150117", + "ThumbnailIds": ["/d05UThmFSAUD6FTHNjYxhrmdTec.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.4, + "NumRating": 483}, {"MovieId": "221902", "Title": "Two Days, One Night", "Casts": [], "PlotId": "221902", + "ThumbnailIds": ["/1mYAejpMskvskGr0J0SaBvdjmrH.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.9, "NumRating": 506}, + {"MovieId": "433808", "Title": "The Ritual", "Casts": [], "PlotId": "433808", + "ThumbnailIds": ["/hHuJqzby593lmYmw1SzT0XYy99t.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 1054}, {"MovieId": "153158", "Title": "Underdogs", "Casts": [], "PlotId": "153158", + "ThumbnailIds": ["/rKnfNwbRGd6p7SidQYlGemCE8wb.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.1, "NumRating": 190}, + {"MovieId": "209274", "Title": "Ida", "Casts": [], "PlotId": "209274", + "ThumbnailIds": ["/aO8dgaxaY1tcAdtFkLuz1WSxgdd.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 448}, + {"MovieId": "58", "Title": "Pirates of the Caribbean: Dead Man's Chest", "Casts": [], "PlotId": "58", + "ThumbnailIds": ["/waFr5RVKaQ9dzOt3nQuIVB1FiPu.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 9232}, {"MovieId": "10972", "Title": "Session 9", "Casts": [], "PlotId": "10972", + "ThumbnailIds": ["/mWwrWhPHiHSoCWUkYcq4GplRF6V.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.2, "NumRating": 412}, + {"MovieId": "168705", "Title": "BloodRayne", "Casts": [], "PlotId": "168705", + "ThumbnailIds": ["/qCrvpGxWjOh7tVLxyqWLR65ZuWX.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 3.8, + "NumRating": 207}, + {"MovieId": "382322", "Title": "Batman: The Killing Joke", "Casts": [], "PlotId": "382322", + "ThumbnailIds": ["/zm0ODjtfJfJW0W269LqsQl5OhJ8.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 860}, {"MovieId": "3035", "Title": "Frankenstein", "Casts": [], "PlotId": "3035", + "ThumbnailIds": ["/wB58wlBAr6484Wm6VyFDqSD8VOJ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.5, "NumRating": 591}, + {"MovieId": "245916", "Title": "Kill the Messenger", "Casts": [], "PlotId": "245916", + "ThumbnailIds": ["/8gaNZiKZHvKCqMDByY00dUIV0YC.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 502}, {"MovieId": "84287", "Title": "The Imposter", "Casts": [], "PlotId": "84287", + "ThumbnailIds": ["/aj6kq3QskRkC1qJyau8ukB478Hw.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 394}, + {"MovieId": "300681", "Title": "Replicas", "Casts": [], "PlotId": "300681", + "ThumbnailIds": ["/hhPBTAn9b4TYOxc1JYNsX4BFAlW.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.7, + "NumRating": 239}, {"MovieId": "157851", "Title": "Maps to the Stars", "Casts": [], "PlotId": "157851", + "ThumbnailIds": ["/ciIFIMR78V9BMfLEylJGSwnlwm.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 599}, + {"MovieId": "2614", "Title": "Innerspace", "Casts": [], "PlotId": "2614", + "ThumbnailIds": ["/krRl7QKdIYsVUPYmOwElA1sqQE7.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 557}, {"MovieId": "12233", "Title": "Oliver & Company", "Casts": [], "PlotId": "12233", + "ThumbnailIds": ["/m54pXsIUy3IJoaGhk3RwKsZQPG8.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 759}, + {"MovieId": "11128", "Title": "Ladder 49", "Casts": [], "PlotId": "11128", + "ThumbnailIds": ["/opgPMSdkXsTOB5W169VlivKyaZQ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 348}, + {"MovieId": "408", "Title": "Snow White and the Seven Dwarfs", "Casts": [], "PlotId": "408", + "ThumbnailIds": ["/wbVGRBYPRRahIZNGXY9TfHDUSc2.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 4036}, + {"MovieId": "2312", "Title": "In the Name of the King: A Dungeon Siege Tale", "Casts": [], + "PlotId": "2312", "ThumbnailIds": ["/bbN1lmDk1PT0GsTFCy179sk5nIF.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 4.4, "NumRating": 369}, + {"MovieId": "74", "Title": "War of the Worlds", "Casts": [], "PlotId": "74", + "ThumbnailIds": ["/xXMM9KY2eq1SDOQif9zO91YOBA8.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 4330}, {"MovieId": "6522", "Title": "Life", "Casts": [], "PlotId": "6522", + "ThumbnailIds": ["/c9phL8FXuL7i15CrjQH5PfM60NB.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.5, "NumRating": 313}, + {"MovieId": "11186", "Title": "Child's Play 2", "Casts": [], "PlotId": "11186", + "ThumbnailIds": ["/xy0qUntbDOVgiTDvSIFlSM3MMzP.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.9, + "NumRating": 663}, {"MovieId": "2322", "Title": "Sneakers", "Casts": [], "PlotId": "2322", + "ThumbnailIds": ["/pAGKtPF8nMiJq4lsINOYJWvMS2D.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.9, "NumRating": 440}, + {"MovieId": "550", "Title": "Fight Club", "Casts": [], "PlotId": "550", + "ThumbnailIds": ["/adw6Lq9FiC9zjYEpOqfq03ituwp.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.4, + "NumRating": 16048}, + {"MovieId": "10191", "Title": "How to Train Your Dragon", "Casts": [], "PlotId": "10191", + "ThumbnailIds": ["/ygGmAO60t8GyqUo9xYeYxSZAR3b.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.7, + "NumRating": 7446}, {"MovieId": "1534", "Title": "Pathfinder", "Casts": [], "PlotId": "1534", + "ThumbnailIds": ["/y3OvDjTiq7LdM6nDlZQbuEAFKou.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.4, "NumRating": 294}, + {"MovieId": "454652", "Title": "Colette", "Casts": [], "PlotId": "454652", + "ThumbnailIds": ["/pGiUIkcTOEn2CwE5CUBFxWkcyxO.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.1, + "NumRating": 293}, {"MovieId": "157847", "Title": "Joe", "Casts": [], "PlotId": "157847", + "ThumbnailIds": ["/bvcLnoffCm67n7pZPVdJ6pluJsi.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 504}, + {"MovieId": "13851", "Title": "Batman: Gotham Knight", "Casts": [], "PlotId": "13851", + "ThumbnailIds": ["/eIAhXROHG8t3QQ7qU0HfZgL5XFf.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 270}, {"MovieId": "5550", "Title": "RoboCop 3", "Casts": [], "PlotId": "5550", + "ThumbnailIds": ["/1YSXqKHjMAutWpIat9AOTIgozDb.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 4.5, "NumRating": 508}, + {"MovieId": "11230", "Title": "Drunken Master", "Casts": [], "PlotId": "11230", + "ThumbnailIds": ["/fWrUTM8jeiU6G6rA4NKwkeigxGa.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 319}, + {"MovieId": "9513", "Title": "Garfield: A Tail of Two Kitties", "Casts": [], "PlotId": "9513", + "ThumbnailIds": ["/aagx3t2Xv7R26hcqzrayTT28Yww.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.3, + "NumRating": 988}, {"MovieId": "571786", "Title": "Miss & Mrs. Cops", "Casts": [], "PlotId": "571786", + "ThumbnailIds": ["/v1sfULur2KiJIPGiXjGTDWNWxG.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 0.0, "NumRating": 0}, + {"MovieId": "790", "Title": "The Fog", "Casts": [], "PlotId": "790", + "ThumbnailIds": ["/u9wAIHya0BHJvGJh3mBPr010U7C.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 572}, {"MovieId": "245698", "Title": "Pawn Sacrifice", "Casts": [], "PlotId": "245698", + "ThumbnailIds": ["/mxdpBuSMqql2Uvv27NIV1pahcsW.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 438}, + {"MovieId": "345887", "Title": "The Equalizer 2", "Casts": [], "PlotId": "345887", + "ThumbnailIds": ["/cQvc9N6JiMVKqol3wcYrGshsIdZ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 1987}, + {"MovieId": "12207", "Title": "The Legend of Drunken Master", "Casts": [], "PlotId": "12207", + "ThumbnailIds": ["/zZODgJHFtzs6wwa4cp6Nezb5AGe.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 373}, + {"MovieId": "26123", "Title": "American Pie Presents: The Book of Love", "Casts": [], "PlotId": "26123", + "ThumbnailIds": ["/hwP0GEP0zy8ar965Xaht19SmMd3.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.1, + "NumRating": 900}, + {"MovieId": "172", "Title": "Star Trek V: The Final Frontier", "Casts": [], "PlotId": "172", + "ThumbnailIds": ["/kugwPq2E5IkzrgoxRycnoqqUS9H.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.7, + "NumRating": 572}, {"MovieId": "36647", "Title": "Blade", "Casts": [], "PlotId": "36647", + "ThumbnailIds": ["/r0RQ9ZOEZglLOeYDNJTehVTRoR6.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 3083}, + {"MovieId": "226486", "Title": "Tammy", "Casts": [], "PlotId": "226486", + "ThumbnailIds": ["/cq6wvDqETqJXgpQplkL0FBw2leM.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.2, + "NumRating": 719}, {"MovieId": "11967", "Title": "Young Guns", "Casts": [], "PlotId": "11967", + "ThumbnailIds": ["/wkGbwWHIM23BkmMBm7GQHaCZan8.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 405}, + {"MovieId": "130150", "Title": "Labor Day", "Casts": [], "PlotId": "130150", + "ThumbnailIds": ["/4TKMiTqZnG6KJWjmxy2ZCDuG3M0.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 531}, + {"MovieId": "12118", "Title": "Police Academy 3: Back in Training", "Casts": [], "PlotId": "12118", + "ThumbnailIds": ["/r4j14ZjmWfcQbboG76gxloqa3N3.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.7, + "NumRating": 492}, + {"MovieId": "9064", "Title": "Hellbound: Hellraiser II", "Casts": [], "PlotId": "9064", + "ThumbnailIds": ["/ryuo4unQkzcEKhfAfzhbQo8oxVm.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 398}, {"MovieId": "1683", "Title": "The Reaping", "Casts": [], "PlotId": "1683", + "ThumbnailIds": ["/wwpI649fFYWaOhit5O1WLsFuHOI.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.5, "NumRating": 375}, + {"MovieId": "517166", "Title": "The Axiom", "Casts": [], "PlotId": "517166", + "ThumbnailIds": ["/6lxM6WEXxJUFOsnHbWPIYnwiT0c.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 4}, {"MovieId": "11826", "Title": "Sexy Beast", "Casts": [], "PlotId": "11826", + "ThumbnailIds": ["/rpo9njGpJVLPqRrdM6R7wIqiQ7K.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.0, "NumRating": 354}, + {"MovieId": "318846", "Title": "The Big Short", "Casts": [], "PlotId": "318846", + "ThumbnailIds": ["/p11Ftd4VposrAzthkhF53ifYZRl.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 4562}, + {"MovieId": "74997", "Title": "The Human Centipede 2 (Full Sequence)", "Casts": [], "PlotId": "74997", + "ThumbnailIds": ["/kFtAdkCO0vXN2RWu2oMcR9GZ9Hi.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.6, + "NumRating": 580}, {"MovieId": "339380", "Title": "On the Basis of Sex", "Casts": [], "PlotId": "339380", + "ThumbnailIds": ["/izY9Le3QWtu7xkHq7bjJnuE5yGI.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 187}, + {"MovieId": "10756", "Title": "The Haunted Mansion", "Casts": [], "PlotId": "10756", + "ThumbnailIds": ["/lGi5yio4pdDz5PkSeZCbnMQz5vK.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.4, + "NumRating": 998}, {"MovieId": "24122", "Title": "The Rebound", "Casts": [], "PlotId": "24122", + "ThumbnailIds": ["/nkkFebe8ZgE843rnTrlaRWJy0g9.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.1, "NumRating": 393}, + {"MovieId": "10016", "Title": "Ghosts of Mars", "Casts": [], "PlotId": "10016", + "ThumbnailIds": ["/rBmkaKxRg55zBZr11EGbedFJM0.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.0, + "NumRating": 483}, {"MovieId": "242582", "Title": "Nightcrawler", "Casts": [], "PlotId": "242582", + "ThumbnailIds": ["/8oPY6ULFOTbAEskySNhgsUIN4fW.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.7, "NumRating": 5760}, + {"MovieId": "66", "Title": "Absolute Power", "Casts": [], "PlotId": "66", + "ThumbnailIds": ["/oJQdp09Oc51DkArsMDvgDLdWiDu.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 384}, {"MovieId": "304357", "Title": "Woman in Gold", "Casts": [], "PlotId": "304357", + "ThumbnailIds": ["/mNg0HRIhJyPgKo3NoxD3rfw061O.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 780}, + {"MovieId": "333339", "Title": "Ready Player One", "Casts": [], "PlotId": "333339", + "ThumbnailIds": ["/pU1ULUq8D3iRxl1fdX2lZIzdHuI.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 6795}, {"MovieId": "8984", "Title": "Disclosure", "Casts": [], "PlotId": "8984", + "ThumbnailIds": ["/qbwcjmHnXkeE8l7wNBsdnlrp3J4.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.0, "NumRating": 392}, + {"MovieId": "10466", "Title": "The Money Pit", "Casts": [], "PlotId": "10466", + "ThumbnailIds": ["/vzUfCSAXDIQemW6Kdk2RU2JtfiV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 468}, {"MovieId": "5511", "Title": "Le Samoura\u00ef", "Casts": [], "PlotId": "5511", + "ThumbnailIds": ["/axuBeLVBeXfVZPGg6ph2taWRDFq.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.0, "NumRating": 351}, + {"MovieId": "4176", "Title": "Murder on the Orient Express", "Casts": [], "PlotId": "4176", + "ThumbnailIds": ["/66B9pHWl2KU7CwB3xcBNYvEo2CH.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.1, + "NumRating": 560}, {"MovieId": "286217", "Title": "The Martian", "Casts": [], "PlotId": "286217", + "ThumbnailIds": ["/5aGhaIHYuQbqlHWvWYqMCnj40y2.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.7, "NumRating": 12004}, + {"MovieId": "331962", "Title": "Exposed", "Casts": [], "PlotId": "331962", + "ThumbnailIds": ["/nM26QosEfgjEegONCiNNrYMBTxD.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.6, + "NumRating": 243}, {"MovieId": "11866", "Title": "Flight of the Phoenix", "Casts": [], "PlotId": "11866", + "ThumbnailIds": ["/g1wvC9RyapnPQBgKcFJ3FChPEeC.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 427}, + {"MovieId": "606", "Title": "Out of Africa", "Casts": [], "PlotId": "606", + "ThumbnailIds": ["/gYNfg38sM4aSpxfC8gPkwg5UZHN.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 573}, {"MovieId": "191714", "Title": "The Lunchbox", "Casts": [], "PlotId": "191714", + "ThumbnailIds": ["/xFJqU1W5WlJiKr4Witnb7h9HNHn.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.3, "NumRating": 347}, + {"MovieId": "87101", "Title": "Terminator Genisys", "Casts": [], "PlotId": "87101", + "ThumbnailIds": ["/5JU9ytZJyR3zmClGmVm9q4Geqbd.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.9, + "NumRating": 5248}, {"MovieId": "238636", "Title": "The Purge: Anarchy", "Casts": [], "PlotId": "238636", + "ThumbnailIds": ["/l1DRl40x2OWUoPP42v8fjKdS1Z3.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.6, "NumRating": 3694}, + {"MovieId": "121875", "Title": "In the House", "Casts": [], "PlotId": "121875", + "ThumbnailIds": ["/4qAnWMNKUadx10jlCrkRAAuT4bN.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 416}, {"MovieId": "2275", "Title": "The General's Daughter", "Casts": [], "PlotId": "2275", + "ThumbnailIds": ["/4fc4mB9tGgYV4mdJrWPLMfcRvnC.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.2, "NumRating": 367}, + {"MovieId": "396461", "Title": "Under the Silver Lake", "Casts": [], "PlotId": "396461", + "ThumbnailIds": ["/771Ey73LqsE9ORJhQCI25rgMXS2.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 305}, + {"MovieId": "11418", "Title": "National Lampoon's European Vacation", "Casts": [], "PlotId": "11418", + "ThumbnailIds": ["/6AhVFxvDs2WZgfpC0bm2n2mshaa.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 365}, + {"MovieId": "10437", "Title": "The Muppet Christmas Carol", "Casts": [], "PlotId": "10437", + "ThumbnailIds": ["/fe4nLJkDNXhyuMnrZ60MfoSpPes.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 415}, {"MovieId": "75802", "Title": "Hysteria", "Casts": [], "PlotId": "75802", + "ThumbnailIds": ["/1vtHZYTBS5iRA5ADibY6cwU7Ffm.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 448}, + {"MovieId": "246655", "Title": "X-Men: Apocalypse", "Casts": [], "PlotId": "246655", + "ThumbnailIds": ["/zSouWWrySXshPCT4t3UKCQGayyo.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 8149}, {"MovieId": "10975", "Title": "Operation Condor", "Casts": [], "PlotId": "10975", + "ThumbnailIds": ["/loOps7gaDy1h6FTxQQdmh1L9TXn.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.1, "NumRating": 189}, + {"MovieId": "338970", "Title": "Tomb Raider", "Casts": [], "PlotId": "338970", + "ThumbnailIds": ["/3zrC5tUiR35rTz9stuIxnU1nUS5.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 4176}, {"MovieId": "597", "Title": "Titanic", "Casts": [], "PlotId": "597", + "ThumbnailIds": ["/kHXEpyfl6zqn8a6YuozZUujufXf.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.8, "NumRating": 13986}, + {"MovieId": "22586", "Title": "The Swan Princess", "Casts": [], "PlotId": "22586", + "ThumbnailIds": ["/oYp2bckWNBKOIbznjAfypVoMOFl.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 496}, {"MovieId": "10029", "Title": "Very Bad Things", "Casts": [], "PlotId": "10029", + "ThumbnailIds": ["/rGZ4bR7dNcAWntnVY5R8kge2IVq.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.3, "NumRating": 303}, + {"MovieId": "245168", "Title": "Suffragette", "Casts": [], "PlotId": "245168", + "ThumbnailIds": ["/gG2Y8GNXzMrZCx65a8EhdNq2iuu.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 961}, {"MovieId": "1975", "Title": "The Grudge 2", "Casts": [], "PlotId": "1975", + "ThumbnailIds": ["/lPN7FyCuPDHwoupcuPT72cRE8lY.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.4, "NumRating": 486}, + {"MovieId": "273477", "Title": "Scouts Guide to the Zombie Apocalypse", "Casts": [], "PlotId": "273477", + "ThumbnailIds": ["/3wHxtemithpFQiB7ffa50ZKI6bz.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 900}, {"MovieId": "8392", "Title": "My Neighbor Totoro", "Casts": [], "PlotId": "8392", + "ThumbnailIds": ["/2i0OOjvi7CqNQA6ZtYJtL65P9oZ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.1, "NumRating": 3193}, + {"MovieId": "86597", "Title": "The Tall Man", "Casts": [], "PlotId": "86597", + "ThumbnailIds": ["/1fc87E3ppCq6xBnZJ63Sa9FKRUd.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.9, + "NumRating": 558}, {"MovieId": "10648", "Title": "Magnum Force", "Casts": [], "PlotId": "10648", + "ThumbnailIds": ["/3gqV4jpKNFqxUWug3BRD6yUzSL1.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 411}, + {"MovieId": "559", "Title": "Spider-Man 3", "Casts": [], "PlotId": "559", + "ThumbnailIds": ["/2N9lhZg6VtVJoGCZDjXVC3a81Ea.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 6969}, + {"MovieId": "527729", "Title": "Asterix: The Secret of the Magic Potion", "Casts": [], "PlotId": "527729", + "ThumbnailIds": ["/wmMq5ypRNJbWpdhC9aPjpdx1MMp.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 341}, {"MovieId": "10052", "Title": "Dragonfly", "Casts": [], "PlotId": "10052", + "ThumbnailIds": ["/gs3fFBoiEaExy5x3m5yAQpmVhXs.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.5, "NumRating": 370}, + {"MovieId": "397", "Title": "French Kiss", "Casts": [], "PlotId": "397", + "ThumbnailIds": ["/bZqvonwVru1XtnniQZgZRa3Znyk.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 331}, {"MovieId": "4477", "Title": "The Devil's Own", "Casts": [], "PlotId": "4477", + "ThumbnailIds": ["/87ObtgniFhhPgESlIl85MfawduY.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 502}, + {"MovieId": "150689", "Title": "Cinderella", "Casts": [], "PlotId": "150689", + "ThumbnailIds": ["/2i0JH5WqYFqki7WDhUW56Sg0obh.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 4437}, {"MovieId": "44114", "Title": "3some", "Casts": [], "PlotId": "44114", + "ThumbnailIds": ["/bUaAybF88Gm9eKrcjB0lewx55Y3.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.0, "NumRating": 14}, + {"MovieId": "4978", "Title": "An American Tail", "Casts": [], "PlotId": "4978", + "ThumbnailIds": ["/glZNfxN4cef0pJeD08xru7ZVWlI.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 649}, {"MovieId": "11880", "Title": "Dog Soldiers", "Casts": [], "PlotId": "11880", + "ThumbnailIds": ["/kJLGyrjnphk6DnaJhYVFFoWJcCy.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 346}, + {"MovieId": "9754", "Title": "Firewall", "Casts": [], "PlotId": "9754", + "ThumbnailIds": ["/bp0BTphk7bgBof20XAfKkujRxwI.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.7, + "NumRating": 440}, {"MovieId": "320288", "Title": "Dark Phoenix", "Casts": [], "PlotId": "320288", + "ThumbnailIds": ["/kZv92eTc0Gg3mKxqjjDAM73z9cy.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 0.0, "NumRating": 0}, + {"MovieId": "9538", "Title": "Scanners", "Casts": [], "PlotId": "9538", + "ThumbnailIds": ["/gl7uT1nm7kpi4Fv0YMgv2C1dGj3.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 441}, {"MovieId": "9530", "Title": "RV", "Casts": [], "PlotId": "9530", + "ThumbnailIds": ["/eqV0JjfwcEJuK3JPZ2rsNvS1p30.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.7, "NumRating": 888}, + {"MovieId": "84165", "Title": "2 Days in New York", "Casts": [], "PlotId": "84165", + "ThumbnailIds": ["/7Zo1oQ7ssDfT3OKUAWqu9TD4J0H.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.6, + "NumRating": 148}, {"MovieId": "80038", "Title": "Friends with Kids", "Casts": [], "PlotId": "80038", + "ThumbnailIds": ["/ojCTu5O2scmRjlX26eS3EZbhjPb.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.8, "NumRating": 257}, + {"MovieId": "2623", "Title": "An Officer and a Gentleman", "Casts": [], "PlotId": "2623", + "ThumbnailIds": ["/1fveHeEaOpGW91ZM4ViT0Jviuak.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 441}, {"MovieId": "10481", "Title": "102 Dalmatians", "Casts": [], "PlotId": "10481", + "ThumbnailIds": ["/dSxnIika9yWwTvEbpsmoGdeh65E.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.4, "NumRating": 678}, + {"MovieId": "136", "Title": "Freaks", "Casts": [], "PlotId": "136", + "ThumbnailIds": ["/9hmmE4K6tOEXB1KeTajIC0pNta6.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.8, + "NumRating": 426}, {"MovieId": "135397", "Title": "Jurassic World", "Casts": [], "PlotId": "135397", + "ThumbnailIds": ["/jjBgi2r5cRt36xF6iNUEhzscEcb.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 13870}, + {"MovieId": "10934", "Title": "Under the Tuscan Sun", "Casts": [], "PlotId": "10934", + "ThumbnailIds": ["/z5mqOKxS8R9mnzyRulIbMX0U6kG.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 329}, {"MovieId": "178809", "Title": "The Colony", "Casts": [], "PlotId": "178809", + "ThumbnailIds": ["/oYeCfvBSdIfxGSlSXMwtAUzKiJC.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.2, "NumRating": 624}, + {"MovieId": "198185", "Title": "Million Dollar Arm", "Casts": [], "PlotId": "198185", + "ThumbnailIds": ["/rYOQL42cDdsbhfgaxeEW4SlszUp.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 348}, {"MovieId": "11782", "Title": "Hard Boiled", "Casts": [], "PlotId": "11782", + "ThumbnailIds": ["/8CfUC5QPTNe5NHlXWvYzpKIl3xs.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.6, "NumRating": 290}, + {"MovieId": "3604", "Title": "Flash Gordon", "Casts": [], "PlotId": "3604", + "ThumbnailIds": ["/9c5vKUX7MB1yixVp0ZFAUDRTaeE.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 398}, {"MovieId": "9303", "Title": "Bound", "Casts": [], "PlotId": "9303", + "ThumbnailIds": ["/xfITNjW2sunPiB7BNotJJsCxhdA.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.0, "NumRating": 341}, + {"MovieId": "143", "Title": "All Quiet on the Western Front", "Casts": [], "PlotId": "143", + "ThumbnailIds": ["/yAU6jklJLUjZot3WyvyJrxVdLKb.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.8, + "NumRating": 290}, {"MovieId": "336004", "Title": "Heist", "Casts": [], "PlotId": "336004", + "ThumbnailIds": ["/t5tGykRvvlLBULIPsAJEzGg1ylm.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.8, "NumRating": 492}, + {"MovieId": "10057", "Title": "The Three Musketeers", "Casts": [], "PlotId": "10057", + "ThumbnailIds": ["/mk8UH7JRmK8adcqJJpB1ygP7B1C.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 385}, {"MovieId": "4437", "Title": "2010", "Casts": [], "PlotId": "4437", + "ThumbnailIds": ["/9Rcz2n16HEYRi2EKGliByP6ESYR.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 426}, + {"MovieId": "3063", "Title": "Duck Soup", "Casts": [], "PlotId": "3063", + "ThumbnailIds": ["/jy4DJN8pKmEz4UNjCGiFaJuMfDJ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 320}, {"MovieId": "457136", "Title": "Mary Queen of Scots", "Casts": [], "PlotId": "457136", + "ThumbnailIds": ["/b5RMzLAyq5QW6GtN9sIeAEMLlBI.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 530}, + {"MovieId": "41602", "Title": "Charlie Countryman", "Casts": [], "PlotId": "41602", + "ThumbnailIds": ["/jG95rLdI1V1pTEtAwSkKZR74AGm.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 364}, {"MovieId": "333385", "Title": "Mr. Right", "Casts": [], "PlotId": "333385", + "ThumbnailIds": ["/y1VT2NoBOx3aC2exhkyN9AGUkMR.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.4, "NumRating": 727}, + {"MovieId": "9992", "Title": "Arthur and the Invisibles", "Casts": [], "PlotId": "9992", + "ThumbnailIds": ["/idbP413bzKWrQXJyElrDazcJUFM.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 1440}, + {"MovieId": "1494", "Title": "Curse of the Golden Flower", "Casts": [], "PlotId": "1494", + "ThumbnailIds": ["/8JGOTkKOCqSfAwIMPQKF0K44lUW.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 346}, {"MovieId": "172533", "Title": "Drinking Buddies", "Casts": [], "PlotId": "172533", + "ThumbnailIds": ["/zongyslIHQmfnf9rgUioPkDaHmq.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 522}, + {"MovieId": "674", "Title": "Harry Potter and the Goblet of Fire", "Casts": [], "PlotId": "674", + "ThumbnailIds": ["/6sASqcdrEHXxUhA3nFpjrRecPD2.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.7, + "NumRating": 10908}, {"MovieId": "12192", "Title": "Pathology", "Casts": [], "PlotId": "12192", + "ThumbnailIds": ["/7VjVRhlM7GyqwTc3OH8Itfx4jaB.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.7, "NumRating": 207}, + {"MovieId": "1825", "Title": "Over the Top", "Casts": [], "PlotId": "1825", + "ThumbnailIds": ["/rnAxGFrFaHemdjjFDQDtjOzV8z5.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 664}, {"MovieId": "8587", "Title": "The Lion King", "Casts": [], "PlotId": "8587", + "ThumbnailIds": ["/bKPtXn9n4M4s8vvZrbw40mYsefB.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.2, "NumRating": 9974}, + {"MovieId": "10477", "Title": "Driven", "Casts": [], "PlotId": "10477", + "ThumbnailIds": ["/vtPvxgQBoNBFnnnYjS1lb6pLXtv.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.7, + "NumRating": 315}, + {"MovieId": "131634", "Title": "The Hunger Games: Mockingjay - Part 2", "Casts": [], "PlotId": "131634", + "ThumbnailIds": ["/w93GAiq860UjmgR6tU9h2T24vaV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 7485}, + {"MovieId": "324552", "Title": "John Wick: Chapter 2", "Casts": [], "PlotId": "324552", + "ThumbnailIds": ["/zkXnKIwX5pYorKJp2fjFSfNyKT0.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 5487}, + {"MovieId": "6279", "Title": "Sister Act 2: Back in the Habit", "Casts": [], "PlotId": "6279", + "ThumbnailIds": ["/tLbvfUYw8Pr7DMI5TGL5DX1bx5S.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 811}, {"MovieId": "24150", "Title": "Halloween II", "Casts": [], "PlotId": "24150", + "ThumbnailIds": ["/vSHPM4LQDpWdQrD5KZWK6wNqSOD.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.2, "NumRating": 555}, + {"MovieId": "564394", "Title": "Crypto", "Casts": [], "PlotId": "564394", "ThumbnailIds": [None], + "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, "NumRating": 0}, + {"MovieId": "435577", "Title": "The 12th Man", "Casts": [], "PlotId": "435577", + "ThumbnailIds": ["/dxZ8iGf5jQoLcNqBJL6uzNTivp.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 118}, {"MovieId": "87499", "Title": "The East", "Casts": [], "PlotId": "87499", + "ThumbnailIds": ["/nGoFIX5WmthZZ3eLBd0QwDSkWNy.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 433}, + {"MovieId": "443055", "Title": "Love of My Life", "Casts": [], "PlotId": "443055", + "ThumbnailIds": ["/7b19Sh0Aef5vGa0OFtvJxLe2SK9.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.4, + "NumRating": 8}, {"MovieId": "238", "Title": "The Godfather", "Casts": [], "PlotId": "238", + "ThumbnailIds": ["/rPdtLWNsZmAtoZl9PK7S2wE3qiS.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.6, "NumRating": 9858}, + {"MovieId": "8869", "Title": "Eight Legged Freaks", "Casts": [], "PlotId": "8869", + "ThumbnailIds": ["/udEgrVBvcXV5tGdozogGsOpD1Rs.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.4, + "NumRating": 504}, {"MovieId": "4964", "Title": "Knocked Up", "Casts": [], "PlotId": "4964", + "ThumbnailIds": ["/b4OaXw2MW97VvIiZE0Sbn1NfxSh.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.2, "NumRating": 2118}, + {"MovieId": "49017", "Title": "Dracula Untold", "Casts": [], "PlotId": "49017", + "ThumbnailIds": ["/4oy4e0DP6LRwRszfx8NY8EYBj8V.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 3679}, {"MovieId": "11527", "Title": "Excalibur", "Casts": [], "PlotId": "11527", + "ThumbnailIds": ["/j8UmbdA1TrIVY4FANymwBSmUuCH.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.1, "NumRating": 431}, + {"MovieId": "39210", "Title": "Somewhere", "Casts": [], "PlotId": "39210", + "ThumbnailIds": ["/d3A0bbRp6sHN4uz6n3ddbtyL1tt.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 370}, + {"MovieId": "166426", "Title": "Pirates of the Caribbean: Dead Men Tell No Tales", "Casts": [], + "PlotId": "166426", "ThumbnailIds": ["/xbpSDU3p7YUGlu9Mr6Egg2Vweto.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 6804}, + {"MovieId": "12445", "Title": "Harry Potter and the Deathly Hallows: Part 2", "Casts": [], + "PlotId": "12445", "ThumbnailIds": ["/fTplI1NCSuEDP4ITLcTps739fcC.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.1, "NumRating": 11352}, + {"MovieId": "8689", "Title": "Cannibal Holocaust", "Casts": [], "PlotId": "8689", + "ThumbnailIds": ["/r0VmAFHWAqlaraVg0krIEhDCPWH.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 612}, {"MovieId": "22949", "Title": "Old Dogs", "Casts": [], "PlotId": "22949", + "ThumbnailIds": ["/2B4tOJ71vJQktyjsG0iqGM0Yh10.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.3, "NumRating": 351}, + {"MovieId": "156022", "Title": "The Equalizer", "Casts": [], "PlotId": "156022", + "ThumbnailIds": ["/2eQfjqlvPAxd9aLDs8DvsKLnfed.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 4883}, {"MovieId": "190469", "Title": "Redirected", "Casts": [], "PlotId": "190469", + "ThumbnailIds": ["/dbmXS7kEJxhAqVVmm5vpmDYpiqh.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.8, "NumRating": 211}, + {"MovieId": "18148", "Title": "Tokyo Story", "Casts": [], "PlotId": "18148", + "ThumbnailIds": ["/g2YbTYKpY7N2yDSk7BfXZ18I5QV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.3, + "NumRating": 338}, + {"MovieId": "10539", "Title": "James and the Giant Peach", "Casts": [], "PlotId": "10539", + "ThumbnailIds": ["/kyAl5UfUtGJC1wHrPVieqKKCpn8.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 635}, {"MovieId": "11932", "Title": "Bride of Chucky", "Casts": [], "PlotId": "11932", + "ThumbnailIds": ["/u5Lc1Li0Hpc452o57E2KaToezZX.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.6, "NumRating": 702}, + {"MovieId": "103620", "Title": "Maniac", "Casts": [], "PlotId": "103620", + "ThumbnailIds": ["/ag1IgAqYartblOy0IiDIMNoJUVI.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 461}, {"MovieId": "10163", "Title": "The Lawnmower Man", "Casts": [], "PlotId": "10163", + "ThumbnailIds": ["/3tWLM3zMyh3KZOactn8mfjHml05.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.4, "NumRating": 317}, + {"MovieId": "9815", "Title": "Goal! II: Living the Dream", "Casts": [], "PlotId": "9815", + "ThumbnailIds": ["/sKKej989oVbZahldAkrMVmyB8pU.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 313}, + {"MovieId": "295699", "Title": "Everybody Wants Some!!", "Casts": [], "PlotId": "295699", + "ThumbnailIds": ["/mIpd0rGxruvxCnMSmh4gd8wuhmv.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 669}, {"MovieId": "9289", "Title": "The Longest Day", "Casts": [], "PlotId": "9289", + "ThumbnailIds": ["/7fnuirXJpuRHggi2lOCBEwZ3eWU.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.5, "NumRating": 406}, + {"MovieId": "177572", "Title": "Big Hero 6", "Casts": [], "PlotId": "177572", + "ThumbnailIds": ["/9gLu47Zw5ertuFTZaxXOvNfy78T.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.8, + "NumRating": 10037}, {"MovieId": "25643", "Title": "Love Happens", "Casts": [], "PlotId": "25643", + "ThumbnailIds": ["/pN51u0l8oSEsxAYiHUzzbMrMXH7.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.9, "NumRating": 345}, + {"MovieId": "11212", "Title": "Baby's Day Out", "Casts": [], "PlotId": "11212", + "ThumbnailIds": ["/21U2jwl36hoTHsXB3fDuIQkcchu.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.9, + "NumRating": 565}, {"MovieId": "65759", "Title": "Happy Feet Two", "Casts": [], "PlotId": "65759", + "ThumbnailIds": ["/2gWiQ4mn85jcXtREVePlVViupeV.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 736}, + {"MovieId": "38234", "Title": "Undisputed III: Redemption", "Casts": [], "PlotId": "38234", + "ThumbnailIds": ["/vLSnWLCnqk5j3oLnMnBHJGL3bO0.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 296}, {"MovieId": "5375", "Title": "Fred Claus", "Casts": [], "PlotId": "5375", + "ThumbnailIds": ["/vFniWmciRi4tAjAYGp2wrK0P8dJ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.5, "NumRating": 406}, + {"MovieId": "8409", "Title": "A Man Apart", "Casts": [], "PlotId": "8409", + "ThumbnailIds": ["/l9UIm6rCHzfbMy6KY8ynjV4kLHX.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.9, + "NumRating": 406}, {"MovieId": "8090", "Title": "Untraceable", "Casts": [], "PlotId": "8090", + "ThumbnailIds": ["/ySUwDRDEn01lKIMPQorpFCMLWqE.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 392}, + {"MovieId": "93", "Title": "Anatomy of a Murder", "Casts": [], "PlotId": "93", + "ThumbnailIds": ["/kDFnM2zlHZirR2ItTo56lyrxuAS.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.9, + "NumRating": 347}, {"MovieId": "553100", "Title": "Wild and Free", "Casts": [], "PlotId": "553100", + "ThumbnailIds": ["/jLGNqaymD0ygyhafhv5fM3nXcge.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 10.0, "NumRating": 2}, + {"MovieId": "200505", "Title": "Draft Day", "Casts": [], "PlotId": "200505", + "ThumbnailIds": ["/pb5FXL6pypVQbcs3TCzp5GqyTYr.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 454}, {"MovieId": "10610", "Title": "The Medallion", "Casts": [], "PlotId": "10610", + "ThumbnailIds": ["/9iJrg37ceCBeQkQHnIy97VekINb.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.3, "NumRating": 501}, + {"MovieId": "16577", "Title": "Astro Boy", "Casts": [], "PlotId": "16577", + "ThumbnailIds": ["/4kQczIhUFTnDWwG6HKsgCxLoi6.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 706}, {"MovieId": "330947", "Title": "Song to Song", "Casts": [], "PlotId": "330947", + "ThumbnailIds": ["/rEvtGhNY2DQ4L8Ma6rpMhL6IbKM.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.3, "NumRating": 463}, + {"MovieId": "514692", "Title": "Neon Heart", "Casts": [], "PlotId": "514692", + "ThumbnailIds": ["/RgniFwvK2vL3yapRrz4GdGZAnE.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "449985", "Title": "Triple Threat", "Casts": [], "PlotId": "449985", + "ThumbnailIds": ["/cSpM3QxmoSLp4O1WAMQpUDcaB7R.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.5, "NumRating": 75}, + {"MovieId": "10731", "Title": "The Client", "Casts": [], "PlotId": "10731", + "ThumbnailIds": ["/bCWpxGGcP9DsCLwfNpnfcl1vLk8.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 500}, {"MovieId": "209276", "Title": "Starred Up", "Casts": [], "PlotId": "209276", + "ThumbnailIds": ["/cP5vXTItgkNahGEsaIHudbdIDRG.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.2, "NumRating": 521}, + {"MovieId": "10623", "Title": "Cradle 2 the Grave", "Casts": [], "PlotId": "10623", + "ThumbnailIds": ["/v8iPcn54TNsSPabD9ZYQVQUWbXk.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 379}, {"MovieId": "9741", "Title": "Unbreakable", "Casts": [], "PlotId": "9741", + "ThumbnailIds": ["/pvL37V88plePxFSszCbV3wRHiBm.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 4835}, + {"MovieId": "862", "Title": "Toy Story", "Casts": [], "PlotId": "862", + "ThumbnailIds": ["/rhIRbceoE9lR4veEXuwCC2wARtG.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.9, + "NumRating": 9986}, {"MovieId": "505948", "Title": "I Am Mother", "Casts": [], "PlotId": "505948", + "ThumbnailIds": ["/eItrj5GcjvCI3oD3bIcz1A2IL9t.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 0.0, "NumRating": 1}, + {"MovieId": "438740", "Title": "Salyut-7", "Casts": [], "PlotId": "438740", + "ThumbnailIds": ["/s2ktgF2ze4aVt9bXBUvxFJllqyd.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 146}, {"MovieId": "369972", "Title": "First Man", "Casts": [], "PlotId": "369972", + "ThumbnailIds": ["/i91mfvFcPPlaegcbOyjGgiWfZzh.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 2180}, + {"MovieId": "14536", "Title": "New in Town", "Casts": [], "PlotId": "14536", + "ThumbnailIds": ["/3C3WWizgIKWQ5O6k3xN5pkwXEYJ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.6, + "NumRating": 211}, {"MovieId": "10518", "Title": "Marathon Man", "Casts": [], "PlotId": "10518", + "ThumbnailIds": ["/uPNgubSiri2yvBQRPtP77ViYjN.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 397}, + {"MovieId": "9053", "Title": "DOA: Dead or Alive", "Casts": [], "PlotId": "9053", + "ThumbnailIds": ["/w1DbRfGKWWS3JR74q4vawHVQy5B.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.0, + "NumRating": 362}, {"MovieId": "426563", "Title": "Holmes & Watson", "Casts": [], "PlotId": "426563", + "ThumbnailIds": ["/orEUlKndjV1rEcWqXbbjegjfv97.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 4.2, "NumRating": 181}, + {"MovieId": "9972", "Title": "Lock Up", "Casts": [], "PlotId": "9972", + "ThumbnailIds": ["/wRWZDNzebz2a52GtdhN1bx3ujE7.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 382}, {"MovieId": "4961", "Title": "Mimic", "Casts": [], "PlotId": "4961", + "ThumbnailIds": ["/tkmSgkekKleTx46a1ERny967Ws5.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.8, "NumRating": 418}, + {"MovieId": "249", "Title": "The War of the Roses", "Casts": [], "PlotId": "249", + "ThumbnailIds": ["/9VWwYsuXhRImUtrJGvN6bYJB2He.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 499}, {"MovieId": "884", "Title": "Crash", "Casts": [], "PlotId": "884", + "ThumbnailIds": ["/4PDLGJsS5uVBQlYjLMZ7t85dU0P.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.5, "NumRating": 390}, + {"MovieId": "9356", "Title": "Look Who's Talking Too", "Casts": [], "PlotId": "9356", + "ThumbnailIds": ["/7HtSFUYBPOUd9ylUXU0LyPsxpRm.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.2, + "NumRating": 655}, {"MovieId": "324670", "Title": "Spectral", "Casts": [], "PlotId": "324670", + "ThumbnailIds": ["/oXV2ayQYUQfHwpuMdWnZF0Geng5.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.3, "NumRating": 813}, + {"MovieId": "1280", "Title": "3-Iron", "Casts": [], "PlotId": "1280", + "ThumbnailIds": ["/hr8ghKbdo3UGUROYafpN38Sohfe.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.8, + "NumRating": 444}, {"MovieId": "8879", "Title": "Pale Rider", "Casts": [], "PlotId": "8879", + "ThumbnailIds": ["/7C18VUCvZH5O0ibZogZP4TQJTiu.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 420}, + {"MovieId": "12160", "Title": "Wyatt Earp", "Casts": [], "PlotId": "12160", + "ThumbnailIds": ["/zSGDgcWHqmQxmX4mLJZgT5UgLjj.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 341}, {"MovieId": "327331", "Title": "The Dirt", "Casts": [], "PlotId": "327331", + "ThumbnailIds": ["/xGY5rr8441ib0lT9mtHZn7e8Aay.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.3, "NumRating": 265}, + {"MovieId": "10328", "Title": "Cocoon", "Casts": [], "PlotId": "10328", + "ThumbnailIds": ["/foIhEPQoqDctfwsHmmYwbNz5A2g.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 564}, {"MovieId": "9696", "Title": "Ichi the Killer", "Casts": [], "PlotId": "9696", + "ThumbnailIds": ["/mkmwESVpq7KrcxdPMreTFqlNF0S.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.0, "NumRating": 379}, + {"MovieId": "11565", "Title": "Big Momma's House 2", "Casts": [], "PlotId": "11565", + "ThumbnailIds": ["/tOl6hzfFHdNcL7SxopC2Vbs4mgK.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.6, + "NumRating": 777}, + {"MovieId": "321612", "Title": "Beauty and the Beast", "Casts": [], "PlotId": "321612", + "ThumbnailIds": ["/tWqifoYuwLETmmasnGHO7xBjEtt.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 10886}, {"MovieId": "2163", "Title": "Breakdown", "Casts": [], "PlotId": "2163", + "ThumbnailIds": ["/uj5VmM4jrn3HHeuFfEH8XQizA2g.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.6, "NumRating": 348}, + {"MovieId": "77805", "Title": "Lovelace", "Casts": [], "PlotId": "77805", + "ThumbnailIds": ["/6J4zI97usxdjhzobBkqFW1d9OQ5.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 360}, {"MovieId": "50725", "Title": "Take Me Home Tonight", "Casts": [], "PlotId": "50725", + "ThumbnailIds": ["/glRBoVKClP7qYQO0gQi5keCQ6ko.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.1, "NumRating": 326}, + {"MovieId": "643", "Title": "Battleship Potemkin", "Casts": [], "PlotId": "643", + "ThumbnailIds": ["/tjnaRiHUsxBADaOwrQpnTnjHVwi.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 458}, {"MovieId": "916", "Title": "Bullitt", "Casts": [], "PlotId": "916", + "ThumbnailIds": ["/oyhnoFu2oKQfAIdu9YU8I8Ne0pX.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.2, "NumRating": 437}, + {"MovieId": "336050", "Title": "Son of Saul", "Casts": [], "PlotId": "336050", + "ThumbnailIds": ["/AcjoM9JielY0Yi42GnICNBntpND.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 493}, + {"MovieId": "335988", "Title": "Transformers: The Last Knight", "Casts": [], "PlotId": "335988", + "ThumbnailIds": ["/s5HQf2Gb3lIO2cRcFwNL9sn1o1o.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 3204}, {"MovieId": "16558", "Title": "Duplicity", "Casts": [], "PlotId": "16558", + "ThumbnailIds": ["/vpWQs3CjwG6Er3DgZlv4L3NFjXg.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.9, "NumRating": 346}, + {"MovieId": "9594", "Title": "Double Impact", "Casts": [], "PlotId": "9594", + "ThumbnailIds": ["/sRxg7BI6y5dXa5SX0RZS3zlhxwc.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.5, + "NumRating": 371}, {"MovieId": "10712", "Title": "Far from Heaven", "Casts": [], "PlotId": "10712", + "ThumbnailIds": ["/7lzlqQmtv4z2CEiiBWU55blk1zo.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 249}, + {"MovieId": "11074", "Title": "Striking Distance", "Casts": [], "PlotId": "11074", + "ThumbnailIds": ["/1bLazrQWJQhO6u58vJZXJZ3ZFDh.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.7, + "NumRating": 363}, {"MovieId": "399055", "Title": "The Shape of Water", "Casts": [], "PlotId": "399055", + "ThumbnailIds": ["/k4FwHlMhuRR5BISY2Gm2QZHlH5Q.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.3, "NumRating": 7081}, + {"MovieId": "13416", "Title": "Friday Night Lights", "Casts": [], "PlotId": "13416", + "ThumbnailIds": ["/8HIqpOgqShRc3TAleabnYispDl1.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 248}, {"MovieId": "300668", "Title": "Annihilation", "Casts": [], "PlotId": "300668", + "ThumbnailIds": ["/d3qcpfNwbAMCNqWDHzPQsUYiUgS.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.3, "NumRating": 4359}, + {"MovieId": "119450", "Title": "Dawn of the Planet of the Apes", "Casts": [], "PlotId": "119450", + "ThumbnailIds": ["/2EUAUIu5lHFlkj5FRryohlH6CRO.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 6984}, {"MovieId": "11386", "Title": "The Crying Game", "Casts": [], "PlotId": "11386", + "ThumbnailIds": ["/9Qqk3svM0I9QuabOPDNrJZdv3XJ.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.0, "NumRating": 307}, + {"MovieId": "10589", "Title": "After the Sunset", "Casts": [], "PlotId": "10589", + "ThumbnailIds": ["/seEavscJqc1TuHfSZMeXQwohkNf.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 340}, {"MovieId": "49026", "Title": "The Dark Knight Rises", "Casts": [], "PlotId": "49026", + "ThumbnailIds": ["/dEYnvnUfXrqvqeRSqvIEtmzhoA8.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.7, "NumRating": 13731}, + {"MovieId": "1552", "Title": "Parenthood", "Casts": [], "PlotId": "1552", + "ThumbnailIds": ["/e51tNNQBJpJi9xkyuj0QFhyBcz7.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 267}, {"MovieId": "13944", "Title": "Passengers", "Casts": [], "PlotId": "13944", + "ThumbnailIds": ["/fGI0FFre8W454JqHgexV6zDWC9H.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 344}, + {"MovieId": "524309", "Title": "The Gift", "Casts": [], "PlotId": "524309", + "ThumbnailIds": ["/oiymQKFIqbCbWamrQ5EbOYdmjvn.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, + {"MovieId": "9385", "Title": "The Twelve Tasks of Asterix", "Casts": [], "PlotId": "9385", + "ThumbnailIds": ["/7cZQZOYZFJcLjZnxWjO5PcUtmDZ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 501}, {"MovieId": "36419", "Title": "After.Life", "Casts": [], "PlotId": "36419", + "ThumbnailIds": ["/8mtT1Hqp63s3DiGxZckxQj0Mioy.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.6, "NumRating": 406}, + {"MovieId": "775", "Title": "A Trip to the Moon", "Casts": [], "PlotId": "775", + "ThumbnailIds": ["/zztHYAfSecYuaDyIjQudjKaOLLY.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.0, + "NumRating": 672}, {"MovieId": "931", "Title": "Don't Look Now", "Casts": [], "PlotId": "931", + "ThumbnailIds": ["/qRUMQN3fa43ZuEldhNG7UYoURDG.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 336}, + {"MovieId": "77875", "Title": "Playing for Keeps", "Casts": [], "PlotId": "77875", + "ThumbnailIds": ["/eOBekVFBGK4TwETQSwfuk22B41o.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.6, + "NumRating": 410}, {"MovieId": "10876", "Title": "Quills", "Casts": [], "PlotId": "10876", + "ThumbnailIds": ["/4fz5LDFsapXDJVLsy2jh5VPZtR0.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.9, "NumRating": 238}, + {"MovieId": "10675", "Title": "Frantic", "Casts": [], "PlotId": "10675", + "ThumbnailIds": ["/wOaBEIhiEA832ifEQ4CNxVHrh1c.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 414}, {"MovieId": "543343", "Title": "Guava Island", "Casts": [], "PlotId": "543343", + "ThumbnailIds": ["/noE2O4XaRSrNZ75MUShaQD0pFN0.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.5, "NumRating": 75}, + {"MovieId": "9796", "Title": "Turistas", "Casts": [], "PlotId": "9796", + "ThumbnailIds": ["/lwj9HLVIfLaFYyF7ZIxBlntT9jM.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.3, + "NumRating": 354}, {"MovieId": "11817", "Title": "Bulletproof Monk", "Casts": [], "PlotId": "11817", + "ThumbnailIds": ["/hzk3kf5h54cCP3MYDptEjmLKVLF.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.3, "NumRating": 439}, + {"MovieId": "11901", "Title": "High Plains Drifter", "Casts": [], "PlotId": "11901", + "ThumbnailIds": ["/557vwDdOIQ07Q1QvTwHVmxHJpXU.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 468}, {"MovieId": "49521", "Title": "Man of Steel", "Casts": [], "PlotId": "49521", + "ThumbnailIds": ["/xWlaTLnD8NJMTT9PGOD9z5re1SL.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.5, "NumRating": 9624}, + {"MovieId": "228203", "Title": "McFarland, USA", "Casts": [], "PlotId": "228203", + "ThumbnailIds": ["/kV3Bk0PGwYhHLy1JppK3ZbVh7IB.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 353}, {"MovieId": "261", "Title": "Cat on a Hot Tin Roof", "Casts": [], "PlotId": "261", + "ThumbnailIds": ["/tkHug8LLo9dBgkVQqvmC4sib9HB.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.8, "NumRating": 306}, + {"MovieId": "257445", "Title": "Goosebumps", "Casts": [], "PlotId": "257445", + "ThumbnailIds": ["/9yOnWCvpNr6RRhc4zhJdVqR7GKw.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 2087}, {"MovieId": "1730", "Title": "Inland Empire", "Casts": [], "PlotId": "1730", + "ThumbnailIds": ["/s5f0FbVAABEnJYKaApWORTxhiFC.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.1, "NumRating": 489}, + {"MovieId": "10632", "Title": "The Hunted", "Casts": [], "PlotId": "10632", + "ThumbnailIds": ["/k8fV0lYH9DqGtB7M681DTa8hRuN.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 312}, {"MovieId": "11864", "Title": "Enemy Mine", "Casts": [], "PlotId": "11864", + "ThumbnailIds": ["/hKsZY8jHagbiTtvq8mwHV36a9ki.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.8, "NumRating": 397}, + {"MovieId": "506", "Title": "Marnie", "Casts": [], "PlotId": "506", + "ThumbnailIds": ["/mwEuBWMJyebtJ1OP4W2jeRcVf3k.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 413}, + {"MovieId": "9644", "Title": "National Lampoon's Loaded Weapon 1", "Casts": [], "PlotId": "9644", + "ThumbnailIds": ["/tIdlDgiVQ4kbgVXXIlP8LswubkN.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 284}, {"MovieId": "9093", "Title": "The Four Feathers", "Casts": [], "PlotId": "9093", + "ThumbnailIds": ["/1mr4V13SFK4En8f4ZdyBgnZmVar.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 259}, + {"MovieId": "419430", "Title": "Get Out", "Casts": [], "PlotId": "419430", + "ThumbnailIds": ["/1SwAVYpuLj8KsHxllTF8Dt9dSSX.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 8505}, {"MovieId": "44945", "Title": "Trust", "Casts": [], "PlotId": "44945", + "ThumbnailIds": ["/oWEy6b2LxwcuZtWLmrrdBWp3hda.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.6, "NumRating": 546}, + {"MovieId": "10303", "Title": "The Jewel of the Nile", "Casts": [], "PlotId": "10303", + "ThumbnailIds": ["/iQXxVWuXmaEPhieETkThDgGGKCb.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 492}, {"MovieId": "468", "Title": "My Own Private Idaho", "Casts": [], "PlotId": "468", + "ThumbnailIds": ["/xgWJPruxsL2TGVwhZDM8YiarbEY.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 426}, + {"MovieId": "16804", "Title": "Departures", "Casts": [], "PlotId": "16804", + "ThumbnailIds": ["/aiRKdQ3CqzMv88Zlk69utBBbseO.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.9, + "NumRating": 308}, {"MovieId": "1396", "Title": "Mirror", "Casts": [], "PlotId": "1396", + "ThumbnailIds": ["/9PknVc5uubhVLZ6ofvfJAprM9UZ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.1, "NumRating": 300}, + {"MovieId": "2830", "Title": "My Boss's Daughter", "Casts": [], "PlotId": "2830", + "ThumbnailIds": ["/du7joY11DJZeXXph7FwkJ3uo7om.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.6, + "NumRating": 350}, + {"MovieId": "442062", "Title": "Goosebumps 2: Haunted Halloween", "Casts": [], "PlotId": "442062", + "ThumbnailIds": ["/kOrUF0EH2C3KHoI7tqANZMFZaTN.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.6, + "NumRating": 395}, {"MovieId": "1714", "Title": "Fahrenheit 451", "Casts": [], "PlotId": "1714", + "ThumbnailIds": ["/78NFjrD9onl9ciKSjsENUfnTHbT.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.2, "NumRating": 386}, + {"MovieId": "10160", "Title": "A Nightmare on Elm Street: The Dream Child", "Casts": [], + "PlotId": "10160", "ThumbnailIds": ["/vnorsG2pKAVqIMYsDJbKoOr4CsX.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.4, "NumRating": 506}, + {"MovieId": "17130", "Title": "Crossroads", "Casts": [], "PlotId": "17130", + "ThumbnailIds": ["/egIs4cWxn0iBErFKrQ7STckjj49.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.9, + "NumRating": 287}, + {"MovieId": "102382", "Title": "The Amazing Spider-Man 2", "Casts": [], "PlotId": "102382", + "ThumbnailIds": ["/mUjWof8LHDgCZC9mFp0UYKBf1Dm.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 7085}, {"MovieId": "301", "Title": "Rio Bravo", "Casts": [], "PlotId": "301", + "ThumbnailIds": ["/gyEfGVDe5puz4wgIq6073fn8pHc.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.9, "NumRating": 471}, + {"MovieId": "3085", "Title": "His Girl Friday", "Casts": [], "PlotId": "3085", + "ThumbnailIds": ["/fmQLvnDEL9wlE6FzB1S84yskdkT.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 308}, {"MovieId": "11184", "Title": "Kinsey", "Casts": [], "PlotId": "11184", + "ThumbnailIds": ["/aOmsDieEAsRDgDOYbY8tSUoTdnA.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 214}, + {"MovieId": "10658", "Title": "Howard the Duck", "Casts": [], "PlotId": "10658", + "ThumbnailIds": ["/f2pj3SSj1GdFSrS5bUojT56umL6.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.1, + "NumRating": 416}, {"MovieId": "9626", "Title": "Red Sonja", "Casts": [], "PlotId": "9626", + "ThumbnailIds": ["/lvRlHJeMAYlUzSD8cebfIfNOhyl.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.1, "NumRating": 352}, + {"MovieId": "140420", "Title": "Paperman", "Casts": [], "PlotId": "140420", + "ThumbnailIds": ["/3TpMBcAYH4cxCw5WoRacWodMTCG.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.1, + "NumRating": 1107}, {"MovieId": "205321", "Title": "Sharknado", "Casts": [], "PlotId": "205321", + "ThumbnailIds": ["/6nxewN7l5XGxDqCcGkLaXJ3ljdz.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 3.8, "NumRating": 863}, + {"MovieId": "9945", "Title": "Vampires", "Casts": [], "PlotId": "9945", + "ThumbnailIds": ["/qHGawU64MeGvtU86s6V0MA7MqFV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 456}, {"MovieId": "12142", "Title": "Alone in the Dark", "Casts": [], "PlotId": "12142", + "ThumbnailIds": ["/3HsprIjUEwYfnlEf7jumGm037Bk.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 3.1, "NumRating": 276}, + {"MovieId": "38360", "Title": "The Cranes Are Flying", "Casts": [], "PlotId": "38360", + "ThumbnailIds": ["/xFE2hx2kGYcsQHttJobRajuyA6Q.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.1, + "NumRating": 110}, {"MovieId": "11137", "Title": "The Prince & Me", "Casts": [], "PlotId": "11137", + "ThumbnailIds": ["/nB75Ht9rppAVMj0lZWZQ51PPYUe.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 516}, + {"MovieId": "10923", "Title": "Agent Cody Banks", "Casts": [], "PlotId": "10923", + "ThumbnailIds": ["/fhK0mqqirPsckxkNisvi32A4lf6.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.2, + "NumRating": 560}, + {"MovieId": "9728", "Title": "Friday the 13th Part III", "Casts": [], "PlotId": "9728", + "ThumbnailIds": ["/5wg2NZyIhcMbIBAahBODXHyJ54S.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 516}, {"MovieId": "10577", "Title": "Dracula 2000", "Casts": [], "PlotId": "10577", + "ThumbnailIds": ["/6YF56U91zP1mQvle83KA1A7CPop.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 4.7, "NumRating": 275}, + {"MovieId": "1649", "Title": "Bill & Ted's Bogus Journey", "Casts": [], "PlotId": "1649", + "ThumbnailIds": ["/q8ssJWstfsWHmmFdigDk4r3l5gM.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 474}, {"MovieId": "9825", "Title": "Lake Placid", "Casts": [], "PlotId": "9825", + "ThumbnailIds": ["/6lE0EahK7xDOYWRH6On5uKPnwQZ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.5, "NumRating": 429}, + {"MovieId": "281338", "Title": "War for the Planet of the Apes", "Casts": [], "PlotId": "281338", + "ThumbnailIds": ["/ijQHiImv16vNSeZQsmih04kwn0C.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 5166}, {"MovieId": "512239", "Title": "The Corrupted", "Casts": [], "PlotId": "512239", + "ThumbnailIds": ["/tNGJw2R1l3XuLRi749GQaFua9yZ.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 0.0, "NumRating": 0}, + {"MovieId": "10197", "Title": "Nine", "Casts": [], "PlotId": "10197", + "ThumbnailIds": ["/2PSmbXIUWgUVsXq2U1MoVj3f38g.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.5, + "NumRating": 308}, + {"MovieId": "24438", "Title": "Did You Hear About the Morgans?", "Casts": [], "PlotId": "24438", + "ThumbnailIds": ["/yBrceyep5ZWaU9XuodM1X5c67K6.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.3, + "NumRating": 496}, {"MovieId": "8427", "Title": "I Spy", "Casts": [], "PlotId": "8427", + "ThumbnailIds": ["/6mtUJKyedvQwEKXfWzJt3vtWx1M.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.3, "NumRating": 475}, + {"MovieId": "347969", "Title": "The Ridiculous 6", "Casts": [], "PlotId": "347969", + "ThumbnailIds": ["/k77xcsBtZMq9zIlTVQV7UBemzXD.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.9, + "NumRating": 661}, {"MovieId": "649", "Title": "Belle de Jour", "Casts": [], "PlotId": "649", + "ThumbnailIds": ["/rHvKWARrhNwSjbTMJrn5v4LtUJE.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.5, "NumRating": 310}, + {"MovieId": "7548", "Title": "The Libertine", "Casts": [], "PlotId": "7548", + "ThumbnailIds": ["/a0gI62QrqJ7PjTsUhJlEsWOPHeU.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 236}, + {"MovieId": "35552", "Title": "The Extraordinary Adventures of Ad\u00e8le Blanc-Sec", "Casts": [], + "PlotId": "35552", "ThumbnailIds": ["/og6Jv55bF0uzF1F1kpUW6Je7z0H.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 655}, + {"MovieId": "3525", "Title": "Working Girl", "Casts": [], "PlotId": "3525", + "ThumbnailIds": ["/bzPuHOl8DjfZDzwGDCIHonnHUT6.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 420}, {"MovieId": "13", "Title": "Forrest Gump", "Casts": [], "PlotId": "13", + "ThumbnailIds": ["/yE5d3BUhE8hCnkMUJOo1QDoOGNz.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.4, "NumRating": 14523}, + {"MovieId": "100402", "Title": "Captain America: The Winter Soldier", "Casts": [], "PlotId": "100402", + "ThumbnailIds": ["/5TQ6YDmymBpnF005OyoB7ohZps9.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.7, + "NumRating": 10760}, {"MovieId": "936", "Title": "The Pink Panther", "Casts": [], "PlotId": "936", + "ThumbnailIds": ["/azIoCxiH9wIPCCGnqaDW8DJwCLl.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.1, "NumRating": 436}, + {"MovieId": "56288", "Title": "Spy Kids: All the Time in the World", "Casts": [], "PlotId": "56288", + "ThumbnailIds": ["/eHldUGDNxb4ZPQSPDnGolyFDECa.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.7, + "NumRating": 475}, {"MovieId": "9549", "Title": "The Right Stuff", "Casts": [], "PlotId": "9549", + "ThumbnailIds": ["/df1gdyE9S3DxmTfAeN3pNdSm64J.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.5, "NumRating": 378}, + {"MovieId": "11185", "Title": "See No Evil, Hear No Evil", "Casts": [], "PlotId": "11185", + "ThumbnailIds": ["/psB1Zb6sqnzHyCgtJrEJqphflVx.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 402}, {"MovieId": "10155", "Title": "U Turn", "Casts": [], "PlotId": "10155", + "ThumbnailIds": ["/mG4GerNC6ZVnsFTXcsTeKuj8OG.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 303}, + {"MovieId": "1412", "Title": "sex, lies, and videotape", "Casts": [], "PlotId": "1412", + "ThumbnailIds": ["/kOXATYKziiKmoMxgKA10JOO13JZ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 328}, {"MovieId": "11559", "Title": "Tideland", "Casts": [], "PlotId": "11559", + "ThumbnailIds": ["/xR8vo6xGJbk8QljOlcjLNyLrwB5.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.5, "NumRating": 245}, + {"MovieId": "1554", "Title": "Down by Law", "Casts": [], "PlotId": "1554", + "ThumbnailIds": ["/r4HxZPAhfg2akFaixpVIAM83GCY.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 280}, {"MovieId": "11416", "Title": "The Mission", "Casts": [], "PlotId": "11416", + "ThumbnailIds": ["/oTU13XXz4WHisDWKX3X6dFWEjC0.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.3, "NumRating": 574}, + {"MovieId": "11253", "Title": "Hellboy II: The Golden Army", "Casts": [], "PlotId": "11253", + "ThumbnailIds": ["/fFcZqnWDeQsImDAAIyAimc3SGEl.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 2761}, {"MovieId": "12154", "Title": "3 Men and a Baby", "Casts": [], "PlotId": "12154", + "ThumbnailIds": ["/q4O2ravqDhUTAsshllGv7orgJOO.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.9, "NumRating": 447}, + {"MovieId": "209403", "Title": "Bad Words", "Casts": [], "PlotId": "209403", + "ThumbnailIds": ["/cEnpV3XLvgTp3eVXhY2eyQKZk4r.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 444}, {"MovieId": "9443", "Title": "Chariots of Fire", "Casts": [], "PlotId": "9443", + "ThumbnailIds": ["/Ae5ABhyD30jY9rkciOVCG8nJDwO.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.0, "NumRating": 369}, + {"MovieId": "206647", "Title": "Spectre", "Casts": [], "PlotId": "206647", + "ThumbnailIds": ["/hE24GYddaxB9MVZl1CaiI86M3kp.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 6648}, {"MovieId": "98548", "Title": "People Like Us", "Casts": [], "PlotId": "98548", + "ThumbnailIds": ["/lMAHB1r1vSUOZRKCYR143mm6VMk.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.8, "NumRating": 355}, + {"MovieId": "44629", "Title": "Animal Kingdom", "Casts": [], "PlotId": "44629", + "ThumbnailIds": ["/zhj8YPQKuRev5N3KoHacsPnF4mB.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 395}, {"MovieId": "63493", "Title": "The Ledge", "Casts": [], "PlotId": "63493", + "ThumbnailIds": ["/7iHQhaF2LRGlHpZcjcdODDn6pj.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.1, "NumRating": 183}, + {"MovieId": "1710", "Title": "Copycat", "Casts": [], "PlotId": "1710", + "ThumbnailIds": ["/80czeJGSoik22fhtUM9WzyjUU4r.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 398}, {"MovieId": "10398", "Title": "Double Jeopardy", "Casts": [], "PlotId": "10398", + "ThumbnailIds": ["/9Pl7lqUzU7lxQHYjcmTT6ZvbbDY.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.4, "NumRating": 503}, + {"MovieId": "9607", "Title": "Super Mario Bros.", "Casts": [], "PlotId": "9607", + "ThumbnailIds": ["/bmv7fmcBFzjnJvirfAdZm75qERY.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.1, + "NumRating": 478}, {"MovieId": "3132", "Title": "Bad Company", "Casts": [], "PlotId": "3132", + "ThumbnailIds": ["/v4Jz1ALH2LdIrpr681WVIsYbRQL.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.4, "NumRating": 360}, + {"MovieId": "9879", "Title": "Striptease", "Casts": [], "PlotId": "9879", + "ThumbnailIds": ["/4zMy6R7acotCmGoDk4sjzRtDwKn.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.7, + "NumRating": 351}, {"MovieId": "11929", "Title": "Dolores Claiborne", "Casts": [], "PlotId": "11929", + "ThumbnailIds": ["/ewmVWV0TP8LTlYZ4OzCcguwC9d1.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.2, "NumRating": 310}, + {"MovieId": "9664", "Title": "Flyboys", "Casts": [], "PlotId": "9664", + "ThumbnailIds": ["/ap7UcuDPhyaY165YppEZJdbU1sA.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 414}, {"MovieId": "36819", "Title": "Time Bandits", "Casts": [], "PlotId": "36819", + "ThumbnailIds": ["/4VZtpwdhHQSa4LUkvujyGAHb1hG.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 414}, + {"MovieId": "9566", "Title": "The Fan", "Casts": [], "PlotId": "9566", + "ThumbnailIds": ["/fdW8T9kkYlPyOGh0V5eodFr8SQq.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.9, + "NumRating": 344}, {"MovieId": "9587", "Title": "Little Women", "Casts": [], "PlotId": "9587", + "ThumbnailIds": ["/tD9YDE8pjtSvvKUpkmooE6YDSm8.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.2, "NumRating": 466}, + {"MovieId": "19255", "Title": "Away We Go", "Casts": [], "PlotId": "19255", + "ThumbnailIds": ["/zHDoca5jdrJQ8rwgN9HsWk8HRaG.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 299}, {"MovieId": "14636", "Title": "The Condemned", "Casts": [], "PlotId": "14636", + "ThumbnailIds": ["/dYocAyHDOUg4y4pcetRWqHWH0fi.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 335}, + {"MovieId": "14013", "Title": "BASEketball", "Casts": [], "PlotId": "14013", + "ThumbnailIds": ["/pEg0tYYhSfsyjLIYpvD9lCWLhcH.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 249}, {"MovieId": "9542", "Title": "The Hitcher", "Casts": [], "PlotId": "9542", + "ThumbnailIds": ["/pdPKlwAOX66Zqktp1amFooovAjT.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.0, "NumRating": 337}, + {"MovieId": "141043", "Title": "A Long Way Down", "Casts": [], "PlotId": "141043", + "ThumbnailIds": ["/vIa83hicQj1ZFDG2bWfZaoUoa2e.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 522}, {"MovieId": "10784", "Title": "Cabaret", "Casts": [], "PlotId": "10784", + "ThumbnailIds": ["/1RmUfX3LcS897GhUWrWp5nRADo4.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.5, "NumRating": 389}, + {"MovieId": "113833", "Title": "The Normal Heart", "Casts": [], "PlotId": "113833", + "ThumbnailIds": ["/fIf4nLpWHK8BsbH76fPgMbLSjuU.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.0, + "NumRating": 557}, {"MovieId": "11515", "Title": "Goya's Ghosts", "Casts": [], "PlotId": "11515", + "ThumbnailIds": ["/x04CKIV43Y9K5iTD98fB25lB0F2.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 214}, + {"MovieId": "71864", "Title": "The Odd Life of Timothy Green", "Casts": [], "PlotId": "71864", + "ThumbnailIds": ["/nwWtTWWOJIDU1mM3cBYvSURB03B.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 599}, {"MovieId": "58232", "Title": "Chalet Girl", "Casts": [], "PlotId": "58232", + "ThumbnailIds": ["/64AQrBEKJVSIRaocOCtkMqExToz.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.3, "NumRating": 365}, + {"MovieId": "10588", "Title": "The Cat in the Hat", "Casts": [], "PlotId": "10588", + "ThumbnailIds": ["/cyfCsdxGYIRlMd2z3dncOZWrgvk.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.1, + "NumRating": 715}, {"MovieId": "12155", "Title": "Alice in Wonderland", "Casts": [], "PlotId": "12155", + "ThumbnailIds": ["/pvEE5EN5N1yjmHmldfL4aJWm56l.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 8557}, + {"MovieId": "4599", "Title": "Raising Helen", "Casts": [], "PlotId": "4599", + "ThumbnailIds": ["/oROgudstoH3i4KyI5ZJbBVW9mKK.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 371}, {"MovieId": "28597", "Title": "Problem Child 2", "Casts": [], "PlotId": "28597", + "ThumbnailIds": ["/npr7j2HuRgvsKrXLIxIiXevTH8A.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.3, "NumRating": 218}, + {"MovieId": "2362", "Title": "Westworld", "Casts": [], "PlotId": "2362", + "ThumbnailIds": ["/cOJsaT8jEmG9s1MziVIPpHBRpQ7.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 583}, {"MovieId": "557", "Title": "Spider-Man", "Casts": [], "PlotId": "557", + "ThumbnailIds": ["/A9BYH1DSetvC7bjbHWCaL17Qbp5.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.0, "NumRating": 9635}, + {"MovieId": "401469", "Title": "Widows", "Casts": [], "PlotId": "401469", + "ThumbnailIds": ["/d31SGJSaX29ba5ZUbZcesGoDE7I.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 878}, {"MovieId": "11634", "Title": "Show Me Love", "Casts": [], "PlotId": "11634", + "ThumbnailIds": ["/gkM27kmi9woapo1oQ2lho59u5Hn.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.2, "NumRating": 214}, + {"MovieId": "10403", "Title": "The Player", "Casts": [], "PlotId": "10403", + "ThumbnailIds": ["/eoQ9H9CEySIYSPElKn2VRVY0MEa.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 301}, {"MovieId": "126889", "Title": "Alien: Covenant", "Casts": [], "PlotId": "126889", + "ThumbnailIds": ["/zecMELPbU5YMQpC81Z8ImaaXuf9.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 4894}, + {"MovieId": "21734", "Title": "Shadow of a Doubt", "Casts": [], "PlotId": "21734", + "ThumbnailIds": ["/sqHoIsWkGdPpX6zdPwX6HOMOWjj.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.7, + "NumRating": 379}, {"MovieId": "184314", "Title": "Young & Beautiful", "Casts": [], "PlotId": "184314", + "ThumbnailIds": ["/fqsab9s1yv60bDpak8p7b7R9Dn9.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.3, "NumRating": 581}, + {"MovieId": "9597", "Title": "Vidocq", "Casts": [], "PlotId": "9597", + "ThumbnailIds": ["/7VQgyNCJV1TGpT8yTVDXaADQ5F2.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.7, + "NumRating": 236}, {"MovieId": "10468", "Title": "28 Days", "Casts": [], "PlotId": "10468", + "ThumbnailIds": ["/qTrrElHcONoix4hb4IpLiqEnjlb.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 401}, + {"MovieId": "76341", "Title": "Mad Max: Fury Road", "Casts": [], "PlotId": "76341", + "ThumbnailIds": ["/kqjL17yufvn9OVLyXYpvtyrFfak.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 14217}, {"MovieId": "543540", "Title": "The Perfect Date", "Casts": [], "PlotId": "543540", + "ThumbnailIds": ["/pi7J3iH3Z1NO9q2E13ChY7imyKb.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.4, "NumRating": 1114}, + {"MovieId": "51300", "Title": "The Barber of Siberia", "Casts": [], "PlotId": "51300", + "ThumbnailIds": ["/xganmHk2g5PnzENlkGvg7brbbbM.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 54}, {"MovieId": "10808", "Title": "Dr. Dolittle 2", "Casts": [], "PlotId": "10808", + "ThumbnailIds": ["/aecUc1LobMV56Doj99ZvJR2dXgG.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.1, "NumRating": 862}, + {"MovieId": "62046", "Title": "Flypaper", "Casts": [], "PlotId": "62046", + "ThumbnailIds": ["/czpYP8p0HBXNL3cIsGFMd0a9rSu.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 395}, {"MovieId": "106", "Title": "Predator", "Casts": [], "PlotId": "106", + "ThumbnailIds": ["/gUpto7r2XwoM5eW7MUvd8hl1etB.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 3658}, + {"MovieId": "204", "Title": "The Wages of Fear", "Casts": [], "PlotId": "204", + "ThumbnailIds": ["/3IGuAr1xsErR4XqmHZcfyQ4f8KY.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.1, + "NumRating": 314}, + {"MovieId": "106646", "Title": "The Wolf of Wall Street", "Casts": [], "PlotId": "106646", + "ThumbnailIds": ["/vK1o5rZGqxyovfIhZyMELhk03wO.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.9, + "NumRating": 12171}, + {"MovieId": "13067", "Title": "In the Land of Women", "Casts": [], "PlotId": "13067", + "ThumbnailIds": ["/vyt7OcWLFc0OsnNSh8XY7Z2bCur.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 265}, {"MovieId": "209263", "Title": "Enough Said", "Casts": [], "PlotId": "209263", + "ThumbnailIds": ["/gPJ7KyzdHVpCaxdiq22sPEmNJZV.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 461}, + {"MovieId": "484247", "Title": "A Simple Favor", "Casts": [], "PlotId": "484247", + "ThumbnailIds": ["/5EJWZQ8dh99hfgXP9zAD5Ak5Hrn.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 1675}, + {"MovieId": "567604", "Title": "Once Upon a Deadpool", "Casts": [], "PlotId": "567604", + "ThumbnailIds": ["/5Ka49BWWyKMXr93YMbH5wLN7aAM.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 213}, {"MovieId": "13576", "Title": "This Is It", "Casts": [], "PlotId": "13576", + "ThumbnailIds": ["/4ZgT8FRJxxpG0mgOIPeCgjzAsBR.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.8, "NumRating": 420}, + {"MovieId": "34480", "Title": "The Descent: Part 2", "Casts": [], "PlotId": "34480", + "ThumbnailIds": ["/4rZtrCSvDjK43kO2rK2z9FaytlW.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 468}, {"MovieId": "44147", "Title": "Wild Target", "Casts": [], "PlotId": "44147", + "ThumbnailIds": ["/x1O8MOIY41fpKbCUt1I4sKeLwwr.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.4, "NumRating": 352}, + {"MovieId": "1023", "Title": "Adam's Apples", "Casts": [], "PlotId": "1023", + "ThumbnailIds": ["/ysLgwV21cnoI9nkWIvdZWqJuwjE.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 296}, {"MovieId": "150540", "Title": "Inside Out", "Casts": [], "PlotId": "150540", + "ThumbnailIds": ["/aAmfIX3TT40zUHGcCKrlOZRKC7u.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.0, "NumRating": 12454}, + {"MovieId": "234200", "Title": "Pride", "Casts": [], "PlotId": "234200", + "ThumbnailIds": ["/fk81iMvZTYo7MaYWGirPc0uzA55.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.9, + "NumRating": 725}, {"MovieId": "360920", "Title": "The Grinch", "Casts": [], "PlotId": "360920", + "ThumbnailIds": ["/gpkHvkCtZOeCQ2DelnJ2LB1WjZ5.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.4, "NumRating": 1005}, + {"MovieId": "2665", "Title": "Airplane II: The Sequel", "Casts": [], "PlotId": "2665", + "ThumbnailIds": ["/pjI6j5sVTxJXuxnr2JM2FgvyFXS.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 459}, + {"MovieId": "318121", "Title": "The Fundamentals of Caring", "Casts": [], "PlotId": "318121", + "ThumbnailIds": ["/k1JZUr1wwpEdTwSVQdZWheRAIui.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 1181}, {"MovieId": "24056", "Title": "The Tournament", "Casts": [], "PlotId": "24056", + "ThumbnailIds": ["/ldqexoGgxnuV1hpX64MR6WnyEVG.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.8, "NumRating": 282}, + {"MovieId": "11562", "Title": "Crimes and Misdemeanors", "Casts": [], "PlotId": "11562", + "ThumbnailIds": ["/8I7Dzaah4FD6PTUjwIFm4H6Md0U.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 375}, {"MovieId": "394741", "Title": "Stan & Ollie", "Casts": [], "PlotId": "394741", + "ThumbnailIds": ["/8qDBDXA8Od8gc4IQMnoXUKyj8Pf.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 207}, + {"MovieId": "73937", "Title": "The Big Year", "Casts": [], "PlotId": "73937", + "ThumbnailIds": ["/kBnR6jbFRnbSQTqLEcDpKd7FxK5.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 383}, {"MovieId": "10207", "Title": "Message in a Bottle", "Casts": [], "PlotId": "10207", + "ThumbnailIds": ["/majQar87QNbrC47qYMcmK1oCohZ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.1, "NumRating": 373}, + {"MovieId": "10757", "Title": "Kabhi Khushi Kabhie Gham", "Casts": [], "PlotId": "10757", + "ThumbnailIds": ["/1WfrkYkYL52JQCMtoeMbWdjRmv6.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 194}, {"MovieId": "1833", "Title": "Rent", "Casts": [], "PlotId": "1833", + "ThumbnailIds": ["/c3GY40OxnPQZZPJk6WOShw63HGn.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.9, "NumRating": 284}, + {"MovieId": "2742", "Title": "Naked Lunch", "Casts": [], "PlotId": "2742", + "ThumbnailIds": ["/tMninSG4OAAhx9SNjdnMJM1R3Wn.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.1, + "NumRating": 404}, {"MovieId": "10762", "Title": "Without a Paddle", "Casts": [], "PlotId": "10762", + "ThumbnailIds": ["/q1jdD13xunSf22Ts4pAhVbVJ6IH.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.4, "NumRating": 324}, + {"MovieId": "250066", "Title": "American Heist", "Casts": [], "PlotId": "250066", + "ThumbnailIds": ["/eqqf7DvhOBaEHiqLMkMMp0e8wzp.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.6, + "NumRating": 210}, {"MovieId": "245706", "Title": "True Story", "Casts": [], "PlotId": "245706", + "ThumbnailIds": ["/hBG9DzRbzId4uC4aUv3XTDZCn4i.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.2, "NumRating": 881}, + {"MovieId": "6471", "Title": "The Jerk", "Casts": [], "PlotId": "6471", + "ThumbnailIds": ["/wb6mB6R9vscPhyAegbbNDFZUAIs.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 410}, {"MovieId": "2062", "Title": "Ratatouille", "Casts": [], "PlotId": "2062", + "ThumbnailIds": ["/xVxxSYHAfrEbllyWFQG5df5nwH4.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.6, "NumRating": 8847}, + {"MovieId": "11030", "Title": "Zelig", "Casts": [], "PlotId": "11030", + "ThumbnailIds": ["/5dgjgBMWHcOrXL7EbGnU73WpXNB.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 380}, {"MovieId": "1613", "Title": "The 51st State", "Casts": [], "PlotId": "1613", + "ThumbnailIds": ["/hLtjOggvtlpBQ9tRNp5OwMw5mIk.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.2, "NumRating": 279}, + {"MovieId": "10495", "Title": "The Karate Kid, Part III", "Casts": [], "PlotId": "10495", + "ThumbnailIds": ["/2Z0EJl11kOSPMMvHqZ4r5Csh7Ph.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.7, + "NumRating": 609}, + {"MovieId": "50837", "Title": "Martha Marcy May Marlene", "Casts": [], "PlotId": "50837", + "ThumbnailIds": ["/f68uBuxkEfesHmw5eJxFOnNalTY.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 409}, {"MovieId": "2039", "Title": "Moonstruck", "Casts": [], "PlotId": "2039", + "ThumbnailIds": ["/2OIxyH2zMPYBzaECdIlX81Qc398.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.8, "NumRating": 347}, + {"MovieId": "11453", "Title": "Deuce Bigalow: European Gigolo", "Casts": [], "PlotId": "11453", + "ThumbnailIds": ["/1P8fWex0NzgtPE4cgIUDHrVSARM.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.7, + "NumRating": 406}, {"MovieId": "85", "Title": "Raiders of the Lost Ark", "Casts": [], "PlotId": "85", + "ThumbnailIds": ["/44sKJOGP3fTm4QXBcIuqu0RkdP7.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.9, "NumRating": 6505}, + {"MovieId": "332", "Title": "Inspector Gadget", "Casts": [], "PlotId": "332", + "ThumbnailIds": ["/kvlKBGtyyNVyGJB72aD1vwPk2d4.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.4, + "NumRating": 602}, {"MovieId": "4133", "Title": "Blow", "Casts": [], "PlotId": "4133", + "ThumbnailIds": ["/yCLLbZzAa7jreGus7pvjZmL0bj7.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 2314}, + {"MovieId": "49950", "Title": "The Roommate", "Casts": [], "PlotId": "49950", + "ThumbnailIds": ["/yvKZBncLbuNd3HX7dcqcepq83qy.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.3, + "NumRating": 401}, {"MovieId": "11006", "Title": "Smokey and the Bandit", "Casts": [], "PlotId": "11006", + "ThumbnailIds": ["/5uTIEUSBVzmEZ8TnFEZbCCOVHPj.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.8, "NumRating": 306}, + {"MovieId": "10060", "Title": "Get Rich or Die Tryin'", "Casts": [], "PlotId": "10060", + "ThumbnailIds": ["/wKeSnhQfdwrycHorc9OPQ5KxVxJ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 351}, {"MovieId": "11324", "Title": "Shutter Island", "Casts": [], "PlotId": "11324", + "ThumbnailIds": ["/aZqKsvpJDFy2UzUMsdskNFbfkOd.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.1, "NumRating": 12391}, + {"MovieId": "576071", "Title": "Unplanned", "Casts": [], "PlotId": "576071", + "ThumbnailIds": ["/hQvf3RHgmp4XXXl2y6zhMe4G4kg.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 22}, + {"MovieId": "11658", "Title": "Tae Guk Gi: The Brotherhood of War", "Casts": [], "PlotId": "11658", + "ThumbnailIds": ["/1SEDI2qeNZgBK5XiJfKwWTmhZqC.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 217}, + {"MovieId": "11850", "Title": "Invasion of the Body Snatchers", "Casts": [], "PlotId": "11850", + "ThumbnailIds": ["/skS02wdeH2C0nrbCQP3qKwJdZtZ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 384}, + {"MovieId": "98566", "Title": "Teenage Mutant Ninja Turtles", "Casts": [], "PlotId": "98566", + "ThumbnailIds": ["/oDL2ryJ0sV2bmjgshVgJb3qzvwp.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 4192}, {"MovieId": "10488", "Title": "Nim's Island", "Casts": [], "PlotId": "10488", + "ThumbnailIds": ["/wNSQsaz1btAHSZ6vNJpjbVmQWQG.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.8, "NumRating": 630}, + {"MovieId": "2000", "Title": "Aguirre: The Wrath of God", "Casts": [], "PlotId": "2000", + "ThumbnailIds": ["/uHP3AtAnudp5w8d3jx2el1AtV6a.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 400}, {"MovieId": "11566", "Title": "Dave", "Casts": [], "PlotId": "11566", + "ThumbnailIds": ["/nb2tSTxjhuiJqn9R6BEcwopq0dW.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.5, "NumRating": 329}, + {"MovieId": "10907", "Title": "The Adventures of Robin Hood", "Casts": [], "PlotId": "10907", + "ThumbnailIds": ["/lEx5IKjqwYS7PcGnIXivYaDMzQr.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 286}, {"MovieId": "10412", "Title": "Romper Stomper", "Casts": [], "PlotId": "10412", + "ThumbnailIds": ["/wW9J9InM3gdZS6vcoJVQJo9pIVj.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 224}, + {"MovieId": "30197", "Title": "The Producers", "Casts": [], "PlotId": "30197", + "ThumbnailIds": ["/xgKikS0QQcWS1CvhjbemeoBFd32.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 293}, {"MovieId": "402900", "Title": "Ocean's Eight", "Casts": [], "PlotId": "402900", + "ThumbnailIds": ["/MvYpKlpFukTivnlBhizGbkAe3v.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.9, "NumRating": 3548}, + {"MovieId": "14292", "Title": "Miracle", "Casts": [], "PlotId": "14292", + "ThumbnailIds": ["/u2OCExn2VMgEQnVeoGaHl9bSrtQ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.1, + "NumRating": 264}, {"MovieId": "5820", "Title": "The Sentinel", "Casts": [], "PlotId": "5820", + "ThumbnailIds": ["/wUE2EsKkktVz8IiTzrpO6GbI6gh.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 355}, + {"MovieId": "16899", "Title": "Easy Virtue", "Casts": [], "PlotId": "16899", + "ThumbnailIds": ["/1VO09so985ZSbEj0JnuebHMqTip.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 191}, + {"MovieId": "6878", "Title": "Homeward Bound: The Incredible Journey", "Casts": [], "PlotId": "6878", + "ThumbnailIds": ["/el6dJEpK97OJRQiQhuiSGk2jkV5.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 412}, {"MovieId": "472451", "Title": "Boy Erased", "Casts": [], "PlotId": "472451", + "ThumbnailIds": ["/oZbhTdi0ZQY7iiSQ0L7h3ya6NDF.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.2, "NumRating": 374}, + {"MovieId": "8764", "Title": "Top Secret!", "Casts": [], "PlotId": "8764", + "ThumbnailIds": ["/2ArC4IAAGXf3hTVJCzQfE0G5Vbf.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 428}, {"MovieId": "13595", "Title": "Airheads", "Casts": [], "PlotId": "13595", + "ThumbnailIds": ["/jx06ghWxAC0BdrxUa03rti81RcY.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.8, "NumRating": 341}, + {"MovieId": "83", "Title": "Open Water", "Casts": [], "PlotId": "83", + "ThumbnailIds": ["/hua2eluUhiLvKqwHFPV2aTiY8pp.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.4, + "NumRating": 558}, {"MovieId": "2313", "Title": "Prime", "Casts": [], "PlotId": "2313", + "ThumbnailIds": ["/dQxtI83slU5fAq6WZjEmIDAtYvM.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 456}, + {"MovieId": "11787", "Title": "Harvey", "Casts": [], "PlotId": "11787", + "ThumbnailIds": ["/dgd82hYmpiXDM1G867HqNaWe8wj.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.8, + "NumRating": 274}, {"MovieId": "636", "Title": "THX 1138", "Casts": [], "PlotId": "636", + "ThumbnailIds": ["/8cie5mojY6MlIrYMs9EtNSyterv.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 414}, + {"MovieId": "13193", "Title": "Saved!", "Casts": [], "PlotId": "13193", + "ThumbnailIds": ["/sh61XwFfemE6MrDzAVNpJb6ZRgR.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 223}, {"MovieId": "12093", "Title": "Lilya 4-ever", "Casts": [], "PlotId": "12093", + "ThumbnailIds": ["/1uqtBLEXFO6qxofV9Hm1SigE3WH.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.8, "NumRating": 239}, + {"MovieId": "604", "Title": "The Matrix Reloaded", "Casts": [], "PlotId": "604", + "ThumbnailIds": ["/ezIurBz2fdUc68d98Fp9dRf5ihv.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 5597}, {"MovieId": "44716", "Title": "In a Better World", "Casts": [], "PlotId": "44716", + "ThumbnailIds": ["/qxCTUrnC5ZdsKQepBMPt09ihse.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.3, "NumRating": 185}, + {"MovieId": "588001", "Title": "Despite Everything", "Casts": [], "PlotId": "588001", + "ThumbnailIds": ["/1GH4KCS8IgWcDt5toXYFYX5AmX4.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 60}, + {"MovieId": "57158", "Title": "The Hobbit: The Desolation of Smaug", "Casts": [], "PlotId": "57158", + "ThumbnailIds": ["/gQCiuxGsfiXH1su6lp9n0nd0UeH.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 7602}, {"MovieId": "500904", "Title": "A Vigilante", "Casts": [], "PlotId": "500904", + "ThumbnailIds": ["/avoKZfgBzyBGJsrAr2WQOwZK978.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 4.7, "NumRating": 53}, + {"MovieId": "595680", "Title": "Il grande spirito", "Casts": [], "PlotId": "595680", + "ThumbnailIds": ["/hFuxBhwfj75c0fy1byTheD1aAKC.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "260", "Title": "The 39 Steps", "Casts": [], "PlotId": "260", + "ThumbnailIds": ["/9v283GWj9a0AC8wwC4zriNqY1lZ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 376}, + {"MovieId": "17295", "Title": "The Battle of Algiers", "Casts": [], "PlotId": "17295", + "ThumbnailIds": ["/zsNc43QfSqeMBW186o9Fozfmkst.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.0, + "NumRating": 279}, + {"MovieId": "234", "Title": "The Cabinet of Dr. Caligari", "Casts": [], "PlotId": "234", + "ThumbnailIds": ["/myK9DeIsXWGKgUTZyGXg2IfFk0W.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.9, + "NumRating": 536}, {"MovieId": "12690", "Title": "Appaloosa", "Casts": [], "PlotId": "12690", + "ThumbnailIds": ["/ar26XwTJz6BPFRricdNpMctyjB0.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.5, "NumRating": 475}, + {"MovieId": "1694", "Title": "Re-Animator", "Casts": [], "PlotId": "1694", + "ThumbnailIds": ["/hJnP2O0uTuh8HsR094WLTi3sQwc.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.1, + "NumRating": 472}, + {"MovieId": "780", "Title": "The Passion of Joan of Arc", "Casts": [], "PlotId": "780", + "ThumbnailIds": ["/5HL0dEJfd7PF0eRiKz8BiNfe8Tf.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.1, + "NumRating": 319}, {"MovieId": "19833", "Title": "In the Loop", "Casts": [], "PlotId": "19833", + "ThumbnailIds": ["/jL6txnziFSeEifQkqnPBtaPaiXU.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.2, "NumRating": 293}, + {"MovieId": "405774", "Title": "Bird Box", "Casts": [], "PlotId": "405774", + "ThumbnailIds": ["/rGfGfgL2pEPCfhIvqHXieXFn7gp.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 4562}, {"MovieId": "452832", "Title": "Serenity", "Casts": [], "PlotId": "452832", + "ThumbnailIds": ["/hgWAcic93phg4DOuQ8NrsgQWiqu.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.1, "NumRating": 282}, + {"MovieId": "10136", "Title": "The Golden Child", "Casts": [], "PlotId": "10136", + "ThumbnailIds": ["/nVJIyS2gh0hBvVEr8s9SrpSBTxL.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.7, + "NumRating": 495}, {"MovieId": "531", "Title": "The Wrong Trousers", "Casts": [], "PlotId": "531", + "ThumbnailIds": ["/O3fFWazkIL1QrmH5t9numsUgmR.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.6, "NumRating": 457}, + {"MovieId": "581585", "Title": "A Regular Woman", "Casts": [], "PlotId": "581585", + "ThumbnailIds": ["/eXArCCQj3W4b55SohTo5fBHiu7G.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "41154", "Title": "Men in Black 3", "Casts": [], "PlotId": "41154", + "ThumbnailIds": ["/l9hrvXyGq19f6jPRZhSVRibTMwW.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.4, "NumRating": 6297}, + {"MovieId": "956", "Title": "Mission: Impossible III", "Casts": [], "PlotId": "956", + "ThumbnailIds": ["/qjy8ABAbWooV4jLG6UjzDHlv4RB.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 3513}, {"MovieId": "13532", "Title": "Fanboys", "Casts": [], "PlotId": "13532", + "ThumbnailIds": ["/ywI92p2x7D96nuEayGVOMll35SF.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.2, "NumRating": 436}, + {"MovieId": "351044", "Title": "Welcome to Marwen", "Casts": [], "PlotId": "351044", + "ThumbnailIds": ["/o45VIAUYDcVCGuzd43l8Sr5Dfti.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 241}, + {"MovieId": "9876", "Title": "Stop! Or My Mom Will Shoot", "Casts": [], "PlotId": "9876", + "ThumbnailIds": ["/sQXdlCaX6demEdOgI1FbV9fX8aO.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.9, + "NumRating": 410}, + {"MovieId": "595188", "Title": "Pariban : Idola Dari Tanah Jawa", "Casts": [], "PlotId": "595188", + "ThumbnailIds": ["/yxiDlS5tKpuPHHl4eOLgJCdTqU6.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "10985", "Title": "The New Guy", "Casts": [], "PlotId": "10985", + "ThumbnailIds": ["/uqUVToyQmcqBqm7USWEQIYDNERd.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.6, "NumRating": 251}, + {"MovieId": "11469", "Title": "Black Knight", "Casts": [], "PlotId": "11469", + "ThumbnailIds": ["/xJnGEOnYUelD6wLgpQfNg5tq8jV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.2, + "NumRating": 533}, {"MovieId": "11377", "Title": "House on Haunted Hill", "Casts": [], "PlotId": "11377", + "ThumbnailIds": ["/yedchBbI23FgDjWP1tvahOZgiks.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.5, "NumRating": 373}, + {"MovieId": "13291", "Title": "Traitor", "Casts": [], "PlotId": "13291", + "ThumbnailIds": ["/euLk9RxsPHfpyWUHzTDNY6Dksch.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 321}, + {"MovieId": "10242", "Title": "What Ever Happened to Baby Jane?", "Casts": [], "PlotId": "10242", + "ThumbnailIds": ["/t2hPlHc2pFweBqQgrsNfSLNIv1j.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.1, + "NumRating": 418}, + {"MovieId": "887", "Title": "The Best Years of Our Lives", "Casts": [], "PlotId": "887", + "ThumbnailIds": ["/fxjWVRlmD0EKn8ZgFKREqpkTiRH.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.8, + "NumRating": 236}, {"MovieId": "21755", "Title": "The Brothers Bloom", "Casts": [], "PlotId": "21755", + "ThumbnailIds": ["/xIz8iwzyjgWGzcIwHKpqhBs77ML.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 318}, + {"MovieId": "4105", "Title": "Black Rain", "Casts": [], "PlotId": "4105", + "ThumbnailIds": ["/dmMIMuByEbzE0x73gjc9YcDjjlx.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 434}, {"MovieId": "5961", "Title": "Fanny & Alexander", "Casts": [], "PlotId": "5961", + "ThumbnailIds": ["/zZVkPy2PJuWWZbYGXG38a1nZp7l.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.8, "NumRating": 275}, + {"MovieId": "10494", "Title": "Perfect Blue", "Casts": [], "PlotId": "10494", + "ThumbnailIds": ["/sxBzVuwqIABKIbdij7lOrRvDb15.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.1, + "NumRating": 638}, {"MovieId": "353576", "Title": "The Con Is On", "Casts": [], "PlotId": "353576", + "ThumbnailIds": ["/d4ZzzFOokK356jf6L6vUp3RKNvM.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 4.0, "NumRating": 54}, + {"MovieId": "181533", "Title": "Night at the Museum: Secret of the Tomb", "Casts": [], "PlotId": "181533", + "ThumbnailIds": ["/tWwASv4CU1Au1IukacdSUewDCV3.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 3467}, {"MovieId": "854", "Title": "The Mask", "Casts": [], "PlotId": "854", + "ThumbnailIds": ["/v8x8p441l1Bep8p82pAG6rduBoK.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.8, "NumRating": 5123}, + {"MovieId": "449924", "Title": "Ip Man 4", "Casts": [], "PlotId": "449924", + "ThumbnailIds": ["/mAWBfTDAmfpvQGMVFuzuVl49N1P.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "607", "Title": "Men in Black", "Casts": [], "PlotId": "607", + "ThumbnailIds": ["/f24UVKq3UiQWLqGWdqjwkzgB8j8.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.0, "NumRating": 7726}, + {"MovieId": "499533", "Title": "Float Like A Butterfly", "Casts": [], "PlotId": "499533", + "ThumbnailIds": ["/qOLSeKboDzZ1lPfuMTFWOj9Xl0z.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "6623", "Title": "The Peacemaker", "Casts": [], "PlotId": "6623", + "ThumbnailIds": ["/hc3p6pvIrfO4AmrVLb6qTviOwW2.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.8, "NumRating": 446}, + {"MovieId": "10681", "Title": "WALL\u00b7E", "Casts": [], "PlotId": "10681", + "ThumbnailIds": ["/9cJETuLMc6R0bTWRA5i7ctY9bxk.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.0, + "NumRating": 10623}, + {"MovieId": "16869", "Title": "Inglourious Basterds", "Casts": [], "PlotId": "16869", + "ThumbnailIds": ["/ai0LXkzVM3hMjDhvFdKMUemoBe.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.1, + "NumRating": 11739}, {"MovieId": "49012", "Title": "Arthur", "Casts": [], "PlotId": "49012", + "ThumbnailIds": ["/28HW1Kc4OwQH50M0XmcEuIYrCWk.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.4, "NumRating": 395}, + {"MovieId": "9612", "Title": "Coneheads", "Casts": [], "PlotId": "9612", + "ThumbnailIds": ["/9vuFI0xV1yZfsWr23evJlkRDL8j.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.0, + "NumRating": 394}, {"MovieId": "218", "Title": "The Terminator", "Casts": [], "PlotId": "218", + "ThumbnailIds": ["/q8ffBuxQlYOHrvPniLgCbmKK4Lv.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.5, "NumRating": 6691}, + {"MovieId": "4244", "Title": "The Kid", "Casts": [], "PlotId": "4244", + "ThumbnailIds": ["/bTsn3l3QLmtKGPnOkKWpLObOgu7.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 429}, + {"MovieId": "10152", "Title": "Dumb and Dumberer: When Harry Met Lloyd", "Casts": [], "PlotId": "10152", + "ThumbnailIds": ["/eizaKEnF108gQq89f1XsAyVxjq6.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.1, + "NumRating": 395}, {"MovieId": "2925", "Title": "The First Wives Club", "Casts": [], "PlotId": "2925", + "ThumbnailIds": ["/k4gb8CcRgvYM0XB0SBEiWzBhe3f.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 331}, + {"MovieId": "103328", "Title": "Holy Motors", "Casts": [], "PlotId": "103328", + "ThumbnailIds": ["/d5amUFExQCqLkRpWQ6QspBtyWUe.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 418}, + {"MovieId": "8010", "Title": "Highlander 2: The Quickening", "Casts": [], "PlotId": "8010", + "ThumbnailIds": ["/7W43CXQh6jF9NhnBnlA4BoaFp8W.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.6, + "NumRating": 352}, {"MovieId": "324786", "Title": "Hacksaw Ridge", "Casts": [], "PlotId": "324786", + "ThumbnailIds": ["/bndiUFfJxNd2fYx8XO610L9a07m.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.1, "NumRating": 6488}, + {"MovieId": "424139", "Title": "Halloween", "Casts": [], "PlotId": "424139", + "ThumbnailIds": ["/lNkDYKmrVem1J0aAfCnQlJOCKnT.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 1913}, + {"MovieId": "285", "Title": "Pirates of the Caribbean: At World's End", "Casts": [], "PlotId": "285", + "ThumbnailIds": ["/bXb00CkHqx7TPchTGG131sWV59y.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.1, + "NumRating": 8203}, {"MovieId": "584804", "Title": "Kleine Germanen", "Casts": [], "PlotId": "584804", + "ThumbnailIds": ["/uM6JkQRqAd4Q4MVAxAn8rayrI1.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 0.0, "NumRating": 0}, + {"MovieId": "483157", "Title": "Morning Has Broken", "Casts": [], "PlotId": "483157", + "ThumbnailIds": [None], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, "NumRating": 0}, + {"MovieId": "11560", "Title": "High Crimes", "Casts": [], "PlotId": "11560", + "ThumbnailIds": ["/adujr0eiutryiWmK8i0DPGTrOpU.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 321}, {"MovieId": "10956", "Title": "Joe Dirt", "Casts": [], "PlotId": "10956", + "ThumbnailIds": ["/8FKSjl4cEg4OEyWdCTjqAT8M6qv.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.4, "NumRating": 360}, + {"MovieId": "585", "Title": "Monsters, Inc.", "Casts": [], "PlotId": "585", + "ThumbnailIds": ["/93Y9BGx8blzmZOPSoivkFfaifqU.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.8, + "NumRating": 10614}, + {"MovieId": "1865", "Title": "Pirates of the Caribbean: On Stranger Tides", "Casts": [], "PlotId": "1865", + "ThumbnailIds": ["/wNUDAq5OUMOtxMlz64YaCp7gZma.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 8421}, {"MovieId": "8092", "Title": "This Boy\u2019s Life", "Casts": [], "PlotId": "8092", + "ThumbnailIds": ["/xs8ebrGRGu6Y9ebu2dJFdm9yaZP.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.1, "NumRating": 376}, + {"MovieId": "569064", "Title": "Just Say Goodbye", "Casts": [], "PlotId": "569064", + "ThumbnailIds": ["/fHVlfvADGo9EMfEU6iMYsrTkusW.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "101173", "Title": "Coriolanus", "Casts": [], "PlotId": "101173", + "ThumbnailIds": ["/yE026snGPK1Cm61y6qmPdX5Hh8h.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 210}, + {"MovieId": "9087", "Title": "The American President", "Casts": [], "PlotId": "9087", + "ThumbnailIds": ["/yObOAYFIHXHkFPQ3jhgkN2ezaD.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 318}, {"MovieId": "9464", "Title": "Buffalo '66", "Casts": [], "PlotId": "9464", + "ThumbnailIds": ["/taVFuUhUWoX9YE7bb2bWkSPjC9P.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.2, "NumRating": 336}, + {"MovieId": "11260", "Title": "Meet Dave", "Casts": [], "PlotId": "11260", + "ThumbnailIds": ["/gQKUAQGaNIsUhGL07zdhcrPyWdV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.1, + "NumRating": 704}, {"MovieId": "8882", "Title": "Gomorrah", "Casts": [], "PlotId": "8882", + "ThumbnailIds": ["/6aVJi30jxywKXbrpcBD11AuVoKv.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.9, "NumRating": 519}, + {"MovieId": "9515", "Title": "The Matador", "Casts": [], "PlotId": "9515", + "ThumbnailIds": ["/u4Pfeuz52JPBdxwxpMpyMApWltB.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 220}, {"MovieId": "34769", "Title": "Defendor", "Casts": [], "PlotId": "34769", + "ThumbnailIds": ["/bGwWUN9aqglafPmZRNlW3tmspuZ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.4, "NumRating": 290}, + {"MovieId": "11197", "Title": "Evil", "Casts": [], "PlotId": "11197", + "ThumbnailIds": ["/1ULoIF7u3hVoxOij4GMaqlYZRxc.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 142}, {"MovieId": "97051", "Title": "Would You Rather", "Casts": [], "PlotId": "97051", + "ThumbnailIds": ["/yOAU7HHVw4RrDGK97i6ll46JVfj.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.8, "NumRating": 456}, + {"MovieId": "983", "Title": "The Man Who Would Be King", "Casts": [], "PlotId": "983", + "ThumbnailIds": ["/21BANIzXEKyZDUFOr9NdUEgP4EA.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 242}, {"MovieId": "445651", "Title": "The Darkest Minds", "Casts": [], "PlotId": "445651", + "ThumbnailIds": ["/94RaS52zmsqaiAe1TG20pdbJCZr.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.8, "NumRating": 1119}, + {"MovieId": "447332", "Title": "A Quiet Place", "Casts": [], "PlotId": "447332", + "ThumbnailIds": ["/nAU74GmpUk7t5iklEp3bufwDq4n.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 5654}, {"MovieId": "11686", "Title": "Love and Death", "Casts": [], "PlotId": "11686", + "ThumbnailIds": ["/qRNisGLmcHoEvMjzcvVlcdCnOhO.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.8, "NumRating": 349}, + {"MovieId": "426543", "Title": "The Nutcracker and the Four Realms", "Casts": [], "PlotId": "426543", + "ThumbnailIds": ["/tysit5m7HSEvf1wknPR0DeEhR7e.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 838}, {"MovieId": "646", "Title": "Dr. No", "Casts": [], "PlotId": "646", + "ThumbnailIds": ["/gRdfLVVf6FheOw6mw6wOsKhZG1l.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.0, "NumRating": 1578}, + {"MovieId": "293", "Title": "A River Runs Through It", "Casts": [], "PlotId": "293", + "ThumbnailIds": ["/4nfpZVadu7nCJETENNhVWfR3okU.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 433}, {"MovieId": "7300", "Title": "One Fine Day", "Casts": [], "PlotId": "7300", + "ThumbnailIds": ["/yJETLNpYeEpDar7tJK1KedYGLgx.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.3, "NumRating": 308}, + {"MovieId": "11374", "Title": "Edtv", "Casts": [], "PlotId": "11374", + "ThumbnailIds": ["/sF3YBtYn6yAHMPDbiC58WArIWMl.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.9, + "NumRating": 286}, + {"MovieId": "63815", "Title": "Something Something... Unakkum Enakkum", "Casts": [], "PlotId": "63815", + "ThumbnailIds": ["/7bkeiY70hQZ3xVVNLwkmezpa0FC.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.9, + "NumRating": 9}, {"MovieId": "679", "Title": "Aliens", "Casts": [], "PlotId": "679", + "ThumbnailIds": ["/nORMXEkYEbzkU5WkMWMgRDJwjSZ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.9, "NumRating": 5025}, + {"MovieId": "11051", "Title": "The Last Temptation of Christ", "Casts": [], "PlotId": "11051", + "ThumbnailIds": ["/dutPJWBeQgdfjRqeKojqmyIdXFd.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 401}, {"MovieId": "238628", "Title": "Tangerines", "Casts": [], "PlotId": "238628", + "ThumbnailIds": ["/5PINEEPwh11MjUKw4M58NcVoCCb.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.8, "NumRating": 188}, + {"MovieId": "268896", "Title": "Pacific Rim: Uprising", "Casts": [], "PlotId": "268896", + "ThumbnailIds": ["/v5HlmJK9bdeHxN2QhaFP1ivjX3U.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.9, + "NumRating": 2153}, + {"MovieId": "631", "Title": "Sunrise: A Song of Two Humans", "Casts": [], "PlotId": "631", + "ThumbnailIds": ["/hEDMD8Lu7tMurqIglE8mo4GKBN.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.0, + "NumRating": 301}, {"MovieId": "38363", "Title": "Fair Game", "Casts": [], "PlotId": "38363", + "ThumbnailIds": ["/yE8uba2FkmFagMpeNNoo0cPyJ7D.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 379}, + {"MovieId": "1880", "Title": "Red Dawn", "Casts": [], "PlotId": "1880", + "ThumbnailIds": ["/hVyeN1aFmqLsRK9VNElETYBDtnf.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 338}, {"MovieId": "11141", "Title": "Laws of Attraction", "Casts": [], "PlotId": "11141", + "ThumbnailIds": ["/raE9gFRS2oH4jBBjwIJvJresOQn.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.7, "NumRating": 197}, + {"MovieId": "11158", "Title": "Honey I Blew Up the Kid", "Casts": [], "PlotId": "11158", + "ThumbnailIds": ["/gwda8CnPAfHGwygclGiKddt60fX.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.0, + "NumRating": 545}, {"MovieId": "10186", "Title": "The Rocker", "Casts": [], "PlotId": "10186", + "ThumbnailIds": ["/yQfng0iKQVuGDdI5JeoKPINlXuH.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.8, "NumRating": 274}, + {"MovieId": "4520", "Title": "Sleuth", "Casts": [], "PlotId": "4520", + "ThumbnailIds": ["/5659i917XvBIf3i5fCPrOmCBja5.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 261}, {"MovieId": "1792", "Title": "Stuck on You", "Casts": [], "PlotId": "1792", + "ThumbnailIds": ["/gUbNzcIdKCoqp9amNFpFQfpBzrB.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.3, "NumRating": 324}, + {"MovieId": "10122", "Title": "Flight of the Navigator", "Casts": [], "PlotId": "10122", + "ThumbnailIds": ["/69kD2bpkwSadpN30JB9vJrlz8HW.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 341}, {"MovieId": "9909", "Title": "Dangerous Minds", "Casts": [], "PlotId": "9909", + "ThumbnailIds": ["/y5Jee3QmYOlpqfaPPbfvtdVc5wj.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.5, "NumRating": 425}, + {"MovieId": "119675", "Title": "Behind the Candelabra", "Casts": [], "PlotId": "119675", + "ThumbnailIds": ["/46J5VoqnneIz3hs50Ptk2o5bmXB.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 450}, {"MovieId": "185", "Title": "A Clockwork Orange", "Casts": [], "PlotId": "185", + "ThumbnailIds": ["/4sHeTAp65WrSSuc05nRBKddhBxO.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.2, "NumRating": 6563}, + {"MovieId": "9966", "Title": "The Messengers", "Casts": [], "PlotId": "9966", + "ThumbnailIds": ["/x2Z5iXhHaoK22vwfxju5vq25apS.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.5, + "NumRating": 387}, {"MovieId": "64639", "Title": "Straw Dogs", "Casts": [], "PlotId": "64639", + "ThumbnailIds": ["/hSW5Msz5Cr6vUQxChPSPdxufnss.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.7, "NumRating": 246}, + {"MovieId": "1406", "Title": "City Slickers", "Casts": [], "PlotId": "1406", + "ThumbnailIds": ["/tdOHDekHHGyjcR3Ay6WQ6uiFHoz.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 358}, + {"MovieId": "259316", "Title": "Fantastic Beasts and Where to Find Them", "Casts": [], "PlotId": "259316", + "ThumbnailIds": ["/1M91Bt3oGspda75H9eLqYZkJzgO.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 11951}, + {"MovieId": "512954", "Title": "Leaving Afghanistan", "Casts": [], "PlotId": "512954", + "ThumbnailIds": ["/aqNiKKr5fvJqCR4LEb1pcw48k1y.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "940", "Title": "The Lady Vanishes", "Casts": [], "PlotId": "940", + "ThumbnailIds": ["/edL8YmyR1BjICz3mp1fhUqSOPnF.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.6, "NumRating": 313}, + {"MovieId": "11854", "Title": "Kuch Kuch Hota Hai", "Casts": [], "PlotId": "11854", + "ThumbnailIds": ["/hS1BvHAi29RXhKTyCoOEen0uU6j.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 162}, {"MovieId": "2771", "Title": "American Splendor", "Casts": [], "PlotId": "2771", + "ThumbnailIds": ["/qIJHt9PO13mAuFnHgrJUAq5p8Bf.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.3, "NumRating": 192}, + {"MovieId": "704", "Title": "A Hard Day's Night", "Casts": [], "PlotId": "704", + "ThumbnailIds": ["/raJc1rxX0SQGzU1sRBAAFLkKGv1.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 268}, {"MovieId": "257344", "Title": "Pixels", "Casts": [], "PlotId": "257344", + "ThumbnailIds": ["/ktyVmIqfoaJ8w0gDSZyjhhOPpD6.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.6, "NumRating": 4370}, + {"MovieId": "16642", "Title": "Days of Heaven", "Casts": [], "PlotId": "16642", + "ThumbnailIds": ["/rjw7tQbiBnbIeufyEt02oVickDM.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.7, + "NumRating": 391}, {"MovieId": "175", "Title": "The Big Blue", "Casts": [], "PlotId": "175", + "ThumbnailIds": ["/RgvRBAD5LTGsePMzjaqaNPkSYf.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.6, "NumRating": 691}, + {"MovieId": "71668", "Title": "Piranha 3DD", "Casts": [], "PlotId": "71668", + "ThumbnailIds": ["/xua3x47piJTIRCVtVGjzXPDrbsN.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.2, + "NumRating": 498}, {"MovieId": "608", "Title": "Men in Black II", "Casts": [], "PlotId": "608", + "ThumbnailIds": ["/qWjRfBwr4VculczswwojXgoU0mq.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.3, "NumRating": 5582}, + {"MovieId": "10909", "Title": "Kalifornia", "Casts": [], "PlotId": "10909", + "ThumbnailIds": ["/5KGQYEsJvQdWZQH6o1zIyzkZZRC.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 354}, {"MovieId": "287", "Title": "Bull Durham", "Casts": [], "PlotId": "287", + "ThumbnailIds": ["/wZwXLiR1cisTXaJiuBLvgkS5HWw.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.8, "NumRating": 255}, + {"MovieId": "10758", "Title": "Waitress", "Casts": [], "PlotId": "10758", + "ThumbnailIds": ["/ux3SPTHHmJytSjWEaV7xlo9nbOZ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 228}, {"MovieId": "953", "Title": "Madagascar", "Casts": [], "PlotId": "953", + "ThumbnailIds": ["/2YiESGB68BGQSAFvfJxBi774sc4.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.8, "NumRating": 6092}, + {"MovieId": "621", "Title": "Grease", "Casts": [], "PlotId": "621", + "ThumbnailIds": ["/iMHdFTrCYhue74sBnXkdO39AJ3R.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 3712}, {"MovieId": "11824", "Title": "Teen Wolf", "Casts": [], "PlotId": "11824", + "ThumbnailIds": ["/3TKJbKNpHvRP8YVnwbgfok41AAC.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.0, "NumRating": 475}, + {"MovieId": "10414", "Title": "The Mighty Ducks", "Casts": [], "PlotId": "10414", + "ThumbnailIds": ["/4qXfjDlDEGuN3xRNawh4WZo5o96.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 335}, {"MovieId": "262500", "Title": "Insurgent", "Casts": [], "PlotId": "262500", + "ThumbnailIds": ["/6w1VjTPTjTaA5oNvsAg0y4H6bou.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.3, "NumRating": 6665}, + {"MovieId": "9766", "Title": "Gridiron Gang", "Casts": [], "PlotId": "9766", + "ThumbnailIds": ["/8UlDsCPjAfg15L5Sgkcmy8rv1rg.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 366}, {"MovieId": "14072", "Title": "Rab Ne Bana Di Jodi", "Casts": [], "PlotId": "14072", + "ThumbnailIds": ["/m8x6I2qf3R98HtF4DmJXcdxCU64.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 101}, + {"MovieId": "27585", "Title": "Rabbit Hole", "Casts": [], "PlotId": "27585", + "ThumbnailIds": ["/qBhDlqdLZzxqKIXO53vMjpqLgZu.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 287}, {"MovieId": "632", "Title": "Stalag 17", "Casts": [], "PlotId": "632", + "ThumbnailIds": ["/2PtDbmN7HAu4W5WqrUocBV8lgiZ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.8, "NumRating": 239}, + {"MovieId": "575766", "Title": "Hatsukoi: Otosan, Chibi ga Inaku Narimashita", "Casts": [], + "PlotId": "575766", "ThumbnailIds": ["/yMeXxSZ4IEitEWySboQaYAeEtXQ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 0.0, "NumRating": 0}, + {"MovieId": "372058", "Title": "Your Name.", "Casts": [], "PlotId": "372058", + "ThumbnailIds": ["/xq1Ugd62d23K2knRUx6xxuALTZB.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.6, + "NumRating": 4017}, {"MovieId": "225886", "Title": "Sex Tape", "Casts": [], "PlotId": "225886", + "ThumbnailIds": ["/2BMmIPGu5ZbTrwXwomdSUuTB2Ul.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.3, "NumRating": 2644}, + {"MovieId": "593325", "Title": "Sodemacom Killer", "Casts": [], "PlotId": "593325", + "ThumbnailIds": ["/loTuS8ryototYPvGJaNsmlHCvIG.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "293167", "Title": "Kong: Skull Island", "Casts": [], "PlotId": "293167", + "ThumbnailIds": ["/r2517Vz9EhDhj88qwbDVj8DCRZN.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.3, "NumRating": 5937}, + {"MovieId": "132316", "Title": "Jab Tak Hai Jaan", "Casts": [], "PlotId": "132316", + "ThumbnailIds": ["/oGoPWxz4v8HT8ydr2VdAa1OVohS.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 105}, {"MovieId": "11178", "Title": "My Sassy Girl", "Casts": [], "PlotId": "11178", + "ThumbnailIds": ["/jhlthtFzzEA8RYqRHy1y8dus4Q8.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.5, "NumRating": 266}, + {"MovieId": "493922", "Title": "Hereditary", "Casts": [], "PlotId": "493922", + "ThumbnailIds": ["/lHV8HHlhwNup2VbpiACtlKzaGIQ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 2370}, {"MovieId": "64586", "Title": "Sleeping Beauty", "Casts": [], "PlotId": "64586", + "ThumbnailIds": ["/9fc7K82At056IlrK9dOzPSQYhSY.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.3, "NumRating": 297}, + {"MovieId": "18570", "Title": "Food, Inc.", "Casts": [], "PlotId": "18570", + "ThumbnailIds": ["/gWYCM3YVYwZQcpfye6biAlPy9Xt.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 324}, {"MovieId": "553141", "Title": "The Head Hunter", "Casts": [], "PlotId": "553141", + "ThumbnailIds": ["/ol0DSLOIN8Rq1BcWDTsk6NNwas6.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.3, "NumRating": 35}, + {"MovieId": "37414", "Title": "The Killer Inside Me", "Casts": [], "PlotId": "37414", + "ThumbnailIds": ["/9iaV9bNNFBxEWLjnMGvSmnNY4Uj.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.9, + "NumRating": 299}, {"MovieId": "129", "Title": "Spirited Away", "Casts": [], "PlotId": "129", + "ThumbnailIds": ["/oRvMaJOmapypFUcQqpgHMZA6qL9.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.5, "NumRating": 7271}, + {"MovieId": "336843", "Title": "Maze Runner: The Death Cure", "Casts": [], "PlotId": "336843", + "ThumbnailIds": ["/2zYfzA3TBwrMC8tfFbpiTLODde0.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.1, + "NumRating": 3791}, {"MovieId": "273248", "Title": "The Hateful Eight", "Casts": [], "PlotId": "273248", + "ThumbnailIds": ["/fqe8JxDNO8B8QfOGTdjh6sPCdSC.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.7, "NumRating": 7887}, + {"MovieId": "10720", "Title": "Down with Love", "Casts": [], "PlotId": "10720", + "ThumbnailIds": ["/c5fcbQM7uPObficCZjyTvwOcU1l.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 367}, {"MovieId": "438", "Title": "Cube Zero", "Casts": [], "PlotId": "438", + "ThumbnailIds": ["/m6Vn1rysx6vmgJuy78Ih9QxFoOy.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.7, "NumRating": 482}, + {"MovieId": "14506", "Title": "The Adventures of Baron Munchausen", "Casts": [], "PlotId": "14506", + "ThumbnailIds": ["/75SlrEn0RCb2Ng18cCE82S00Hi5.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 356}, {"MovieId": "8398", "Title": "The Hitcher", "Casts": [], "PlotId": "8398", + "ThumbnailIds": ["/mIPCt79baN62Xv6TAEwLDxLUqBz.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.7, "NumRating": 422}, + {"MovieId": "348", "Title": "Alien", "Casts": [], "PlotId": "348", + "ThumbnailIds": ["/2h00HrZs89SL3tXB4nbkiM7BKHs.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.1, + "NumRating": 7439}, {"MovieId": "812", "Title": "Aladdin", "Casts": [], "PlotId": "812", + "ThumbnailIds": ["/7f53XAE4nPiGe9XprpGAeWHuKPw.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.6, "NumRating": 6232}, + {"MovieId": "401981", "Title": "Red Sparrow", "Casts": [], "PlotId": "401981", + "ThumbnailIds": ["/vLCogyfQGxVLDC1gqUdNAIkc29L.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 3321}, {"MovieId": "8491", "Title": "Weekend at Bernie's", "Casts": [], "PlotId": "8491", + "ThumbnailIds": ["/h3hjMREZEUPtDkZBiYDzLq0THk0.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.4, "NumRating": 414}, + {"MovieId": "590927", "Title": "Solo cose belle", "Casts": [], "PlotId": "590927", + "ThumbnailIds": ["/htp3wtXbB0b4qrBmGLCpjvXKvrS.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "8386", "Title": "How High", "Casts": [], "PlotId": "8386", + "ThumbnailIds": ["/AdQDz4LF6BzFfmyIJkMtrj707BZ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.2, "NumRating": 407}, + {"MovieId": "447200", "Title": "Skyscraper", "Casts": [], "PlotId": "447200", + "ThumbnailIds": ["/5LYSsOPzuP13201qSzMjNxi8FxN.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 2089}, {"MovieId": "449443", "Title": "Den of Thieves", "Casts": [], "PlotId": "449443", + "ThumbnailIds": ["/AfybH6GbGFw1F9bcETe2yu25mIE.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.5, "NumRating": 1208}, + {"MovieId": "2928", "Title": "Michael", "Casts": [], "PlotId": "2928", + "ThumbnailIds": ["/xof1HmoI3NrSrmzHByjK6W7dU8E.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.6, + "NumRating": 283}, {"MovieId": "9333", "Title": "Last Man Standing", "Casts": [], "PlotId": "9333", + "ThumbnailIds": ["/fkURS96D2ceuocZIBuyiIBGHilF.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.2, "NumRating": 399}, + {"MovieId": "11078", "Title": "National Security", "Casts": [], "PlotId": "11078", + "ThumbnailIds": ["/5YFPkMgy5NCzUSYl5Ep8p0vK9qB.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.5, + "NumRating": 407}, {"MovieId": "857", "Title": "Saving Private Ryan", "Casts": [], "PlotId": "857", + "ThumbnailIds": ["/miDoEMlYDJhOCvxlzI0wZqBs9Yt.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.1, "NumRating": 8292}, + {"MovieId": "376565", "Title": "The Duelist", "Casts": [], "PlotId": "376565", + "ThumbnailIds": ["/zlVkUY2bBJ5XYTDiry16u70oayQ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 40}, {"MovieId": "10326", "Title": "Forever Young", "Casts": [], "PlotId": "10326", + "ThumbnailIds": ["/AvkLZ5EqehwUlJD93ZeobesDmHB.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.2, "NumRating": 401}, + {"MovieId": "9777", "Title": "Proof", "Casts": [], "PlotId": "9777", + "ThumbnailIds": ["/eGVCVdbUR4gaAKPk1P4mar91qSX.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 335}, {"MovieId": "399360", "Title": "Alpha", "Casts": [], "PlotId": "399360", + "ThumbnailIds": ["/afdZAIcAQscziqVtsEoh2PwsYTW.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 1117}, + {"MovieId": "11797", "Title": "Fright Night", "Casts": [], "PlotId": "11797", + "ThumbnailIds": ["/jE0YbuFlmaZUWeVTyYNpzYXjIbn.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 515}, {"MovieId": "6691", "Title": "Priceless", "Casts": [], "PlotId": "6691", + "ThumbnailIds": ["/7GtzgK56NeIDzA6DksPihYVqTFm.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.4, "NumRating": 393}, + {"MovieId": "21334", "Title": "Children of Heaven", "Casts": [], "PlotId": "21334", + "ThumbnailIds": ["/ik83L4ap4gYzlzGMsm2UqlIlsNe.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.0, + "NumRating": 210}, {"MovieId": "9589", "Title": "Christiane F.", "Casts": [], "PlotId": "9589", + "ThumbnailIds": ["/sM989XlpNVK8ITU8sGa1I4Fw1in.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.3, "NumRating": 549}, + {"MovieId": "4515", "Title": "Lions for Lambs", "Casts": [], "PlotId": "4515", + "ThumbnailIds": ["/iLuo4swOCHF2a8BlaxXZGdqETUs.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 373}, {"MovieId": "8592", "Title": "Dick Tracy", "Casts": [], "PlotId": "8592", + "ThumbnailIds": ["/4UfKEGnj9jPWV11LNKBXTgge3We.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.1, "NumRating": 406}, + {"MovieId": "76487", "Title": "The Devil Inside", "Casts": [], "PlotId": "76487", + "ThumbnailIds": ["/xClxu7PoHWMpCHHaqd3ZRdwOCnr.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.7, + "NumRating": 431}, {"MovieId": "23706", "Title": "All About Steve", "Casts": [], "PlotId": "23706", + "ThumbnailIds": ["/Ap0Lx5mv2tvE3LL8U3VQfCNdziL.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 4.8, "NumRating": 525}, + {"MovieId": "626", "Title": "Un Chien Andalou", "Casts": [], "PlotId": "626", + "ThumbnailIds": ["/obvE7ElAvCUhKtWFwDSvNbPw9PV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 540}, {"MovieId": "8046", "Title": "Gigli", "Casts": [], "PlotId": "8046", + "ThumbnailIds": ["/hy4HOGwrWc8et7jUJFzE9getbsp.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 3.6, "NumRating": 187}, + {"MovieId": "19123", "Title": "Knock on Wood", "Casts": [], "PlotId": "19123", + "ThumbnailIds": ["/30dsG6akb4UYJiNDVdSTBSQsrTe.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.1, + "NumRating": 235}, {"MovieId": "19426", "Title": "Nights of Cabiria", "Casts": [], "PlotId": "19426", + "ThumbnailIds": ["/xF4oCG3PLNbcrtPZbqB3BtkIbKg.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.0, "NumRating": 235}, + {"MovieId": "12877", "Title": "Dead Man's Shoes", "Casts": [], "PlotId": "12877", + "ThumbnailIds": ["/jGjlrDooATSSdmkrjH7lqFy6sWy.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.1, + "NumRating": 261}, {"MovieId": "545919", "Title": "Singel 39", "Casts": [], "PlotId": "545919", + "ThumbnailIds": ["/3SV996rVNm7KYe6HKDB6mYbu0Mp.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 0.0, "NumRating": 0}, + {"MovieId": "350", "Title": "The Devil Wears Prada", "Casts": [], "PlotId": "350", + "ThumbnailIds": ["/8unCRm0LeiO0fM6skWAZy3ZfXR1.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 6394}, {"MovieId": "9516", "Title": "Menace II Society", "Casts": [], "PlotId": "9516", + "ThumbnailIds": ["/s1uQMgbK2tfaYltK6uDXU1xoYWA.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.4, "NumRating": 288}, + {"MovieId": "769", "Title": "GoodFellas", "Casts": [], "PlotId": "769", + "ThumbnailIds": ["/hAPeXBdGDGmXRPj4OZZ0poH65Iu.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.4, + "NumRating": 5504}, {"MovieId": "508763", "Title": "A Dog's Way Home", "Casts": [], "PlotId": "508763", + "ThumbnailIds": ["/pZn87R7gtmMCGGO8KeaAfZDhXLg.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.5, "NumRating": 211}, + {"MovieId": "9918", "Title": "Glory Road", "Casts": [], "PlotId": "9918", + "ThumbnailIds": ["/bGRSV5tStxDNPRLCewnOeeiZzrY.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 285}, {"MovieId": "51241", "Title": "Heartbeats", "Casts": [], "PlotId": "51241", + "ThumbnailIds": ["/ne84HBcScDVO9jBzgVv5qb7rZd0.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 476}, + {"MovieId": "302", "Title": "Swimming Pool", "Casts": [], "PlotId": "302", + "ThumbnailIds": ["/mHV60wGXvvkMR9ebVO5UWvxL716.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 272}, {"MovieId": "14048", "Title": "Man on Wire", "Casts": [], "PlotId": "14048", + "ThumbnailIds": ["/86kXY7u0HWSIK27RqRnu4r0cADz.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.5, "NumRating": 458}, + {"MovieId": "310", "Title": "Bruce Almighty", "Casts": [], "PlotId": "310", + "ThumbnailIds": ["/lgYKHifMMLT8OxYObMKa8b4STsr.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 6093}, {"MovieId": "194662", "Title": "Birdman", "Casts": [], "PlotId": "194662", + "ThumbnailIds": ["/rSZs93P0LLxqlVEbI001UKoeCQC.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.4, "NumRating": 7597}, + {"MovieId": "1487", "Title": "Hellboy", "Casts": [], "PlotId": "1487", + "ThumbnailIds": ["/3fAWzI9MUosggdGMu7EaDhn44m6.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 3930}, {"MovieId": "376540", "Title": "Mathilde", "Casts": [], "PlotId": "376540", + "ThumbnailIds": ["/kFXGRh6zIgX4e9x4ym4C4l5aVLo.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.4, "NumRating": 34}, + {"MovieId": "10538", "Title": "Passenger 57", "Casts": [], "PlotId": "10538", + "ThumbnailIds": ["/mOh1t38TkW6JVg6ylQusKBPxxqa.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.7, + "NumRating": 416}, {"MovieId": "598073", "Title": "Daymohk", "Casts": [], "PlotId": "598073", + "ThumbnailIds": ["/tXx3ihWKWbOfzXUXkqo613PyETj.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 0.0, "NumRating": 0}, + {"MovieId": "26280", "Title": "I Killed My Mother", "Casts": [], "PlotId": "26280", + "ThumbnailIds": ["/cxb02CLALt9He1Dt0BIDKj9fdvM.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.7, + "NumRating": 584}, + {"MovieId": "12107", "Title": "Nutty Professor II: The Klumps", "Casts": [], "PlotId": "12107", + "ThumbnailIds": ["/r1WXXXtpNBsgCnCTdjT6ERxOcEV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.8, + "NumRating": 661}, {"MovieId": "17927", "Title": "Fired Up!", "Casts": [], "PlotId": "17927", + "ThumbnailIds": ["/bHhLZIg9VKsconJdnwIoUTqakEG.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.1, "NumRating": 304}, + {"MovieId": "12508", "Title": "Rock Star", "Casts": [], "PlotId": "12508", + "ThumbnailIds": ["/eMbsUnRfRciBGLclsqmICPOXbir.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 295}, {"MovieId": "14410", "Title": "Notorious", "Casts": [], "PlotId": "14410", + "ThumbnailIds": ["/lfKALQovRf46vVC12Rnl0mSRPyu.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 285}, + {"MovieId": "9611", "Title": "Romy and Michele's High School Reunion", "Casts": [], "PlotId": "9611", + "ThumbnailIds": ["/fvgLTx3vM3dUQqytaWCRaTdAdrs.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 251}, {"MovieId": "10890", "Title": "Stripes", "Casts": [], "PlotId": "10890", + "ThumbnailIds": ["/nqfLX1bJLUlZnfflqpUIWrGQwSv.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 405}, + {"MovieId": "11983", "Title": "Proof of Life", "Casts": [], "PlotId": "11983", + "ThumbnailIds": ["/qyNoOGCajNWH6heJ0kAmFFJtRQS.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 347}, + {"MovieId": "14976", "Title": "Rachel Getting Married", "Casts": [], "PlotId": "14976", + "ThumbnailIds": ["/rq1z6yiYW1LTgXZhsyNgmtmOedK.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 291}, {"MovieId": "84306", "Title": "Liberal Arts", "Casts": [], "PlotId": "84306", + "ThumbnailIds": ["/c9Q2to1Jpq8a0Zqto6Mqc1ujLYn.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.3, "NumRating": 278}, + {"MovieId": "7862", "Title": "The Counterfeiters", "Casts": [], "PlotId": "7862", + "ThumbnailIds": ["/bRQddrgVemZtFdnrPy9AxTpkhpj.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 235}, {"MovieId": "50797", "Title": "Burnt by the Sun", "Casts": [], "PlotId": "50797", + "ThumbnailIds": ["/e7bE9BiLyKaZtNF3Q2ro8gVCEQA.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.2, "NumRating": 60}, + {"MovieId": "605", "Title": "The Matrix Revolutions", "Casts": [], "PlotId": "605", + "ThumbnailIds": ["/2aJvwc4zXqtVUDbEu62e14J0mhe.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 5033}, {"MovieId": "9957", "Title": "The Benchwarmers", "Casts": [], "PlotId": "9957", + "ThumbnailIds": ["/zQy6T5qV0lvLkkKi5S9VIYykSQD.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.4, "NumRating": 386}, + {"MovieId": "1422", "Title": "The Departed", "Casts": [], "PlotId": "1422", + "ThumbnailIds": ["/tGLO9zw5ZtCeyyEWgbYGgsFxC6i.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.1, + "NumRating": 7729}, {"MovieId": "13973", "Title": "Choke", "Casts": [], "PlotId": "13973", + "ThumbnailIds": ["/lltyV3NNtzm4jaI5SfOYYsrr2Cz.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.2, "NumRating": 197}, + {"MovieId": "359940", "Title": "Three Billboards Outside Ebbing, Missouri", "Casts": [], + "PlotId": "359940", "ThumbnailIds": ["/vgvw6w1CtcFkuXXn004S5wQsHRl.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.2, "NumRating": 5306}, + {"MovieId": "13920", "Title": "Radio", "Casts": [], "PlotId": "13920", + "ThumbnailIds": ["/t4b6Ff8N0cvMBeYFtDNDm6xvyC9.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 289}, {"MovieId": "500664", "Title": "Upgrade", "Casts": [], "PlotId": "500664", + "ThumbnailIds": ["/8fDtXi6gVw8WUMWGT9XFz7YwkuE.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 1254}, + {"MovieId": "9410", "Title": "Great Expectations", "Casts": [], "PlotId": "9410", + "ThumbnailIds": ["/djLsfl3lm7BZKWsKfVZ2wM1ZMrP.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 244}, + {"MovieId": "339964", "Title": "Valerian and the City of a Thousand Planets", "Casts": [], + "PlotId": "339964", "ThumbnailIds": ["/jfIpMh79fGRqYJ6PwZLCntzgxlF.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 4065}, + {"MovieId": "425591", "Title": "I Don't Feel at Home in This World Anymore", "Casts": [], + "PlotId": "425591", "ThumbnailIds": ["/1stdUlXBc3nxqhdWvZ6wWWEbCQW.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 582}, + {"MovieId": "598549", "Title": "Low Budget (B.O.)", "Casts": [], "PlotId": "598549", + "ThumbnailIds": ["/qAu9YRsNdLXLH2Mg8mkpr5RBZ8f.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "449563", "Title": "Isn't It Romantic", "Casts": [], "PlotId": "449563", + "ThumbnailIds": ["/5xNBYXuv8wqiLVDhsfqCOr75DL7.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.4, "NumRating": 1509}, + {"MovieId": "10069", "Title": "Stay Alive", "Casts": [], "PlotId": "10069", + "ThumbnailIds": ["/9iPpBKxpkgxHtsyGcnSlSN5KTu6.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.4, + "NumRating": 303}, {"MovieId": "9942", "Title": "Major League", "Casts": [], "PlotId": "9942", + "ThumbnailIds": ["/nD8hYPxh4Ph12dAsWiH3nQbqYDW.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 332}, + {"MovieId": "577109", "Title": "To Paris!", "Casts": [], "PlotId": "577109", + "ThumbnailIds": ["/m5mInKktHKCOob7mpKBN6zEZPAx.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "10823", "Title": "Children of the Corn", "Casts": [], "PlotId": "10823", + "ThumbnailIds": ["/votpiVmvic2ULYe74GES8CxYgp1.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.7, "NumRating": 401}, + {"MovieId": "31175", "Title": "Soul Kitchen", "Casts": [], "PlotId": "31175", + "ThumbnailIds": ["/vUxpCAViRHmW4iIuAiwUxisQ5Wz.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 215}, {"MovieId": "363", "Title": "Head-On", "Casts": [], "PlotId": "363", + "ThumbnailIds": ["/5cPrr36unu3TTbdLdrfV1o7t8Sp.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.5, "NumRating": 232}, + {"MovieId": "13250", "Title": "Butterfly on a Wheel", "Casts": [], "PlotId": "13250", + "ThumbnailIds": ["/wWe26HWNIogpYpGdvzizvBeH77p.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 238}, + {"MovieId": "12140", "Title": "Ghost in the Shell 2: Innocence", "Casts": [], "PlotId": "12140", + "ThumbnailIds": ["/1KSMfpMWMzlvR2DN2fzv31vZ66B.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 364}, {"MovieId": "297761", "Title": "Suicide Squad", "Casts": [], "PlotId": "297761", + "ThumbnailIds": ["/e1mjopzAS2KNsvpbpahQ1a6SkSn.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 13345}, + {"MovieId": "8270", "Title": "The Lookout", "Casts": [], "PlotId": "8270", + "ThumbnailIds": ["/jSJca1tqlj08fGeqngTmr4UJ9ON.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 325}, {"MovieId": "44129", "Title": "The Company Men", "Casts": [], "PlotId": "44129", + "ThumbnailIds": ["/xP1FWgiUT1v1GGCwd94hXLaHyg8.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.5, "NumRating": 348}, + {"MovieId": "55725", "Title": "Win Win", "Casts": [], "PlotId": "55725", + "ThumbnailIds": ["/s8Q2YqsJWXdZeT8Bpuzxic8UVd1.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 263}, {"MovieId": "10053", "Title": "When a Stranger Calls", "Casts": [], "PlotId": "10053", + "ThumbnailIds": ["/pYxd8DM4qBV24rJ9Jf84SYtIao1.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.5, "NumRating": 519}, + {"MovieId": "11252", "Title": "Psycho", "Casts": [], "PlotId": "11252", + "ThumbnailIds": ["/5emaTTGS8rnLgZrBS98zqv1XWqE.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.1, + "NumRating": 457}, {"MovieId": "11894", "Title": "Curly Sue", "Casts": [], "PlotId": "11894", + "ThumbnailIds": ["/4NE36Icn6jrt7YHHXbB7zvNu9gq.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 225}, + {"MovieId": "2639", "Title": "Deconstructing Harry", "Casts": [], "PlotId": "2639", + "ThumbnailIds": ["/krKRthefSZlUjnzDvN4isqty0R7.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 345}, {"MovieId": "13446", "Title": "Withnail & I", "Casts": [], "PlotId": "13446", + "ThumbnailIds": ["/lXD5UR2dvXJF54AIBt8G2kDvYGk.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 256}, + {"MovieId": "354912", "Title": "Coco", "Casts": [], "PlotId": "354912", + "ThumbnailIds": ["/eKi8dIrr8voobbaGzDpe8w0PVbC.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.2, + "NumRating": 8460}, {"MovieId": "808", "Title": "Shrek", "Casts": [], "PlotId": "808", + "ThumbnailIds": ["/140ewbWv8qHStD3mlBDvvGd0Zvu.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.6, "NumRating": 8427}, + {"MovieId": "315837", "Title": "Ghost in the Shell", "Casts": [], "PlotId": "315837", + "ThumbnailIds": ["/si1ZyELNHdPUZw4pXR5KjMIIsBF.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 5054}, {"MovieId": "570426", "Title": "Enterrados", "Casts": [], "PlotId": "570426", + "ThumbnailIds": ["/e8Hb4g3TEPFySwq5iao2IuUbEdb.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 0.0, "NumRating": 0}, + {"MovieId": "10564", "Title": "Where the Heart Is", "Casts": [], "PlotId": "10564", + "ThumbnailIds": ["/rNP5NSdXzl6y5CuQ6Sh2JmEnvt5.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 338}, {"MovieId": "475888", "Title": "Tell It to the Bees", "Casts": [], "PlotId": "475888", + "ThumbnailIds": ["/Rj6zpHhMU1zFW3v28U4PSRSRgP.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.4, "NumRating": 12}, + {"MovieId": "8088", "Title": "Broken Embraces", "Casts": [], "PlotId": "8088", + "ThumbnailIds": ["/9C6UiobINVB9AWKSazi0huHYyFY.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.1, + "NumRating": 283}, {"MovieId": "14052", "Title": "Revenge of the Nerds", "Casts": [], "PlotId": "14052", + "ThumbnailIds": ["/wCcMAx1J4JxBviEzy5Ay2FW79Nb.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.5, "NumRating": 324}, + {"MovieId": "68924", "Title": "The Ice Storm", "Casts": [], "PlotId": "68924", + "ThumbnailIds": ["/e3j1Pn67bWos1pnBDE9GJNeAtcT.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 300}, {"MovieId": "427641", "Title": "Rampage", "Casts": [], "PlotId": "427641", + "ThumbnailIds": ["/vvrXUbiwFl4H8pJE7a4DzOoqVNB.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.2, "NumRating": 3054}, + {"MovieId": "98", "Title": "Gladiator", "Casts": [], "PlotId": "98", + "ThumbnailIds": ["/6WBIzCgmDCYrqh64yDREGeDk9d3.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.1, + "NumRating": 9530}, {"MovieId": "43959", "Title": "Soul Surfer", "Casts": [], "PlotId": "43959", + "ThumbnailIds": ["/t7twsxCK3vIkn4z0w4WwHuVZPNy.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.0, "NumRating": 674}, + {"MovieId": "14197", "Title": "My Sassy Girl", "Casts": [], "PlotId": "14197", + "ThumbnailIds": ["/PuHS7IRd3tRnkscBDq9AQ8wDHr.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 162}, {"MovieId": "13252", "Title": "Cleaner", "Casts": [], "PlotId": "13252", + "ThumbnailIds": ["/bhZ8LZXNOAMTpziYiE4ebn7est4.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 342}, + {"MovieId": "18079", "Title": "Nine Queens", "Casts": [], "PlotId": "18079", + "ThumbnailIds": ["/3LS2qYsmdYoSwQWJulK73lDfz52.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.7, + "NumRating": 281}, {"MovieId": "1368", "Title": "First Blood", "Casts": [], "PlotId": "1368", + "ThumbnailIds": ["/bbYNNEGLXrV3lJpHDg7CKaPscCb.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.3, "NumRating": 2723}, + {"MovieId": "10843", "Title": "After Hours", "Casts": [], "PlotId": "10843", + "ThumbnailIds": ["/s5XkBqUMwE0wQv9NY0XERs64cgs.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 476}, {"MovieId": "45657", "Title": "The Ward", "Casts": [], "PlotId": "45657", + "ThumbnailIds": ["/cMApNxaSq4udLTYBAd755E8VJok.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.8, "NumRating": 601}, + {"MovieId": "4967", "Title": "Keeping the Faith", "Casts": [], "PlotId": "4967", + "ThumbnailIds": ["/wbW4OnSNcuywgmJFdlvL8SvHx4.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 286}, {"MovieId": "37724", "Title": "Skyfall", "Casts": [], "PlotId": "37724", + "ThumbnailIds": ["/lQCkPLDxFONmgzrWLvq085v1g2d.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 10328}, + {"MovieId": "954", "Title": "Mission: Impossible", "Casts": [], "PlotId": "954", + "ThumbnailIds": ["/1PVKS17pIBFsIhgFws2uagPDNLW.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 4636}, {"MovieId": "10741", "Title": "Bobby", "Casts": [], "PlotId": "10741", + "ThumbnailIds": ["/a38CTs2mJ2uDHHH6mUJmuGH8RVI.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.6, "NumRating": 194}, + {"MovieId": "598062", "Title": "Lumpkin, GA", "Casts": [], "PlotId": "598062", + "ThumbnailIds": ["/hLqsvhDrVK5my1fJVIZa0INrtB3.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, + {"MovieId": "595409", "Title": "The Caring City", "Casts": [], "PlotId": "595409", "ThumbnailIds": [None], + "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, "NumRating": 0}, + {"MovieId": "12177", "Title": "The Love Guru", "Casts": [], "PlotId": "12177", + "ThumbnailIds": ["/q3g4UFCMPqbKXfc3qMOsYQBOGbl.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.2, + "NumRating": 384}, + {"MovieId": "1979", "Title": "Fantastic Four: Rise of the Silver Surfer", "Casts": [], "PlotId": "1979", + "ThumbnailIds": ["/fXpziQgnBnB4bLgjKhjTbLQumE5.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.5, + "NumRating": 4668}, {"MovieId": "458344", "Title": "Juliet, Naked", "Casts": [], "PlotId": "458344", + "ThumbnailIds": ["/tj4lbeWQBvPwGjadEAAjJdQolko.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.5, "NumRating": 118}, + {"MovieId": "201088", "Title": "Blackhat", "Casts": [], "PlotId": "201088", + "ThumbnailIds": ["/sW3VEsulmxMlOmQwm0h7H7lZROi.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.3, + "NumRating": 1154}, {"MovieId": "10543", "Title": "Fear", "Casts": [], "PlotId": "10543", + "ThumbnailIds": ["/reA4wlTY9qGwryVftscFT2q0ZLK.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.2, "NumRating": 283}, + {"MovieId": "38031", "Title": "You Will Meet a Tall Dark Stranger", "Casts": [], "PlotId": "38031", + "ThumbnailIds": ["/gbwJPEQZTSjjBAlv8DofNZVWiO.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 429}, {"MovieId": "11973", "Title": "Thirteen Days", "Casts": [], "PlotId": "11973", + "ThumbnailIds": ["/37yRUulECxF96eIok4ZXB8nRJ11.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.0, "NumRating": 314}, + {"MovieId": "564", "Title": "The Mummy", "Casts": [], "PlotId": "564", + "ThumbnailIds": ["/yhIsVvcUm7QxzLfT6HW2wLf5ajY.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 4854}, {"MovieId": "13159", "Title": "Georgia Rule", "Casts": [], "PlotId": "13159", + "ThumbnailIds": ["/hEf4AlcN0wuVby5ysKf2RuckUgj.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.8, "NumRating": 229}, + {"MovieId": "10849", "Title": "The Purple Rose of Cairo", "Casts": [], "PlotId": "10849", + "ThumbnailIds": ["/nWMT2WBzPtrXdjPlb28PFSrBGd0.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 407}, {"MovieId": "7095", "Title": "Jack", "Casts": [], "PlotId": "7095", + "ThumbnailIds": ["/nOgHa5ggt6vbmIuoNW7exrJQJq9.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.1, "NumRating": 596}, + {"MovieId": "103", "Title": "Taxi Driver", "Casts": [], "PlotId": "103", + "ThumbnailIds": ["/ekstpH614fwDX8DUln1a2Opz0N8.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.1, + "NumRating": 5125}, {"MovieId": "26900", "Title": "The Bandit", "Casts": [], "PlotId": "26900", + "ThumbnailIds": ["/u1o4yJg0xkeVV8gfJL49CTZRBiu.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.5, "NumRating": 110}, + {"MovieId": "13279", "Title": "Lakeview Terrace", "Casts": [], "PlotId": "13279", + "ThumbnailIds": ["/iOnQK5te4gEhHpyZ5KpGCtbNn1B.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.9, + "NumRating": 478}, {"MovieId": "241251", "Title": "The Boy Next Door", "Casts": [], "PlotId": "241251", + "ThumbnailIds": ["/h28t2JNNGrZx0fIuAw8aHQFhIxR.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 4.4, "NumRating": 1371}, + {"MovieId": "8987", "Title": "The River Wild", "Casts": [], "PlotId": "8987", + "ThumbnailIds": ["/8NFAQxZud5Kisw1sd9WxfBG54.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 396}, {"MovieId": "6309", "Title": "Flood", "Casts": [], "PlotId": "6309", + "ThumbnailIds": ["/s0F2iDMtj6uYQPqbjwCPlwug2O4.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.5, "NumRating": 63}] diff --git a/deathstar_movie_review/test_movie_review_demo.py b/deathstar_movie_review/test_movie_review_demo.py new file mode 100644 index 0000000..78e3358 --- /dev/null +++ b/deathstar_movie_review/test_movie_review_demo.py @@ -0,0 +1,101 @@ +from cascade.dataflow.dataflow import Event, InitClass, InvokeMethod, OpNode +from cascade.dataflow.optimization.dead_node_elim import dead_node_elimination +from cascade.runtime.python_runtime import PythonClientSync, PythonRuntime +from deathstar_movie_review.entities.compose_review import ComposeReview, compose_review_op +from deathstar_movie_review.entities.user import User, user_op +from deathstar_movie_review.entities.movie import MovieId, movie_id_op, movie_info_op, plot_op +from deathstar_movie_review.entities.frontend import frontend_op, text_op, unique_id_op + + + +def test_deathstar_movie_demo_python(): + print("starting") + runtime = PythonRuntime() + + print(frontend_op.dataflow.to_dot()) + dead_node_elimination([], [frontend_op]) + print(frontend_op.dataflow.to_dot()) + + runtime.add_operator(compose_review_op) + runtime.add_operator(user_op) + runtime.add_operator(movie_info_op) + runtime.add_operator(movie_id_op) + runtime.add_operator(plot_op) + runtime.add_stateless_operator(frontend_op) + runtime.add_stateless_operator(unique_id_op) + runtime.add_stateless_operator(text_op) + + runtime.run() + client = PythonClientSync(runtime) + + init_user = OpNode(User, InitClass(), read_key_from="username") + username = "username_1" + user_data = { + "userId": "user1", + "FirstName": "firstname", + "LastName": "lastname", + "Username": username, + "Password": "****", + "Salt": "salt" + } + print("testing user create") + event = Event(init_user, {"username": username, "user_data": user_data}, None) + result = client.send(event) + assert isinstance(result, User) and result.username == username + + print("testing compose review") + req_id = 1 + movie_title = "Cars 2" + movie_id = 1 + + # make the review + init_compose_review = OpNode(ComposeReview, InitClass(), read_key_from="req_id") + event = Event(init_compose_review, {"req_id": req_id}, None) + result = client.send(event) + print("review made") + + + # make the movie + init_movie = OpNode(MovieId, InitClass(), read_key_from="title") + event = Event(init_movie, {"title": movie_title, "movie_id": movie_id}, None) + result = client.send(event) + print("movie made") + + # compose the review + review_data = { + "review": req_id, + "user": username, + "title": movie_title, + "rating": 5, + "text": "good movie!" + } + + event = Event( + frontend_op.dataflow.entry, + review_data, + frontend_op.dataflow) + result = client.send(event) + print(result) + print("review composed") + + + # read the review + get_review = OpNode(ComposeReview, InvokeMethod("get_data"), read_key_from="req_id") + event = Event( + get_review, + {"req_id": req_id}, + None + ) + result = client.send(event) + expected = { + "userId": user_data["userId"], + "movieId": movie_id, + "text": review_data["text"] + } + print(result, expected) + assert "review_id" in result + del result["review_id"] # randomly generated + assert result == expected + + print("Success!") + \ No newline at end of file diff --git a/deathstar_movie_review/workload_data.py b/deathstar_movie_review/workload_data.py new file mode 100644 index 0000000..b533e36 --- /dev/null +++ b/deathstar_movie_review/workload_data.py @@ -0,0 +1,1008 @@ +charset = ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', 'a', 's', + 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'z', 'x', 'c', 'v', 'b', 'n', 'm', 'Q', + 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', 'A', 'S', 'D', 'F', 'G', 'H', + 'J', 'K', 'L', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '1', '2', '3', '4', '5', + '6', '7', '8', '9', '0'] + +movie_titles = [ + "Avengers: Endgame", + "Kamen Rider Heisei Generations FOREVER", + "Captain Marvel", + "Pokémon Detective Pikachu", + "Hellboy", + "After", + "Avengers: Infinity War", + "Doraemon the Movie: Nobita's Treasure Island", + "Cold Pursuit", + "Shazam!", + "What Men Want", + "Glass", + "The Avengers", + "Black Panther", + "How to Train Your Dragon: The Hidden World", + "Tolkien", + "Fighting with My Family", + "Guardians of the Galaxy Vol. 2", + "Avengers: Age of Ultron", + "The Prodigy", + "Cars", + "Thor: Ragnarok", + "The Wandering Earth", + "Extremely Wicked, Shockingly Evil and Vile", + "Pet Sematary", + "Logan", + "Guardians of the Galaxy", + "Fall in Love at First Kiss", + "Spider-Man: Into the Spider-Verse", + "Bumblebee", + "Thor", + "Aquaman", + "Ant-Man and the Wasp", + "The Lego Movie 2: The Second Part", + "Edge of Tomorrow", + "The Hustle", + "Dumbo", + "Redcon-1", + "Instant Family", + "Spider-Man: Homecoming", + "The Hobbit: The Battle of the Five Armies", + "Little", + "The Curse of La Llorona", + "Escape Room", + "Miss Bala", + "High Life", + "The Highwaymen", + "The Fate of the Furious", + "Iron Man", + "Captain America: The First Avenger", + "Captain America: Civil War", + "Us", + "Detective Conan: The Fist of Blue Sapphire", + "Fate/stay night: Heaven’s Feel II. lost butterfly", + "Robin Hood", + "Fantastic Beasts: The Crimes of Grindelwald", + "Thor: The Dark World", + "Alita: Battle Angel", + "Happy Death Day 2U", + "John Wick", + "Mile 22", + "Iron Man 2", + "Maggie", + "Venom", + "Harry Potter and the Philosopher's Stone", + "The Kid Who Would Be King", + "Pirates of the Caribbean: The Curse of the Black Pearl", + "Bohemian Rhapsody", + "The Mule", + "Ralph Breaks the Internet", + "The Lord of the Rings: The Fellowship of the Ring", + "Death Wish", + "John Wick: Chapter 3 – Parabellum", + "The Sisters Brothers", + "We Are Your Friends", + "Vaarikkuzhiyile Kolapathakam", + "The First Purge", + "Left Behind", + "Deadpool 2", + "BlacKkKlansman", + "Jigsaw", + "The Kissing Booth", + "Shaun the Sheep Movie", + "Under Siege 2: Dark Territory", + "Truth or Dare", + "Doctor Strange", + "The Lord of the Rings: The Return of the King", + "It's a Wonderful Life", + "Interstellar", + "Bad Times at the El Royale", + "A Madea Family Funeral", + "Long Shot", + "Suspiria", + "The Dark Knight", + "Trainspotting", + "Star Wars", + "Mortal Engines", + "Loving Vincent", + "Jurassic World: Fallen Kingdom", + "Velvet Buzzsaw", + "A Wrinkle in Time", + "The Lion King 1½", + "Spider-Man: Far from Home", + "Harry Potter and the Chamber of Secrets", + "Solo: A Star Wars Story", + "The Last Summer", + "Iron Man 3", + "Eighth Grade", + "Mandy", + "Die Hard", + "Home Alone 4", + "Adrift", + "The Predator", + "The Favourite", + "Overlord", + "Camp X-Ray", + "Mary Poppins Returns", + "Master Z: Ip Man Legacy", + "Re: Zero kara Hajimeru Isekai Seikatsu - Memory Snow", + "Scooby-Doo 2: Monsters Unleashed", + "The Transporter Refueled", + "Personal Shopper", + "Endless Love", + "Ant-Man", + "Polar", + "The Silence", + "The Professor and the Madman", + "12 Strong", + "Anon", + "Brightburn", + "Outlaw King", + "Texas Chainsaw 3D", + "The Upside", + "Breakthrough", + "The Emoji Movie", + "Green Book", + "A Bad Moms Christmas", + "Deadpool", + "Herbie Fully Loaded", + "Batman v Superman: Dawn of Justice", + "Bel Ami", + "A Hologram for the King", + "The Hunger Games: Mockingjay - Part 1", + "Planes", + "Wrong Turn 2: Dead End", + "Zodiac", + "The Curious Case of Benjamin Button", + "Wonder Woman", + "A Goofy Movie", + "The Incredible Hulk", + "San Andreas", + "Creed II", + "X-Men: Days of Future Past", + "Executive Decision", + "The Man Who Knew Too Much", + "Footloose", + "Justice League", + "47 Meters Down", + "A Star Is Born", + "Incredibles 2", + "The Square", + "Raw", + "Regression", + "Red Cliff", + "Star Wars: The Last Jedi", + "Hunter Killer", + "Chain Reaction", + "The Wind That Shakes the Barley", + "Last Knights", + "Avatar", + "The Matrix", + "I Spit on Your Grave 2", + "The 100 Year-Old Man Who Climbed Out the Window and Disappeared", + "2046", + "The Smurfs 2", + "Victoria", + "The Gunman", + "Batman Begins", + "I, Daniel Blake", + "Secret in Their Eyes", + "The Man Who Knew Infinity", + "The Past", + "The Belko Experiment", + "The Last Legion", + "Free State of Jones", + "Deadfall", + "The Shawshank Redemption", + "Anthropoid", + "Pitch Perfect 3", + "The Cold Light of Day", + "March of the Penguins", + "Popstar: Never Stop Never Stopping", + "The Cat Returns", + "The Zookeeper's Wife", + "Aladdin", + "Suburbicon", + "The Water Horse", + "Inception", + "The Return of the Living Dead", + "Odd Thomas", + "Jungle", + "Pulp Fiction", + "Hard Target", + "Iron Sky: The Coming Race", + "Terminator 2: Judgment Day", + "Legend", + "Twilight", + "Stealth", + "Battle of the Sexes", + "Split", + "Adore", + "Don't Say a Word", + "A Royal Affair", + "Salò, or the 120 Days of Sodom", + "Triple Frontier", + "The Rescuers", + "Blade Runner 2049", + "Tale of Tales", + "Taken 3", + "The Tale of Despereaux", + "It's Only the End of the World", + "Dark Places", + "Poms", + "Dragged Across Concrete", + "Unsane", + "15 Minutes", + "Batman: The Dark Knight Returns, Part 1", + "Ironclad", + "Taxi", + "Basic Instinct 2", + "Florence Foster Jenkins", + "Gone Girl", + "Rough Night", + "Schindler's List", + "Alexander and the Terrible, Horrible, No Good, Very Bad Day", + "The Great Mouse Detective", + "The Ring Two", + "Toni Erdmann", + "Forbidden Planet", + "The Lazarus Effect", + "The Triplets of Belleville", + "Heartbreaker", + "Someone Great", + "Veronica Mars", + "Byzantium", + "13 Sins", + "Mission: Impossible - Fallout", + "Far from the Madding Crowd", + "Charlie Says", + "'71", + "The Proposition", + "The Railway Man", + "A Ghost Story", + "The Choice", + "A Nightmare on Elm Street 4: The Dream Master", + "Bitter Moon", + "Arrival", + "Alvin and the Chipmunks: Chipwrecked", + "The Salesman", + "Soldier", + "The Lord of the Rings: The Two Towers", + "Sicario: Day of the Soldado", + "Mia and the White Lion", + "Vice", + "Harry Potter and the Deathly Hallows: Part 1", + "Zootopia", + "Whiteout", + "The Amazing Spider-Man", + "The Santa Clause 2", + "Five Feet Apart", + "Star Wars: The Force Awakens", + "No Man's Land", + "Whiplash", + "Hot Tub Time Machine 2", + "The Babysitter", + "The Paperboy", + "The Maze Runner", + "Jason X", + "Harry Potter and the Order of the Phoenix", + "Terms of Endearment", + "Frozen", + "Invasion of the Body Snatchers", + "The Divide", + "Brawl in Cell Block 99", + "Killing Season", + "Harry Potter and the Prisoner of Azkaban", + "Man Up", + "Missing Link", + "Friday the 13th Part 2", + "Force Majeure", + "Léon: The Professional", + "Suck Me Shakespeer", + "Teenage Mutant Ninja Turtles II: The Secret of the Ooze", + "Blood Father", + "Ivan's Childhood", + "The Garden of Words", + "All Dogs Go to Heaven", + "The Hobbit: An Unexpected Journey", + "Step Up All In", + "New Nightmare", + "Slow West", + "The Intruder", + "Arctic", + "Transsiberian", + "W.", + "Mongol: The Rise of Genghis Khan", + "Man of Tai Chi", + "Welcome to the Punch", + "Paranormal Activity: The Marked Ones", + "The Imitation Game", + "Police Academy 4: Citizens on Patrol", + "Fury", + "A Bridge Too Far", + "Citizenfour", + "Wolf Children", + "The Good Girl", + "To All the Boys I've Loved Before", + "The Nun", + "Black Sea", + "Flashdance", + "Where Eagles Dare", + "Rules of Engagement", + "Ladyhawke", + "The Meg", + "How to Train Your Dragon 2", + "Harry Potter and the Half-Blood Prince", + "Che: Part One", + "The Rover", + "Student of the Year 2", + "Destroyer", + "I Give It a Year", + "Two Days, One Night", + "The Ritual", + "Underdogs", + "Ida", + "Pirates of the Caribbean: Dead Man's Chest", + "Session 9", + "BloodRayne", + "Batman: The Killing Joke", + "Frankenstein", + "Kill the Messenger", + "The Imposter", + "Replicas", + "Maps to the Stars", + "Innerspace", + "Oliver & Company", + "Ladder 49", + "Snow White and the Seven Dwarfs", + "In the Name of the King: A Dungeon Siege Tale", + "War of the Worlds", + "Life", + "Child's Play 2", + "Sneakers", + "Fight Club", + "How to Train Your Dragon", + "Pathfinder", + "Colette", + "Joe", + "Batman: Gotham Knight", + "RoboCop 3", + "Drunken Master", + "Garfield: A Tail of Two Kitties", + "Miss & Mrs. Cops", + "The Fog", + "Pawn Sacrifice", + "The Equalizer 2", + "The Legend of Drunken Master", + "American Pie Presents: The Book of Love", + "Star Trek V: The Final Frontier", + "Blade", + "Tammy", + "Young Guns", + "Labor Day", + "Police Academy 3: Back in Training", + "Hellbound: Hellraiser II", + "The Reaping", + "The Axiom", + "Sexy Beast", + "The Big Short", + "The Human Centipede 2 (Full Sequence)", + "On the Basis of Sex", + "The Haunted Mansion", + "The Rebound", + "Ghosts of Mars", + "Nightcrawler", + "Absolute Power", + "Woman in Gold", + "Ready Player One", + "Disclosure", + "The Money Pit", + "Le Samouraï", + "Murder on the Orient Express", + "The Martian", + "Exposed", + "Flight of the Phoenix", + "Out of Africa", + "The Lunchbox", + "Terminator Genisys", + "The Purge: Anarchy", + "In the House", + "The General's Daughter", + "Under the Silver Lake", + "National Lampoon's European Vacation", + "The Muppet Christmas Carol", + "Hysteria", + "X-Men: Apocalypse", + "Operation Condor", + "Tomb Raider", + "Titanic", + "The Swan Princess", + "Very Bad Things", + "Suffragette", + "The Grudge 2", + "Scouts Guide to the Zombie Apocalypse", + "My Neighbor Totoro", + "The Tall Man", + "Magnum Force", + "Spider-Man 3", + "Asterix: The Secret of the Magic Potion", + "Dragonfly", + "French Kiss", + "The Devil's Own", + "Cinderella", + "3some", + "An American Tail", + "Dog Soldiers", + "Firewall", + "Dark Phoenix", + "Scanners", + "RV", + "2 Days in New York", + "Friends with Kids", + "An Officer and a Gentleman", + "102 Dalmatians", + "Freaks", + "Jurassic World", + "Under the Tuscan Sun", + "The Colony", + "Million Dollar Arm", + "Hard Boiled", + "Flash Gordon", + "Bound", + "All Quiet on the Western Front", + "Heist", + "The Three Musketeers", + "2010", + "Duck Soup", + "Mary Queen of Scots", + "Charlie Countryman", + "Mr. Right", + "Arthur and the Invisibles", + "Curse of the Golden Flower", + "Drinking Buddies", + "Harry Potter and the Goblet of Fire", + "Pathology", + "Over the Top", + "The Lion King", + "Driven", + "The Hunger Games: Mockingjay - Part 2", + "John Wick: Chapter 2", + "Sister Act 2: Back in the Habit", + "Halloween II", + "Crypto", + "The 12th Man", + "The East", + "Love of My Life", + "The Godfather", + "Eight Legged Freaks", + "Knocked Up", + "Dracula Untold", + "Excalibur", + "Somewhere", + "Pirates of the Caribbean: Dead Men Tell No Tales", + "Harry Potter and the Deathly Hallows: Part 2", + "Cannibal Holocaust", + "Old Dogs", + "The Equalizer", + "Redirected", + "Tokyo Story", + "James and the Giant Peach", + "Bride of Chucky", + "Maniac", + "The Lawnmower Man", + "Goal! II: Living the Dream", + "Everybody Wants Some!!", + "The Longest Day", + "Big Hero 6", + "Love Happens", + "Baby's Day Out", + "Happy Feet Two", + "Undisputed III: Redemption", + "Fred Claus", + "A Man Apart", + "Untraceable", + "Anatomy of a Murder", + "Wild and Free", + "Draft Day", + "The Medallion", + "Astro Boy", + "Song to Song", + "Neon Heart", + "Triple Threat", + "The Client", + "Starred Up", + "Cradle 2 the Grave", + "Unbreakable", + "Toy Story", + "I Am Mother", + "Salyut-7", + "First Man", + "New in Town", + "Marathon Man", + "DOA: Dead or Alive", + "Holmes & Watson", + "Lock Up", + "Mimic", + "The War of the Roses", + "Crash", + "Look Who's Talking Too", + "Spectral", + "3-Iron", + "Pale Rider", + "Wyatt Earp", + "The Dirt", + "Cocoon", + "Ichi the Killer", + "Big Momma's House 2", + "Beauty and the Beast", + "Breakdown", + "Lovelace", + "Take Me Home Tonight", + "Battleship Potemkin", + "Bullitt", + "Son of Saul", + "Transformers: The Last Knight", + "Duplicity", + "Double Impact", + "Far from Heaven", + "Striking Distance", + "The Shape of Water", + "Friday Night Lights", + "Annihilation", + "Dawn of the Planet of the Apes", + "The Crying Game", + "After the Sunset", + "The Dark Knight Rises", + "Parenthood", + "Passengers", + "The Gift", + "The Twelve Tasks of Asterix", + "After.Life", + "A Trip to the Moon", + "Don't Look Now", + "Playing for Keeps", + "Quills", + "Frantic", + "Guava Island", + "Turistas", + "Bulletproof Monk", + "High Plains Drifter", + "Man of Steel", + "McFarland, USA", + "Cat on a Hot Tin Roof", + "Goosebumps", + "Inland Empire", + "The Hunted", + "Enemy Mine", + "Marnie", + "National Lampoon's Loaded Weapon 1", + "The Four Feathers", + "Get Out", + "Trust", + "The Jewel of the Nile", + "My Own Private Idaho", + "Departures", + "Mirror", + "My Boss's Daughter", + "Goosebumps 2: Haunted Halloween", + "Fahrenheit 451", + "A Nightmare on Elm Street: The Dream Child", + "Crossroads", + "The Amazing Spider-Man 2", + "Rio Bravo", + "His Girl Friday", + "Kinsey", + "Howard the Duck", + "Red Sonja", + "Paperman", + "Sharknado", + "Vampires", + "Alone in the Dark", + "The Cranes Are Flying", + "The Prince & Me", + "Agent Cody Banks", + "Friday the 13th Part III", + "Dracula 2000", + "Bill & Ted's Bogus Journey", + "Lake Placid", + "War for the Planet of the Apes", + "The Corrupted", + "Nine", + "Did You Hear About the Morgans?", + "I Spy", + "The Ridiculous 6", + "Belle de Jour", + "The Libertine", + "The Extraordinary Adventures of Adèle Blanc-Sec", + "Working Girl", + "Forrest Gump", + "Captain America: The Winter Soldier", + "The Pink Panther", + "Spy Kids: All the Time in the World", + "The Right Stuff", + "See No Evil, Hear No Evil", + "U Turn", + "sex, lies, and videotape", + "Tideland", + "Down by Law", + "The Mission", + "Hellboy II: The Golden Army", + "3 Men and a Baby", + "Bad Words", + "Chariots of Fire", + "Spectre", + "People Like Us", + "Animal Kingdom", + "The Ledge", + "Copycat", + "Double Jeopardy", + "Super Mario Bros.", + "Bad Company", + "Striptease", + "Dolores Claiborne", + "Flyboys", + "Time Bandits", + "The Fan", + "Little Women", + "Away We Go", + "The Condemned", + "BASEketball", + "The Hitcher", + "A Long Way Down", + "Cabaret", + "The Normal Heart", + "Goya's Ghosts", + "The Odd Life of Timothy Green", + "Chalet Girl", + "The Cat in the Hat", + "Alice in Wonderland", + "Raising Helen", + "Problem Child 2", + "Westworld", + "Spider-Man", + "Widows", + "Show Me Love", + "The Player", + "Alien: Covenant", + "Shadow of a Doubt", + "Young & Beautiful", + "Vidocq", + "28 Days", + "Mad Max: Fury Road", + "The Perfect Date", + "The Barber of Siberia", + "Dr. Dolittle 2", + "Flypaper", + "Predator", + "The Wages of Fear", + "The Wolf of Wall Street", + "In the Land of Women", + "Enough Said", + "A Simple Favor", + "Once Upon a Deadpool", + "This Is It", + "The Descent: Part 2", + "Wild Target", + "Adam's Apples", + "Inside Out", + "Pride", + "The Grinch", + "Airplane II: The Sequel", + "The Fundamentals of Caring", + "The Tournament", + "Crimes and Misdemeanors", + "Stan & Ollie", + "The Big Year", + "Message in a Bottle", + "Kabhi Khushi Kabhie Gham", + "Rent", + "Naked Lunch", + "Without a Paddle", + "American Heist", + "True Story", + "The Jerk", + "Ratatouille", + "Zelig", + "The 51st State", + "The Karate Kid, Part III", + "Martha Marcy May Marlene", + "Moonstruck", + "Deuce Bigalow: European Gigolo", + "Raiders of the Lost Ark", + "Inspector Gadget", + "Blow", + "The Roommate", + "Smokey and the Bandit", + "Get Rich or Die Tryin'", + "Shutter Island", + "Unplanned", + "Tae Guk Gi: The Brotherhood of War", + "Invasion of the Body Snatchers", + "Teenage Mutant Ninja Turtles", + "Nim's Island", + "Aguirre: The Wrath of God", + "Dave", + "The Adventures of Robin Hood", + "Romper Stomper", + "The Producers", + "Ocean's Eight", + "Miracle", + "The Sentinel", + "Easy Virtue", + "Homeward Bound: The Incredible Journey", + "Boy Erased", + "Top Secret!", + "Airheads", + "Open Water", + "Prime", + "Harvey", + "THX 1138", + "Saved!", + "Lilya 4-ever", + "The Matrix Reloaded", + "In a Better World", + "Despite Everything", + "The Hobbit: The Desolation of Smaug", + "A Vigilante", + "Il grande spirito", + "The 39 Steps", + "The Battle of Algiers", + "The Cabinet of Dr. Caligari", + "Appaloosa", + "Re-Animator", + "The Passion of Joan of Arc", + "In the Loop", + "Bird Box", + "Serenity", + "The Golden Child", + "The Wrong Trousers", + "A Regular Woman", + "Men in Black 3", + "Mission: Impossible III", + "Fanboys", + "Welcome to Marwen", + "Stop! Or My Mom Will Shoot", + "Pariban : Idola Dari Tanah Jawa", + "The New Guy", + "Black Knight", + "House on Haunted Hill", + "Traitor", + "What Ever Happened to Baby Jane?", + "The Best Years of Our Lives", + "The Brothers Bloom", + "Black Rain", + "Fanny & Alexander", + "Perfect Blue", + "The Con Is On", + "Night at the Museum: Secret of the Tomb", + "The Mask", + "Ip Man 4", + "Men in Black", + "Float Like A Butterfly", + "The Peacemaker", + "WALL·E", + "Inglourious Basterds", + "Arthur", + "Coneheads", + "The Terminator", + "The Kid", + "Dumb and Dumberer: When Harry Met Lloyd", + "The First Wives Club", + "Holy Motors", + "Highlander 2: The Quickening", + "Hacksaw Ridge", + "Halloween", + "Pirates of the Caribbean: At World's End", + "Kleine Germanen", + "Morning Has Broken", + "High Crimes", + "Joe Dirt", + "Monsters, Inc.", + "Pirates of the Caribbean: On Stranger Tides", + "This Boy’s Life", + "Just Say Goodbye", + "Coriolanus", + "The American President", + "Buffalo '66", + "Meet Dave", + "Gomorrah", + "The Matador", + "Defendor", + "Evil", + "Would You Rather", + "The Man Who Would Be King", + "The Darkest Minds", + "A Quiet Place", + "Love and Death", + "The Nutcracker and the Four Realms", + "Dr. No", + "A River Runs Through It", + "One Fine Day", + "Edtv", + "Something Something... Unakkum Enakkum", + "Aliens", + "The Last Temptation of Christ", + "Tangerines", + "Pacific Rim: Uprising", + "Sunrise: A Song of Two Humans", + "Fair Game", + "Red Dawn", + "Laws of Attraction", + "Honey I Blew Up the Kid", + "The Rocker", + "Sleuth", + "Stuck on You", + "Flight of the Navigator", + "Dangerous Minds", + "Behind the Candelabra", + "A Clockwork Orange", + "The Messengers", + "Straw Dogs", + "City Slickers", + "Fantastic Beasts and Where to Find Them", + "Leaving Afghanistan", + "The Lady Vanishes", + "Kuch Kuch Hota Hai", + "American Splendor", + "A Hard Day's Night", + "Pixels", + "Days of Heaven", + "The Big Blue", + "Piranha 3DD", + "Men in Black II", + "Kalifornia", + "Bull Durham", + "Waitress", + "Madagascar", + "Grease", + "Teen Wolf", + "The Mighty Ducks", + "Insurgent", + "Gridiron Gang", + "Rab Ne Bana Di Jodi", + "Rabbit Hole", + "Stalag 17", + "Hatsukoi: Otosan, Chibi ga Inaku Narimashita", + "Your Name.", + "Sex Tape", + "Sodemacom Killer", + "Kong: Skull Island", + "Jab Tak Hai Jaan", + "My Sassy Girl", + "Hereditary", + "Sleeping Beauty", + "Food, Inc.", + "The Head Hunter", + "The Killer Inside Me", + "Spirited Away", + "Maze Runner: The Death Cure", + "The Hateful Eight", + "Down with Love", + "Cube Zero", + "The Adventures of Baron Munchausen", + "The Hitcher", + "Alien", + "Aladdin", + "Red Sparrow", + "Weekend at Bernie's", + "Solo cose belle", + "How High", + "Skyscraper", + "Den of Thieves", + "Michael", + "Last Man Standing", + "National Security", + "Saving Private Ryan", + "The Duelist", + "Forever Young", + "Proof", + "Alpha", + "Fright Night", + "Priceless", + "Children of Heaven", + "Christiane F.", + "Lions for Lambs", + "Dick Tracy", + "The Devil Inside", + "All About Steve", + "Un Chien Andalou", + "Gigli", + "Knock on Wood", + "Nights of Cabiria", + "Dead Man's Shoes", + "Singel 39", + "The Devil Wears Prada", + "Menace II Society", + "GoodFellas", + "A Dog's Way Home", + "Glory Road", + "Heartbeats", + "Swimming Pool", + "Man on Wire", + "Bruce Almighty", + "Birdman", + "Hellboy", + "Mathilde", + "Passenger 57", + "Daymohk", + "I Killed My Mother", + "Nutty Professor II: The Klumps", + "Fired Up!", + "Rock Star", + "Notorious", + "Romy and Michele's High School Reunion", + "Stripes", + "Proof of Life", + "Rachel Getting Married", + "Liberal Arts", + "The Counterfeiters", + "Burnt by the Sun", + "The Matrix Revolutions", + "The Benchwarmers", + "The Departed", + "Choke", + "Three Billboards Outside Ebbing, Missouri", + "Radio", + "Upgrade", + "Great Expectations", + "Valerian and the City of a Thousand Planets", + "I Don't Feel at Home in This World Anymore", + "Low Budget (B.O.)", + "Isn't It Romantic", + "Stay Alive", + "Major League", + "To Paris!", + "Children of the Corn", + "Soul Kitchen", + "Head-On", + "Butterfly on a Wheel", + "Ghost in the Shell 2: Innocence", + "Suicide Squad", + "The Lookout", + "The Company Men", + "Win Win", + "When a Stranger Calls", + "Psycho", + "Curly Sue", + "Deconstructing Harry", + "Withnail & I", + "Coco", + "Shrek", + "Ghost in the Shell", + "Enterrados", + "Where the Heart Is", + "Tell It to the Bees", + "Broken Embraces", + "Revenge of the Nerds", + "The Ice Storm", + "Rampage", + "Gladiator", + "Soul Surfer", + "My Sassy Girl", + "Cleaner", + "Nine Queens", + "First Blood", + "After Hours", + "The Ward", + "Keeping the Faith", + "Skyfall", + "Mission: Impossible", + "Bobby", + "Lumpkin, GA", + "The Caring City", + "The Love Guru", + "Fantastic Four: Rise of the Silver Surfer", + "Juliet, Naked", + "Blackhat", + "Fear", + "You Will Meet a Tall Dark Stranger", + "Thirteen Days", + "The Mummy", + "Georgia Rule", + "The Purple Rose of Cairo", + "Jack", + "Taxi Driver", + "The Bandit", + "Lakeview Terrace", + "The Boy Next Door", + "The River Wild", + "Flood" +] diff --git a/display_results.ipynb b/display_results.ipynb new file mode 100644 index 0000000..f47e1d5 --- /dev/null +++ b/display_results.ipynb @@ -0,0 +1,3003 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " event_id sent \\\n", + "0 701 Event(target=OpNode(User, InvokeMethod('login'... \n", + "1 702 Event(target=OpNode(User, InvokeMethod('login'... \n", + "2 703 Event(target=OpNode(User, InvokeMethod('login'... \n", + "3 704 Event(target=OpNode(User, InvokeMethod('login'... \n", + "4 705 Event(target=OpNode(User, InvokeMethod('login'... \n", + "\n", + " sent_t ret \\\n", + "0 (2, 1739358574443) EventResult(event_id=701, result=True, metadat... \n", + "1 (2, 1739358574443) EventResult(event_id=702, result=True, metadat... \n", + "2 (2, 1739358574443) EventResult(event_id=703, result=True, metadat... \n", + "3 (2, 1739358574443) EventResult(event_id=704, result=True, metadat... \n", + "4 (2, 1739358574443) EventResult(event_id=705, result=True, metadat... \n", + "\n", + " ret_t roundtrip flink_time deser_times loops \\\n", + "0 (2, 1739358574525) 0.075989 0.075989 [5.435943603515625e-05] 1 \n", + "1 (2, 1739358574503) 0.053916 0.053916 [3.266334533691406e-05] 1 \n", + "2 (2, 1739358574563) 0.113311 0.113311 [2.6702880859375e-05] 1 \n", + "3 (2, 1739358574563) 0.113266 0.113266 [2.0503997802734375e-05] 1 \n", + "4 (2, 1739358574563) 0.113229 0.113229 [1.5020370483398438e-05] 1 \n", + "\n", + " latency \n", + "0 82 \n", + "1 60 \n", + "2 120 \n", + "3 120 \n", + "4 120 \n" + ] + }, + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2025-02-12T12:21:17.409504\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.9.0, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "\n", + "# Read the CSV file\n", + "df = pd.read_pickle('test2.pkl')\n", + "\n", + "# Display the first few rows of the dataframe to understand its structure\n", + "print(df.head())\n", + "\n", + "df['flink_time'] = df['flink_time'] * 1000\n", + "df.plot(x='event_id', y=['latency', 'flink_time'], kind='line')\n", + "plt.xlabel('Event ID')\n", + "plt.ylabel('Latency (ms)')\n", + "# plt.yscale('log')\n", + "plt.title('Latency per Event')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2025-02-11T16:39:28.082024\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.9.0, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def preprocess_data(pickle_file_path):\n", + " # Read the DataFrame from the pickle file\n", + " df = pd.read_pickle(pickle_file_path)\n", + "\n", + " # Multiply flink_time by 1000 to convert to milliseconds\n", + " df['flink_time'] = df['flink_time'] * 1000\n", + "\n", + " # Calculate the additional Kafka overhead\n", + " df['kafka_overhead'] = df['latency'] - df['flink_time']\n", + "\n", + " # Extract median values from df\n", + " flink_time_median = df['flink_time'].median()\n", + " latency_median = df['latency'].median()\n", + "\n", + " return {\n", + " 'flink_time_median': flink_time_median,\n", + " 'kafka_overhead_median': latency_median - flink_time_median\n", + " }\n", + "\n", + "# List of pickle files\n", + "titles = ['baseline', 'pipelined', 'parallel*']\n", + "pickle_files = ['compose_basic_1mps.pkl', 'compose_pipeline_1mps.pkl', 'compose_parallel_1mps.pkl']\n", + "\n", + "titles = ['baseline', 'parallel*']\n", + "pickle_files = ['reserve_serial_1mps.pkl', 'reserve_parallel.pkl']\n", + "\n", + "# Process each file and collect median data\n", + "all_median_data = []\n", + "for title, file in zip(titles, pickle_files):\n", + " median_values = preprocess_data(file)\n", + " all_median_data.append({\n", + " 'Metric': 'Flink Time',\n", + " 'Value': median_values['flink_time_median'],\n", + " 'Title': title\n", + " })\n", + " all_median_data.append({\n", + " 'Metric': 'Kafka Overhead',\n", + " 'Value': median_values['kafka_overhead_median'],\n", + " 'Title': title\n", + " })\n", + "\n", + "# Create a DataFrame for all median values\n", + "all_median_df = pd.DataFrame(all_median_data)\n", + "\n", + "# Sort titles based on 'Flink Time' in descending order\n", + "sorted_titles = (\n", + " all_median_df[all_median_df['Metric'] == 'Flink Time']\n", + " .sort_values(by='Value', ascending=False)['Title']\n", + ")\n", + "\n", + "# Pivot the DataFrame and reindex to maintain sorted order\n", + "pivot_df = (\n", + " all_median_df.pivot(index='Title', columns='Metric', values='Value')\n", + " .reindex(sorted_titles)\n", + ")\n", + "\n", + "# Plot\n", + "fig, ax = plt.subplots()\n", + "pivot_df.plot(kind='bar', stacked=True, color=['#1f77b4', '#ff7f0e'], ax=ax)\n", + "plt.ylabel('Time (ms)')\n", + "plt.title('Median Flink Time and Kafka Overhead')\n", + "plt.legend(['Flink Time', 'Kafka Overhead'])\n", + "plt.show()\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/cascade/dataflow/dataflow.py b/src/cascade/dataflow/dataflow.py index 7e45dbd..bb5704b 100644 --- a/src/cascade/dataflow/dataflow.py +++ b/src/cascade/dataflow/dataflow.py @@ -1,6 +1,12 @@ -from abc import ABC +from abc import ABC, abstractmethod from dataclasses import dataclass, field from typing import Any, Callable, List, Optional, Type, Union +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + # Prevent circular imports + from cascade.dataflow.operator import StatelessOperator + class Operator(ABC): pass @@ -15,6 +21,9 @@ class InvokeMethod: """A method invocation of the underlying method indentifier.""" method_name: str + def __repr__(self) -> str: + return f"{self.__class__.__name__}('{self.method_name}')" + @dataclass class Filter: """Filter by this function""" @@ -23,10 +32,10 @@ class Filter: @dataclass class Node(ABC): """Base class for Nodes.""" + id: int = field(init=False) """This node's unique id.""" - _id_counter: int = field(init=False, default=0, repr=False) outgoing_edges: list['Edge'] = field(init=False, default_factory=list, repr=False) @@ -35,20 +44,121 @@ def __post_init__(self): self.id = Node._id_counter Node._id_counter += 1 + @abstractmethod + def propogate(self, event: 'Event', targets: list['Node'], result: Any, **kwargs) -> list['Event']: + pass + @dataclass class OpNode(Node): """A node in a `Dataflow` corresponding to a method call of a `StatefulOperator`. + A `Dataflow` may reference the same entity multiple times. + The `StatefulOperator` that this node belongs to is referenced by `entity`.""" + entity: Type + method_type: Union[InitClass, InvokeMethod, Filter] + read_key_from: str + """Which variable to take as the key for this StatefulOperator""" + + assign_result_to: Optional[str] = field(default=None) + """What variable to assign the result of this node to, if any.""" + is_conditional: bool = field(default=False) + """Whether or not the boolean result of this node dictates the following path.""" + collect_target: Optional['CollectTarget'] = field(default=None) + """Whether the result of this node should go to a CollectNode.""" + + def propogate(self, event: 'Event', targets: List[Node], result: Any) -> list['Event']: + return OpNode.propogate_opnode(self, event, targets, result) + + @staticmethod + def propogate_opnode(node: Union['OpNode', 'StatelessOpNode'], event: 'Event', targets: list[Node], + result: Any) -> list['Event']: + num_targets = 1 if node.is_conditional else len(targets) + + if event.collect_target is not None: + # Assign new collect targets + collect_targets = [ + event.collect_target for i in range(num_targets) + ] + else: + # Keep old collect targets + collect_targets = [node.collect_target for i in range(num_targets)] + + if node.is_conditional: + edges = event.dataflow.nodes[event.target.id].outgoing_edges + true_edges = [edge for edge in edges if edge.if_conditional] + false_edges = [edge for edge in edges if not edge.if_conditional] + if not (len(true_edges) == len(false_edges) == 1): + print(edges) + assert len(true_edges) == len(false_edges) == 1 + target_true = true_edges[0].to_node + target_false = false_edges[0].to_node + + assert len(collect_targets) == 1, "num targets should be 1" + ct = collect_targets[0] + + return [Event( + target_true if result else target_false, + event.variable_map, + event.dataflow, + _id=event._id, + collect_target=ct, + metadata=event.metadata) + ] + + else: + return [Event( + target, + event.variable_map, + event.dataflow, + _id=event._id, + collect_target=ct, + metadata=event.metadata) + + for target, ct in zip(targets, collect_targets)] + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.entity.__name__}, {self.method_type})" + +@dataclass +class StatelessOpNode(Node): + """A node in a `Dataflow` corresponding to a method call of a `StatelessOperator`. + A `Dataflow` may reference the same `StatefulOperator` multiple times. The `StatefulOperator` that this node belongs to is referenced by `cls`.""" - operator: Operator - method_type: Union[InitClass, InvokeMethod, Filter] + operator: 'StatelessOperator' + method_type: InvokeMethod + """Which variable to take as the key for this StatefulOperator""" + assign_result_to: Optional[str] = None + """What variable to assign the result of this node to, if any.""" is_conditional: bool = False """Whether or not the boolean result of this node dictates the following path.""" collect_target: Optional['CollectTarget'] = None """Whether the result of this node should go to a CollectNode.""" + def propogate(self, event: 'Event', targets: List[Node], result: Any) -> List['Event']: + return OpNode.propogate_opnode(self, event, targets, result) + +@dataclass +class DataflowNode(Node): + """A node in a `DataFlow` corresponding to the call of another dataflow""" + dataflow: 'DataFlow' + variable_rename: dict[str, str] + + assign_result_to: Optional[str] = None + """What variable to assign the result of this node to, if any.""" + is_conditional: bool = False + """Whether or not the boolean result of this node dictates the following path.""" + collect_target: Optional['CollectTarget'] = None + """Whether the result of this node should go to a CollectNode.""" + + def propogate(self, event: 'Event', targets: List[Node], result: Any) -> List['Event']: + # remap the variable map of event into the new event + + # add the targets as some sort of dataflow "exit nodes" + return self.dataflow + + @dataclass class SelectAllNode(Node): """A node type that will yield all items of an entity filtered by @@ -57,7 +167,24 @@ class SelectAllNode(Node): Think of this as executing `SELECT * FROM cls`""" cls: Type collect_target: 'CollectNode' - + assign_key_to: str + + def propogate(self, event: 'Event', targets: List[Node], result: Any, keys: list[str]): + targets = event.dataflow.get_neighbors(event.target) + assert len(targets) == 1 + n = len(keys) + collect_targets = [ + CollectTarget(self.collect_target, n, i) + for i in range(n) + ] + return [Event( + targets[0], + event.variable_map | {self.assign_key_to: key}, + event.dataflow, + _id=event._id, + collect_target=ct, + metadata=event.metadata) + for ct, key in zip(collect_targets, keys)] @dataclass class CollectNode(Node): @@ -70,6 +197,17 @@ class CollectNode(Node): read_results_from: str """The variable name in the variable map that the individual items put their result in.""" + def propogate(self, event: 'Event', targets: List[Node], result: Any, **kwargs) -> List['Event']: + collect_targets = [event.collect_target for i in range(len(targets))] + return [Event( + target, + event.variable_map, + event.dataflow, + _id=event._id, + collect_target=ct, + metadata=event.metadata) + + for target, ct in zip(targets, collect_targets)] @dataclass class Edge(): @@ -94,35 +232,19 @@ class DataFlow: item1[Item.get_price] item2[Item.get_price] user2[User.buy_items_1] - merge{Merge} + collect{Collect} user1-- item1_key -->item1; user1-- item2_key -->item2; - item1-- item1_price -->merge; - item2-- item2_price -->merge; - merge-- [item1_price, item2_price] -->user2; - ``` - - In code, one would write: - - ```py - df = DataFlow("user.buy_items") - n0 = OpNode(User, InvokeMethod("buy_items_0")) - n1 = OpNode(Item, InvokeMethod("get_price")) - n2 = OpNode(Item, InvokeMethod("get_price")) - n3 = MergeNode() - n4 = OpNode(User, InvokeMethod("buy_items_1")) - df.add_edge(Edge(n0, n1)) - df.add_edge(Edge(n0, n2)) - df.add_edge(Edge(n1, n3)) - df.add_edge(Edge(n2, n3)) - df.add_edge(Edge(n3, n4)) + item1-- item1_price -->collect; + item2-- item2_price -->collect; + collect-- [item1_price, item2_price] -->user2; ``` """ def __init__(self, name: str): self.name: str = name self.adjacency_list: dict[int, list[int]] = {} self.nodes: dict[int, Node] = {} - self.entry: Node = None + self.entry: Union[Node, List[Node]] = None def add_node(self, node: Node): """Add a node to the Dataflow graph if it doesn't already exist.""" @@ -134,24 +256,98 @@ def add_edge(self, edge: Edge): """Add an edge to the Dataflow graph. Nodes that don't exist will be added to the graph automatically.""" self.add_node(edge.from_node) self.add_node(edge.to_node) - self.adjacency_list[edge.from_node.id].append(edge.to_node.id) - edge.from_node.outgoing_edges.append(edge) + if edge.to_node.id not in self.adjacency_list[edge.from_node.id]: + self.adjacency_list[edge.from_node.id].append(edge.to_node.id) + edge.from_node.outgoing_edges.append(edge) + + + def remove_edge(self, from_node: Node, to_node: Node): + """Remove an edge from the Dataflow graph.""" + if from_node.id in self.adjacency_list and to_node.id in self.adjacency_list[from_node.id]: + # Remove from adjacency list + self.adjacency_list[from_node.id].remove(to_node.id) + # Remove from outgoing_edges + from_node.outgoing_edges = [ + edge for edge in from_node.outgoing_edges if edge.to_node.id != to_node.id + ] + + def remove_node(self, node: Node): + """Remove a node from the DataFlow graph and reconnect its parents to its children.""" + if node.id not in self.nodes: + return # Node doesn't exist in the graph + + + if isinstance(node, OpNode) or isinstance(node, StatelessOpNode): + assert not node.is_conditional, "there's no clear way to remove a conditional node" + assert not node.assign_result_to, "can't delete node whose result is used" + assert not node.collect_target, "can't delete node which has a collect_target" + + # Find parents (nodes that have edges pointing to this node) + parents = [parent_id for parent_id, children in self.adjacency_list.items() if node.id in children] + + # Find children (nodes that this node points to) + children = self.adjacency_list[node.id] + + # Set df entry + if self.entry == node: + print(children) + assert len(children) == 1, "cannot remove entry node if it doesn't exactly one child" + self.entry = self.nodes[children[0]] + + # Connect each parent to each child + for parent_id in parents: + parent_node = self.nodes[parent_id] + for child_id in children: + child_node = self.nodes[child_id] + new_edge = Edge(parent_node, child_node) + self.add_edge(new_edge) + + # Remove edges from parents to the node + for parent_id in parents: + parent_node = self.nodes[parent_id] + self.remove_edge(parent_node, node) + + # Remove outgoing edges from the node + for child_id in children: + child_node = self.nodes[child_id] + self.remove_edge(node, child_node) + + + + # Remove the node from the adjacency list and nodes dictionary + del self.adjacency_list[node.id] + del self.nodes[node.id] + def get_neighbors(self, node: Node) -> List[Node]: """Get the outgoing neighbors of this `Node`""" return [self.nodes[id] for id in self.adjacency_list.get(node.id, [])] -class Result(ABC): - pass + def to_dot(self) -> str: + """Output the DataFlow graph in DOT (Graphviz) format.""" + lines = [f"digraph {self.name} {{"] -@dataclass -class Arrived(Result): - val: Any + # Add nodes + for node in self.nodes.values(): + lines.append(f' {node.id} [label="{node}"];') -@dataclass -class NotArrived(Result): - pass + # Add edges + for from_id, to_ids in self.adjacency_list.items(): + for to_id in to_ids: + lines.append(f" {from_id} -> {to_id};") + lines.append("}") + return "\n".join(lines) + + def generate_event(self, variable_map: dict[str, Any]) -> Union['Event', list['Event']]: + if isinstance(self.entry, list): + assert len(self.entry) != 0 + first_event = Event(self.entry[0], variable_map, self) + id = first_event._id + return [first_event] + [Event(entry, variable_map, self, _id=id) for entry in self.entry[1:]] + else: + return Event(self.entry, variable_map, self) + @dataclass class CollectTarget: target_node: CollectNode @@ -161,6 +357,13 @@ class CollectTarget: result_idx: int """The index this result should be in the collected array.""" +def metadata_dict() -> dict: + return { + "in_t": None, + "deser_times": [], + "flink_time": 0 + } + @dataclass class Event(): """An Event is an object that travels through the Dataflow graph.""" @@ -168,11 +371,6 @@ class Event(): target: 'Node' """The Node that this Event wants to go to.""" - key_stack: list[str] - """The keys this event is concerned with. - The top of the stack, i.e. `key_stack[-1]`, should always correspond to a key - on the StatefulOperator of `target.cls` if `target` is an `OpNode`.""" - variable_map: dict[str, Any] """A mapping of variable identifiers to values. If `target` is an `OpNode` this map should include the variables needed for that method.""" @@ -188,6 +386,9 @@ class Event(): """Tells each mergenode (key) how many events to merge on""" _id_counter: int = field(init=False, default=0, repr=False) + + metadata: dict = field(default_factory=metadata_dict) + """Event metadata containing, for example, timestamps for benchmarking""" def __post_init__(self): if self._id is None: @@ -195,88 +396,27 @@ def __post_init__(self): self._id = Event._id_counter Event._id_counter += 1 - def propogate(self, key_stack, result) -> Union['EventResult', list['Event']]: + def propogate(self, result, select_all_keys: Optional[list[str]]=None) -> Union['EventResult', list['Event']]: """Propogate this event through the Dataflow.""" - # TODO: keys should be structs containing Key and Opnode (as we need to know the entity (cls) and method to invoke for that particular key) - # the following method only works because we assume all the keys have the same entity and method - if self.dataflow is None:# or len(key_stack) == 0: - return EventResult(self._id, result) + if self.dataflow is None: + return EventResult(self._id, result, self.metadata) targets = self.dataflow.get_neighbors(self.target) if len(targets) == 0: - return EventResult(self._id, result) + return EventResult(self._id, result, self.metadata) else: - keys = key_stack.pop() - if not isinstance(keys, list): - keys = [keys] - - collect_targets: list[Optional[CollectTarget]] - # Events with SelectAllNodes need to be assigned a CollectTarget - if isinstance(self.target, SelectAllNode): - collect_targets = [ - CollectTarget(self.target.collect_target, len(keys), i) - for i in range(len(keys)) - ] - elif isinstance(self.target, OpNode) and self.target.collect_target is not None: - collect_targets = [ - self.target.collect_target for i in range(len(keys)) - ] - else: - collect_targets = [self.collect_target for i in range(len(keys))] - - if isinstance(self.target, OpNode) and self.target.is_conditional: - # In this case there will be two targets depending on the condition - - edges = self.dataflow.nodes[self.target.id].outgoing_edges - true_edges = [edge for edge in edges if edge.if_conditional] - false_edges = [edge for edge in edges if not edge.if_conditional] - if not (len(true_edges) == len(false_edges) == 1): - print(edges) - assert len(true_edges) == len(false_edges) == 1 - target_true = true_edges[0].to_node - target_false = false_edges[0].to_node - - - return [Event( - target_true if result else target_false, - key_stack + [key], - self.variable_map, - self.dataflow, - _id=self._id, - collect_target=ct) - - for key, ct in zip(keys, collect_targets)] - - elif len(targets) == 1: - # We assume that all keys need to go to the same target - # this is only used for SelectAll propogation - - return [Event( - targets[0], - key_stack + [key], - self.variable_map, - self.dataflow, - _id=self._id, - collect_target=ct) - - for key, ct in zip(keys, collect_targets)] + current_node = self.target + + if isinstance(current_node, SelectAllNode): + assert select_all_keys + return current_node.propogate(self, targets, result, select_all_keys) else: - # An event with multiple targets should have the same number of - # keys in a list on top of its key stack - assert len(targets) == len(keys) - return [Event( - target, - key_stack + [key], - self.variable_map, - self.dataflow, - _id=self._id, - collect_target=ct) - - for target, key, ct in zip(targets, keys, collect_targets)] + return current_node.propogate(self, targets, result) @dataclass class EventResult(): event_id: int - result: Any \ No newline at end of file + result: Any + metadata: dict \ No newline at end of file diff --git a/src/cascade/dataflow/operator.py b/src/cascade/dataflow/operator.py index 6fca4d6..56d3e45 100644 --- a/src/cascade/dataflow/operator.py +++ b/src/cascade/dataflow/operator.py @@ -10,20 +10,19 @@ class MethodCall(Generic[T], Protocol): It corresponds to functions with the following signature: ```py - def my_compiled_method(*args: Any, state: T, key_stack: list[str], **kwargs: Any) -> Any: + def my_compiled_method(variable_map: dict[str, Any], state: T) -> Any ... ``` - `T` corresponds to a Python class, which, if modified, should return as the 2nd item in the tuple. - - The first item in the returned tuple corresponds to the actual return value of the function. + The variable_map contains a mapping from identifiers (variables/keys) to + their values. + The state of type `T` corresponds to a Python class. - The third item in the tuple corresponds to the `key_stack` which should be updated accordingly. - Notably, a terminal function should pop a key off the `key_stack`, whereas a function that calls - other functions should push the correct key(s) onto the `key_stack`. + + The value returned corresponds to the value treturned by the function. """ - def __call__(self, variable_map: dict[str, Any], state: T, key_stack: list[str]) -> dict[str, Any]: ... + def __call__(self, variable_map: dict[str, Any], state: T) -> Any: ... """@private""" @@ -61,14 +60,13 @@ def buy_item(self, item: Item) -> bool: Here, the class could be turned into a StatefulOperator as follows: ```py - def user_get_balance(variable_map: dict[str, Any], state: User, key_stack: list[str]): - key_stack.pop() + def user_get_balance(variable_map: dict[str, Any], state: User): return state.balance - def user_buy_item_0(variable_map: dict[str, Any], state: User, key_stack: list[str]): - key_stack.append(variable_map['item_key']) + def user_buy_item_0(variable_map: dict[str, Any], state: User): + pass - def user_buy_item_1(variable_map: dict[str, Any], state: User, key_stack: list[str]): + def user_buy_item_1(variable_map: dict[str, Any], state: User): state.balance -= variable_map['item_get_price'] return state.balance >= 0 @@ -100,19 +98,19 @@ def handle_init_class(self, *args, **kwargs) -> T: """Create an instance of the underlying class. Equivalent to `T.__init__(*args, **kwargs)`.""" return self.entity(*args, **kwargs) - def handle_invoke_method(self, method: InvokeMethod, variable_map: dict[str, Any], state: T, key_stack: list[str]) -> dict[str, Any]: + def handle_invoke_method(self, method: InvokeMethod, variable_map: dict[str, Any], state: T) -> dict[str, Any]: """Invoke the method of the underlying class. The `cascade.dataflow.dataflow.InvokeMethod` object must contain a method identifier that exists on the underlying compiled class functions. - The state `T` and key_stack is passed along to the function, and may be modified. + The state `T` is passed along to the function, and may be modified. """ - return self._methods[method.method_name](variable_map=variable_map, state=state, key_stack=key_stack) + return self._methods[method.method_name](variable_map=variable_map, state=state) class StatelessMethodCall(Protocol): - def __call__(self, variable_map: dict[str, Any], key_stack: list[str]) -> Any: ... + def __call__(self, variable_map: dict[str, Any]) -> Any: ... """@private""" @@ -123,13 +121,13 @@ def __init__(self, methods: dict[str, StatelessMethodCall], dataflow: DataFlow) self._methods = methods self.dataflow = dataflow - def handle_invoke_method(self, method: InvokeMethod, variable_map: dict[str, Any], key_stack: list[str]) -> dict[str, Any]: + def handle_invoke_method(self, method: InvokeMethod, variable_map: dict[str, Any]) -> dict[str, Any]: """Invoke the method of the underlying class. The `cascade.dataflow.dataflow.InvokeMethod` object must contain a method identifier that exists on the underlying compiled class functions. - The state `T` and key_stack is passed along to the function, and may be modified. + The state `T` is passed along to the function, and may be modified. """ - return self._methods[method.method_name](variable_map=variable_map, key_stack=key_stack) + return self._methods[method.method_name](variable_map=variable_map) diff --git a/src/cascade/dataflow/optimization/__init__.py b/src/cascade/dataflow/optimization/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/cascade/dataflow/optimization/dead_node_elim.py b/src/cascade/dataflow/optimization/dead_node_elim.py new file mode 100644 index 0000000..d1a9d06 --- /dev/null +++ b/src/cascade/dataflow/optimization/dead_node_elim.py @@ -0,0 +1,63 @@ +from cascade.dataflow.dataflow import DataFlow, InvokeMethod +from cascade.dataflow.operator import StatefulOperator, StatelessOperator +import inspect + +def is_no_op(func): + # Get the source code of the function + source = inspect.getsource(func).strip() + + # Extract the function body (skip the signature) + lines = source.splitlines() + if len(lines) < 2: + # A function with only a signature can't have a body + return False + + # Check the body of the function + body = lines[1].strip() + # A valid no-op function body is either 'pass' or 'return' + return body in ("pass", "return") + + +def dead_node_elimination(stateful_ops: list[StatefulOperator], stateless_ops: list[StatelessOperator]): + # Find dead functions + dead_func_names = set() + for op in stateful_ops: + for name, method in op._methods.items(): + if is_no_op(method): + dead_func_names.add(name) + + # Remove them from dataflows + for op in stateful_ops: + for dataflow in op.dataflows.values(): + to_remove = [] + for node in dataflow.nodes.values(): + if hasattr(node, "method_type") and isinstance(node.method_type, InvokeMethod): + im: InvokeMethod = node.method_type + if im.method_name in dead_func_names: + to_remove.append(node) + + for node in to_remove: + print(node) + dataflow.remove_node(node) + print(dataflow.to_dot()) + + # Find dead functions + dead_func_names = set() + for op in stateless_ops: + for name, method in op._methods.items(): + if is_no_op(method): + dead_func_names.add(name) + + # Remove them from dataflows + for op in stateless_ops: + to_remove = [] + for node in op.dataflow.nodes.values(): + if hasattr(node, "method_type") and isinstance(node.method_type, InvokeMethod): + im: InvokeMethod = node.method_type + if im.method_name in dead_func_names: + to_remove.append(node) + + for node in to_remove: + op.dataflow.remove_node(node) + + diff --git a/src/cascade/dataflow/optimization/parallelization.py b/src/cascade/dataflow/optimization/parallelization.py new file mode 100644 index 0000000..79e3ea4 --- /dev/null +++ b/src/cascade/dataflow/optimization/parallelization.py @@ -0,0 +1,191 @@ +""" +When is it safe to parallize nodes? + +-> When they don't affect each other +-> The simpelest way of doing it could be to run individual dataflows in parallel +(e.g. item.get_price() can run in parallel) +-> must convey that we assume no side-affects, so the actual order of execution +does not matter. could go deeper and give a spec. +-> some instructions from the same dataflow could also be completed in parallel? +maybe? like ILP. but might need to think of more contrived examples/do more +advanced program analyis. + +From Control Flow to Dataflow +3. Parallelizing Memory Operations +- operations on different memory locatiosn need not be sequentialized +- circulate a set of access tokens for each variable (=split function?) + - assume that every variable denotes a unique memory location (no aliasing) + +We have to be careful about certain types of parallelization. Consider the example: + +``` +# Calculate the average item price in basket: List[Item] +n = 0 +p = 0 +for item in basket: + n += 1 + p += item.price() +return p / n +``` + +In this example we would want to parallelize the calls to item.price(). +But we have to make sure the calls to `n += 1` remains bounded to the number of +items, even though there is no explicit data dependency. + + +---- + + +There is another type of optimization we could look at. +Suppose the following: + +``` +n = self.basket_size + +prices = [item.price() for item in self.basket] +total_price = sum(prices) + +return total_price / n +``` + +In this case, the variable n is not needed in the list comprehension - unoptimized +versions would generate an extra function instead of having the line be re-ordered +into the bottom function. Instead, analyis of the variables each function needs +access to would be a way to optimize these parts! + +--> Ask Soham about this! + +from "From control flow to dataflow" + +Consider the portion of control-flow graph between a node N and its *immediate +postdominator* P. Every control-flow path starting at N ultimately ends up at P. +Suppose that there is no reference to a variable x in any node on any path between +N and P. It is clear that an access token for x that enters N may bypass this +region of the graph altogether and go directly to P. + + +---- + +"Dataflow-Based Parallelization of Control-Flow Algorithms" + +loop invariant hoisting + +``` +i = 0 +while i < n: + x = y + z + a[i] = 6 * i + x * x + i += 1 +``` + +can be transformed in + +``` +i = 0 +if i < n: + x = y + z # loop invariant 1 + t1 = x * x # loop invariant 2 + do { # do while loop needed in case the conditional has side effects + a[i] = 6 * i + t1 + i += 1 + } while i < n +``` + +this is achieved using reaching definitions analysis. In the paper: +"It is a common optimization to pull those parts of a loop body +that depend on only static datasets outside of the loop, and thus +execute these parts only once [7 , 13 , 15 , 32 ]. However, launching +new dataflow jobs for every iteration step prevents this optimiza- +tion in the case of such binary operators where only one input is +static. For example, if a static dataset is used as the build-side of +a hash join, then the system should not rebuild the hash table at +every iteration step. Labyrinth operators can keep such a hash +table in their internal states between iteration steps. This is made +possible by implementing iterations as a single cyclic dataflow +job, where the lifetimes of operators span all the steps." +Is there a similair example we could leverage for cascade? one with a "static dataset" as loop invariant? +in spark, it's up to the programmer to .cache it + + +In this paper, they also use an intermediate representation of one "basic block" per node. +A "basic block" is a sequence of instructions that always execute one after the other, +in other words contains no control flow. Control flow is defined by the edges in the +dataflow graph that connect the nodes. + +There's also a slightly different focus of this paper. The focus is not on stateful +dataflows, and obviously the application is still focused on bigdata-like applications, +not ones were latency is key issue. + + +Basic Blocks - Aho, A. V., Sethi, R., and Ullman, J. D. Compilers: principles, techniques, and +tools, vol. 2. Addison-wesley Reading, 2007. +SSA - Rastello, F. SSA-based Compiler Design. Springer Publishing Company, +Incorporated, 2016. + + +---- + +ideas from "optimization of dataflows with UDFs:" + +we are basically making a DSL (integrated with python) which would allow for optimization +of UDFs!! this optimization is inside the intermediate representation, and not directly in +the target machine (similair to Emma, which uses a functional style *but* is a DSL (does it +allow for arbitrary scala code?)) + +--- + +our program is essentially a compiler. this allows to take inspiration from existing +works on compilation (which has existed for much longer than work on dataflows (?) - +actually, dataflows were more popular initially when people didn't settle on the von Neumann architecture yet, +see e.g. Monsoon (1990s) or the original control flow to dataflow paper. the popularisation and efficiency of tools +such as drayadlinq, apache spark, apache flink has reinvigorated the attention towards dataflows). +BUT compilers are often have hardware specific optimizations, based on the hardware instruction sets, or hardware-specifics +such as optimization of register allocation, cache line considerations etc etc. +The compiler in Cascade/other cf to df systems do not necessarily have the same considerations. This is because the backend +is software rather than hardware (e.g. we use flink + kafka). Since software is generally a lot more flexible than hardware, +we can instead impose certain considerations on the execution engine (which is now software, instead of a chip) rather than +the other way around (e.g. SIMD introduced --> compiler optimizations introduced). (to be fair, compiler design has had major influences [citation needed] on CPU design, but the point is that hardware iteration +is generally slower and more expensive than software iteration). + + +--- + +for certain optimizations, cascade assumes order of any side effects (such as file IO) does not matter. +otherwise a lot of parallelization operations would become much more costly due to the necessary synchronization issues. + +--- + +other optimization: code duplication + +this would remove nodes (assumption that less nodes = faster) at the cost of more computation per node. +a common example is something like this: + +``` +cost = item.price() +if cost > 30: + shipping_discount = discount_service.get_shipping_discount() + price = cost * shipping_discount +else: + price = cost + +return price +``` + +in this case the "return price" could be duplicated accross the two branches, +such that they don't need to return back to the function body. + +--- + +other ideas: + https://en.wikipedia.org/wiki/Optimizing_compiler#Specific_techniques +""" + +from cascade.dataflow.operator import StatefulOperator, StatelessOperator + + +def node_parallelization(stateful_ops: list[StatefulOperator], stateless_ops: list[StatelessOperator]): + # Find parallelizable nodes + for op in stateful_ops: + for dataflow in op.dataflows.values(): + pass + # Parallize them \ No newline at end of file diff --git a/src/cascade/dataflow/optimization/test_dead_node_elim.py b/src/cascade/dataflow/optimization/test_dead_node_elim.py new file mode 100644 index 0000000..94b30af --- /dev/null +++ b/src/cascade/dataflow/optimization/test_dead_node_elim.py @@ -0,0 +1,103 @@ +from typing import Any + +from cascade.dataflow.dataflow import DataFlow, Edge, InvokeMethod, OpNode +from cascade.dataflow.operator import StatefulOperator +from cascade.dataflow.optimization.dead_node_elim import dead_node_elimination +from cascade.dataflow.optimization.dead_node_elim import is_no_op + +class User: + pass + +class Hotel: + pass + +class Flight: + pass + +def order_compiled_entry_0(variable_map: dict[str, Any], state: User) -> Any: + pass + +def order_compiled_entry_1(variable_map: dict[str, Any], state: User) -> Any: + pass + +def order_compiled_if_cond(variable_map: dict[str, Any], state: User) -> Any: + return variable_map["hotel_reserve"] and variable_map["flight_reserve"] + +def order_compiled_if_body(variable_map: dict[str, Any], state: User) -> Any: + return True + +def order_compiled_else_body(variable_map: dict[str, Any], state: User) -> Any: + return False + +user_op = StatefulOperator( + User, + { + "order_compiled_entry_0": order_compiled_entry_0, + "order_compiled_entry_1": order_compiled_entry_1, + "order_compiled_if_cond": order_compiled_if_cond, + "order_compiled_if_body": order_compiled_if_body, + "order_compiled_else_body": order_compiled_else_body + }, + {} +) + +# For now, the dataflow will be serial instead of parallel (calling hotel, then +# flight). Future optimizations could try to automatically parallelize this. +# There could definetly be some slight changes to this dataflow depending on +# other optimizations aswell. (A naive system could have an empty first entry +# before the first entity call). +def user_order_df(): + df = DataFlow("user_order") + n0 = OpNode(User, InvokeMethod("order_compiled_entry_0"), read_key_from="user_key") + n1 = OpNode(Hotel, InvokeMethod("reserve"), assign_result_to="hotel_reserve", read_key_from="hotel_key") + n2 = OpNode(User, InvokeMethod("order_compiled_entry_1"), read_key_from="user_key") + n3 = OpNode(Flight, InvokeMethod("reserve"), assign_result_to="flight_reserve", read_key_from="flight_key") + n4 = OpNode(User, InvokeMethod("order_compiled_if_cond"), is_conditional=True, read_key_from="user_key") + n5 = OpNode(User, InvokeMethod("order_compiled_if_body"), read_key_from="user_key") + n6 = OpNode(User, InvokeMethod("order_compiled_else_body"), read_key_from="user_key") + + df.add_edge(Edge(n0, n1)) + df.add_edge(Edge(n1, n2)) + df.add_edge(Edge(n2, n3)) + df.add_edge(Edge(n3, n4)) + df.add_edge(Edge(n4, n5, if_conditional=True)) + df.add_edge(Edge(n4, n6, if_conditional=False)) + + df.entry = n0 + return df + +df = user_order_df() +user_op.dataflows[df.name] = df + +def test_dead_node_elim(): + print(user_op.dataflows[df.name].to_dot()) + + dead_node_elimination([user_op], []) + + print(user_op.dataflows[df.name].to_dot()) + + + +### TEST NO OP DETECTION ### + +def c1(variable_map: dict[str, Any]): + return (variable_map["dist"], variable_map["hotel_key"]) + +def c2(variable_map: dict[str, Any]): + return None + +def c3(variable_map: dict[str, Any]): + return + +def c4(variable_map: dict[str, Any]): + pass + +def c5(variable_map: dict[str, Any]): + return True + +def test_no_op_detect(): + assert not is_no_op(c1) + assert not is_no_op(c2) + assert is_no_op(c3) + assert is_no_op(c4) + assert not is_no_op(c5) diff --git a/src/cascade/dataflow/test_dataflow.py b/src/cascade/dataflow/test_dataflow.py index 1e29aad..a5b42af 100644 --- a/src/cascade/dataflow/test_dataflow.py +++ b/src/cascade/dataflow/test_dataflow.py @@ -12,14 +12,12 @@ def buy_item(self, item: 'DummyItem') -> bool: self.balance -= item_price return self.balance >= 0 -def buy_item_0_compiled(variable_map: dict[str, Any], state: DummyUser, key_stack: list[str]) -> dict[str, Any]: - key_stack.append(variable_map["item_key"]) +def buy_item_0_compiled(variable_map: dict[str, Any], state: DummyUser): return -def buy_item_1_compiled(variable_map: dict[str, Any], state: DummyUser, key_stack: list[str]) -> dict[str, Any]: - key_stack.pop() +def buy_item_1_compiled(variable_map: dict[str, Any], state: DummyUser): state.balance -= variable_map["item_price"] - return {"user_postive_balance": state.balance >= 0} + return state.balance >= 0 class DummyItem: def __init__(self, key: str, price: int): @@ -29,10 +27,8 @@ def __init__(self, key: str, price: int): def get_price(self) -> int: return self.price -def get_price_compiled(variable_map: dict[str, Any], state: DummyItem, key_stack: list[str]) -> dict[str, Any]: - key_stack.pop() # final function - variable_map["item_price"] = state.price - # return {"item_price": state.price} +def get_price_compiled(variable_map: dict[str, Any], state: DummyItem): + return state.price ################## TESTS ####################### @@ -46,53 +42,60 @@ def get_price_compiled(variable_map: dict[str, Any], state: DummyItem, key_stack def test_simple_df_propogation(): df = DataFlow("user.buy_item") - n1 = OpNode(DummyUser, InvokeMethod("buy_item_0_compiled")) - n2 = OpNode(DummyItem, InvokeMethod("get_price")) - n3 = OpNode(DummyUser, InvokeMethod("buy_item_1")) + n1 = OpNode(DummyUser, InvokeMethod("buy_item_0_compiled"), read_key_from="user_key") + n2 = OpNode(DummyItem, InvokeMethod("get_price"), read_key_from="item_key", assign_result_to="item_price") + n3 = OpNode(DummyUser, InvokeMethod("buy_item_1"), read_key_from="user_key") df.add_edge(Edge(n1, n2)) df.add_edge(Edge(n2, n3)) user.buy_item(item) - event = Event(n1, ["user"], {"item_key":"fork"}, df) + event = Event(n1, {"user_key": "user", "item_key":"fork"}, df) # Manually propogate - item_key = buy_item_0_compiled(event.variable_map, state=user, key_stack=event.key_stack) - next_event = event.propogate(event.key_stack, item_key) + item_key = buy_item_0_compiled(event.variable_map, state=user) + next_event = event.propogate(event, item_key) + assert isinstance(next_event, list) assert len(next_event) == 1 assert next_event[0].target == n2 - assert next_event[0].key_stack == ["user", "fork"] event = next_event[0] - item_price = get_price_compiled(event.variable_map, state=item, key_stack=event.key_stack) - next_event = event.propogate(event.key_stack, item_price) + # manually add the price to the variable map + item_price = get_price_compiled(event.variable_map, state=item) + assert n2.assign_result_to + event.variable_map[n2.assign_result_to] = item_price + next_event = event.propogate(item_price) + + assert isinstance(next_event, list) assert len(next_event) == 1 assert next_event[0].target == n3 event = next_event[0] - positive_balance = buy_item_1_compiled(event.variable_map, state=user, key_stack=event.key_stack) - next_event = event.propogate(event.key_stack, None) + positive_balance = buy_item_1_compiled(event.variable_map, state=user) + next_event = event.propogate(None) assert isinstance(next_event, EventResult) def test_merge_df_propogation(): df = DataFlow("user.buy_2_items") - n0 = OpNode(DummyUser, InvokeMethod("buy_2_items_0")) + n0 = OpNode(DummyUser, InvokeMethod("buy_2_items_0"), read_key_from="user_key") n3 = CollectNode(assign_result_to="item_prices", read_results_from="item_price") n1 = OpNode( DummyItem, InvokeMethod("get_price"), assign_result_to="item_price", - collect_target=CollectTarget(n3, 2, 0) + collect_target=CollectTarget(n3, 2, 0), + read_key_from="item_1_key" ) n2 = OpNode( DummyItem, InvokeMethod("get_price"), assign_result_to="item_price", - collect_target=CollectTarget(n3, 2, 1) + collect_target=CollectTarget(n3, 2, 1), + read_key_from="item_2_key" ) - n4 = OpNode(DummyUser, InvokeMethod("buy_2_items_1")) + n4 = OpNode(DummyUser, InvokeMethod("buy_2_items_1"), read_key_from="user_key") df.add_edge(Edge(n0, n1)) df.add_edge(Edge(n0, n2)) df.add_edge(Edge(n1, n3)) @@ -100,25 +103,30 @@ def test_merge_df_propogation(): df.add_edge(Edge(n3, n4)) # User with key "foo" buys items with keys "fork" and "spoon" - event = Event(n0, ["foo"], {"item_1_key": "fork", "item_2_key": "spoon"}, df) + event = Event(n0, {"user_key": "foo", "item_1_key": "fork", "item_2_key": "spoon"}, df) # Propogate the event (without actually doing any calculation) # Normally, the key_stack should've been updated by the runtime here: - key_stack = ["foo", ["fork", "spoon"]] - next_event = event.propogate(key_stack, None) + next_event = event.propogate(None) + assert isinstance(next_event, list) assert len(next_event) == 2 assert next_event[0].target == n1 assert next_event[1].target == n2 event1, event2 = next_event - next_event = event1.propogate(event1.key_stack, None) + next_event = event1.propogate(None) + + assert isinstance(next_event, list) assert len(next_event) == 1 assert next_event[0].target == n3 - next_event = event2.propogate(event2.key_stack, None) + next_event = event2.propogate(None) + + assert isinstance(next_event, list) assert len(next_event) == 1 assert next_event[0].target == n3 - final_event = next_event[0].propogate(next_event[0].key_stack, None) + final_event = next_event[0].propogate(None) + assert isinstance(final_event, list) assert final_event[0].target == n4 diff --git a/src/cascade/runtime/flink_runtime.py b/src/cascade/runtime/flink_runtime.py index fd74279..febfc83 100644 --- a/src/cascade/runtime/flink_runtime.py +++ b/src/cascade/runtime/flink_runtime.py @@ -1,9 +1,10 @@ +from abc import ABC from dataclasses import dataclass import os import time import uuid import threading -from typing import Literal, Optional, Type, Union +from typing import Any, Literal, Optional, Type, Union from pyflink.common.typeinfo import Types, get_gateway from pyflink.common import Configuration, DeserializationSchema, SerializationSchema, WatermarkStrategy from pyflink.datastream.connectors import DeliveryGuarantee @@ -12,7 +13,7 @@ from pyflink.datastream.connectors.kafka import KafkaOffsetsInitializer, KafkaRecordSerializationSchema, KafkaSource, KafkaSink from pyflink.datastream import ProcessFunction, StreamExecutionEnvironment import pickle -from cascade.dataflow.dataflow import Arrived, CollectNode, CollectTarget, Event, EventResult, Filter, InitClass, InvokeMethod, Node, NotArrived, OpNode, Result, SelectAllNode +from cascade.dataflow.dataflow import CollectNode, CollectTarget, Event, EventResult, Filter, InitClass, InvokeMethod, Node, OpNode, SelectAllNode, StatelessOpNode from cascade.dataflow.operator import StatefulOperator, StatelessOperator from confluent_kafka import Producer, Consumer import logging @@ -36,6 +37,10 @@ class FlinkRegisterKeyNode(Node): key: str cls: Type + def propogate(self, event: Event, targets: list[Node], result: Any, **kwargs) -> list[Event]: + """A key registration event does not propogate.""" + return [] + class FlinkOperator(KeyedProcessFunction): """Wraps an `cascade.dataflow.datflow.StatefulOperator` in a KeyedProcessFunction so that it can run in Flink. """ @@ -49,51 +54,55 @@ def open(self, runtime_context: RuntimeContext): self.state: ValueState = runtime_context.get_state(descriptor) def process_element(self, event: Event, ctx: KeyedProcessFunction.Context): - key_stack = event.key_stack # should be handled by filters on this FlinkOperator assert(isinstance(event.target, OpNode)) - assert(isinstance(event.target.operator, StatefulOperator)) - assert(event.target.operator.entity == self.operator.entity) - logger.debug(f"FlinkOperator {self.operator.entity.__name__}[{ctx.get_current_key()}]: Processing: {event.target.method_type}") + + assert(event.target.entity == self.operator.entity) + key = ctx.get_current_key() + assert(key is not None) + if isinstance(event.target.method_type, InitClass): # TODO: compile __init__ with only kwargs, and pass the variable_map itself # otherwise, order of variable_map matters for variable assignment result = self.operator.handle_init_class(*event.variable_map.values()) # Register the created key in FlinkSelectAllOperator - register_key_event = Event( - FlinkRegisterKeyNode(key_stack[-1], self.operator.entity), - [], - {}, - None, - _id = event._id - ) - logger.debug(f"FlinkOperator {self.operator.entity.__name__}[{ctx.get_current_key()}]: Registering key: {register_key_event}") - yield register_key_event + # register_key_event = Event( + # FlinkRegisterKeyNode(key, self.operator.entity), + # {}, + # None, + # _id = event._id + # ) + # logger.debug(f"FlinkOperator {self.operator.entity.__name__}[{ctx.get_current_key()}]: Registering key: {register_key_event}") + # yield register_key_event - # Pop this key from the key stack so that we exit - key_stack.pop() self.state.update(pickle.dumps(result)) elif isinstance(event.target.method_type, InvokeMethod): - state = pickle.loads(self.state.value()) - result = self.operator.handle_invoke_method(event.target.method_type, variable_map=event.variable_map, state=state, key_stack=key_stack) + state = self.state.value() + if state is None: + # try to create the state if we haven't been init'ed + state = self.operator.handle_init_class(*event.variable_map.values()) + else: + state = pickle.loads(state) + + result = self.operator.handle_invoke_method(event.target.method_type, variable_map=event.variable_map, state=state) # TODO: check if state actually needs to be updated if state is not None: self.state.update(pickle.dumps(state)) - elif isinstance(event.target.method_type, Filter): - state = pickle.loads(self.state.value()) - result = event.target.method_type.filter_fn(event.variable_map, state) - if not result: - return - result = event.key_stack[-1] + # elif isinstance(event.target.method_type, Filter): + # state = pickle.loads(self.state.value()) + # result = event.target.method_type.filter_fn(event.variable_map, state) + # if not result: + # return + # result = event.key_stack[-1] if event.target.assign_result_to is not None: event.variable_map[event.target.assign_result_to] = result - new_events = event.propogate(key_stack, result) + new_events = event.propogate(result) if isinstance(new_events, EventResult): logger.debug(f"FlinkOperator {self.operator.entity.__name__}[{ctx.get_current_key()}]: Returned {new_events}") yield new_events @@ -110,22 +119,20 @@ def __init__(self, operator: StatelessOperator) -> None: def process_element(self, event: Event, ctx: KeyedProcessFunction.Context): - key_stack = event.key_stack # should be handled by filters on this FlinkOperator - assert(isinstance(event.target, OpNode)) - assert(isinstance(event.target.operator, StatelessOperator)) + assert(isinstance(event.target, StatelessOpNode)) logger.debug(f"FlinkStatelessOperator {self.operator.dataflow.name}[{event._id}]: Processing: {event.target.method_type}") if isinstance(event.target.method_type, InvokeMethod): - result = self.operator.handle_invoke_method(event.target.method_type, variable_map=event.variable_map, key_stack=key_stack) + result = self.operator.handle_invoke_method(event.target.method_type, variable_map=event.variable_map) else: raise Exception(f"A StatelessOperator cannot compute event type: {event.target.method_type}") if event.target.assign_result_to is not None: event.variable_map[event.target.assign_result_to] = result - new_events = event.propogate(key_stack, result) + new_events = event.propogate(result) if isinstance(new_events, EventResult): logger.debug(f"FlinkStatelessOperator {self.operator.dataflow.name}[{event._id}]: Returned {new_events}") yield new_events @@ -157,11 +164,12 @@ def process_element(self, event: Event, ctx: 'ProcessFunction.Context'): logger.debug(f"SelectAllOperator [{event.target.cls.__name__}]: Selecting all") # Yield all the keys we now about - event.key_stack.append(state) + new_keys = state num_events = len(state) # Propogate the event to the next node - new_events = event.propogate(event.key_stack, None) + new_events = event.propogate(None, select_all_keys=new_keys) + assert isinstance(new_events, list), "SelectAll nodes shouldn't directly produce EventResults" assert num_events == len(new_events) logger.debug(f"SelectAllOperator [{event.target.cls.__name__}]: Propogated {num_events} events with target: {event.target.collect_target}") @@ -169,6 +177,21 @@ def process_element(self, event: Event, ctx: 'ProcessFunction.Context'): else: raise Exception(f"Unexpected target for SelectAllOperator: {event.target}") +class Result(ABC): + """A `Result` can be either `Arrived` or `NotArrived`. It is used in the + FlinkCollectOperator to determine whether all the events have completed + their computation.""" + pass + +@dataclass +class Arrived(Result): + val: Any + +@dataclass +class NotArrived(Result): + pass + + class FlinkCollectOperator(KeyedProcessFunction): """Flink implementation of a merge operator.""" def __init__(self): @@ -208,7 +231,7 @@ def process_element(self, event: Event, ctx: KeyedProcessFunction.Context): collection = [r.val for r in collection if r.val is not None] # type: ignore (r is of type Arrived) event.variable_map[target_node.assign_result_to] = collection - new_events = event.propogate(event.key_stack, collection) + new_events = event.propogate(collection) self.collection.clear() if isinstance(new_events, EventResult): @@ -218,28 +241,6 @@ def process_element(self, event: Event, ctx: KeyedProcessFunction.Context): logger.debug(f"FlinkCollectOp [{ctx.get_current_key()}]: Propogated {len(new_events)} new Events") yield from new_events -class FlinkMergeOperator(KeyedProcessFunction): - """Flink implementation of a merge operator.""" - def __init__(self) -> None: - self.other: ValueState = None # type: ignore (expect state to be initialised on .open()) - - def open(self, runtime_context: RuntimeContext): - descriptor = ValueStateDescriptor("merge_state", Types.PICKLED_BYTE_ARRAY()) - self.other = runtime_context.get_state(descriptor) - - def process_element(self, event: Event, ctx: KeyedProcessFunction.Context): - other_map = self.other.value() - logger.debug(f"FlinkMergeOp [{ctx.get_current_key()}]: Processing: {event}") - if other_map == None: - logger.debug(f"FlinkMergeOp [{ctx.get_current_key()}]: Saving variable map") - self.other.update(event.variable_map) - else: - self.other.clear() - logger.debug(f"FlinkMergeOp [{ctx.get_current_key()}]: Yielding merged variables") - event.variable_map |= other_map - new_event = event.propogate(event.key_stack, None) - yield from new_event - class ByteSerializer(SerializationSchema, DeserializationSchema): """A custom serializer which maps bytes to bytes. @@ -284,6 +285,35 @@ def __init__(self): self, j_deserialization_schema=j_byte_string_schema ) +def deserialize_and_timestamp(x) -> Event: + t1 = time.time() + e: Event = pickle.loads(x) + t2 = time.time() + if e.metadata["in_t"] is None: + e.metadata["in_t"] = t1 + e.metadata["current_in_t"] = t1 + e.metadata["deser_times"].append(t2 - t1) + return e + +def timestamp_event(e: Event) -> Event: + t1 = time.time() + try: + e.metadata["flink_time"] += t1 - e.metadata["current_in_t"] + except KeyError: + pass + return e + +def timestamp_result(e: EventResult) -> EventResult: + t1 = time.time() + e.metadata["out_t"] = t1 + e.metadata["flink_time"] += t1 - e.metadata["current_in_t"] + e.metadata["loops"] = len(e.metadata["deser_times"]) + e.metadata["roundtrip"] = e.metadata["out_t"] - e.metadata["in_t"] + return e + +def debug(x, msg=""): + logger.debug(msg) + return x class FlinkRuntime(): """A Runtime that runs Dataflows on Flink.""" @@ -337,6 +367,10 @@ def init(self, kafka_broker="localhost:9092", bundle_time=1, bundle_size=5, para config.set_integer("python.fn-execution.bundle.time", bundle_time) config.set_integer("python.fn-execution.bundle.size", bundle_size) + # optimize for low latency + config.set_integer("taskmanager.memory.managed.size", 0) + config.set_integer("execution.buffer-timeout", 0) + self.env = StreamExecutionEnvironment.get_execution_environment(config) if parallelism: self.env.set_parallelism(parallelism) @@ -401,11 +435,13 @@ def init(self, kafka_broker="localhost:9092", bundle_time=1, bundle_size=5, para WatermarkStrategy.no_watermarks(), "Kafka Source" ) - .map(lambda x: pickle.loads(x)) + .map(lambda x: deserialize_and_timestamp(x)) + # .map(lambda x: debug(x, msg=f"entry: {x}")) .name("DESERIALIZE") # .filter(lambda e: isinstance(e, Event)) # Enforced by `send` type safety ) - + + """REMOVE SELECT ALL NODES # Events with a `SelectAllNode` will first be processed by the select # all operator, which will send out multiple other Events that can # then be processed by operators in the same steam. @@ -415,33 +451,26 @@ def init(self, kafka_broker="localhost:9092", bundle_time=1, bundle_size=5, para .key_by(lambda e: e.target.cls) .process(FlinkSelectAllOperator()).name("SELECT ALL OP") ) - """Stream that ingests events with an `SelectAllNode` or `FlinkRegisterKeyNode`""" + # Stream that ingests events with an `SelectAllNode` or `FlinkRegisterKeyNode` not_select_all_stream = ( event_stream.filter(lambda e: not (isinstance(e.target, SelectAllNode) or isinstance(e.target, FlinkRegisterKeyNode))) ) - event_stream_2 = select_all_stream.union(not_select_all_stream) - - operator_stream = event_stream_2.filter(lambda e: isinstance(e.target, OpNode)).name("OPERATOR STREAM") - - self.stateful_op_stream = ( - operator_stream - .filter(lambda e: isinstance(e.target.operator, StatefulOperator)) - ) + operator_stream = select_all_stream.union(not_select_all_stream) + """ - self.stateless_op_stream = ( - operator_stream - .filter(lambda e: isinstance(e.target.operator, StatelessOperator)) - ) + self.stateful_op_stream = event_stream + self.stateless_op_stream = event_stream - self.merge_op_stream = ( - event_stream.filter(lambda e: isinstance(e.target, CollectNode)) - .key_by(lambda e: e._id) # might not work in the future if we have multiple merges in one dataflow? - .process(FlinkCollectOperator()) - .name("Collect") - ) - """Stream that ingests events with an `cascade.dataflow.dataflow.CollectNode` target""" + # MOVED TO END OF OP STREAMS! + # self.merge_op_stream = ( + # event_stream.filter(lambda e: isinstance(e.target, CollectNode)) + # .key_by(lambda e: e._id) # might not work in the future if we have multiple merges in one dataflow? + # .process(FlinkCollectOperator()) + # .name("Collect") + # ) + # """Stream that ingests events with an `cascade.dataflow.dataflow.CollectNode` target""" self.stateless_op_streams = [] self.stateful_op_streams = [] @@ -455,9 +484,11 @@ def add_operator(self, op: StatefulOperator): flink_op = FlinkOperator(op) op_stream = ( - self.stateful_op_stream.filter(lambda e: e.target.operator.entity == flink_op.operator.entity) - .key_by(lambda e: e.key_stack[-1]) + self.stateful_op_stream.filter(lambda e: isinstance(e.target, OpNode) and e.target.entity == flink_op.operator.entity) + # .map(lambda x: debug(x, msg=f"filtered op: {op.entity}")) + .key_by(lambda e: e.variable_map[e.target.read_key_from]) .process(flink_op) + # .map(lambda x: debug(x, msg=f"processed op: {op.entity}")) .name("STATEFUL OP: " + flink_op.operator.entity.__name__) ) self.stateful_op_streams.append(op_stream) @@ -468,7 +499,7 @@ def add_stateless_operator(self, op: StatelessOperator): op_stream = ( self.stateless_op_stream - .filter(lambda e: e.target.operator.dataflow.name == flink_op.operator.dataflow.name) + .filter(lambda e: isinstance(e.target, StatelessOpNode) and e.target.operator.dataflow.name == flink_op.operator.dataflow.name) .process(flink_op) .name("STATELESS DATAFLOW: " + flink_op.operator.dataflow.name) ) @@ -495,8 +526,21 @@ def run(self, run_async=False, output: Literal["collect", "kafka", "stdout"]="ka logger.debug("FlinkRuntime merging operator streams...") # Combine all the operator streams - operator_streams = self.merge_op_stream.union(*self.stateful_op_streams).union(*self.stateless_op_streams) + # operator_streams = self.merge_op_stream.union(*self.stateful_op_streams[1:], *self.stateless_op_streams)#.map(lambda x: debug(x, msg="combined ops")) + s1 = self.stateful_op_streams[0] + rest = self.stateful_op_streams[1:] + operator_streams = s1.union(*rest, *self.stateless_op_streams)#.map(lambda x: debug(x, msg="combined ops")) + + merge_op_stream = ( + operator_streams.filter(lambda e: isinstance(e, Event) and isinstance(e.target, CollectNode)) + .key_by(lambda e: e._id) # might not work in the future if we have multiple merges in one dataflow? + .process(FlinkCollectOperator()) + .name("Collect") + ) + """Stream that ingests events with an `cascade.dataflow.dataflow.CollectNode` target""" + + """ # Add filtering for nodes with a `Filter` target full_stream_filtered = ( operator_streams @@ -508,18 +552,26 @@ def run(self, run_async=False, output: Literal["collect", "kafka", "stdout"]="ka .filter(lambda e: not (isinstance(e, Event) and isinstance(e.target, Filter))) ) ds = full_stream_filtered.union(full_stream_unfiltered) + """ + # union with EventResults or Events that don't have a CollectNode target + ds = merge_op_stream.union(operator_streams.filter(lambda e: not (isinstance(e, Event) and isinstance(e.target, CollectNode)))) # Output the stream + results = ( + ds + .filter(lambda e: isinstance(e, EventResult)) + .map(lambda e: timestamp_result(e)) + ) if output == "collect": - ds_external = ds.filter(lambda e: isinstance(e, EventResult)).execute_and_collect() + ds_external = results.execute_and_collect() elif output == "stdout": - ds_external = ds.filter(lambda e: isinstance(e, EventResult)).print() + ds_external = results.print() elif output == "kafka": - ds_external = ds.filter(lambda e: isinstance(e, EventResult)).sink_to(self.kafka_external_sink).name("EXTERNAL KAFKA SINK") + ds_external = results.sink_to(self.kafka_external_sink).name("EXTERNAL KAFKA SINK") else: raise ValueError(f"Invalid output: {output}") - ds_internal = ds.filter(lambda e: isinstance(e, Event)).sink_to(self.kafka_internal_sink).name("INTERNAL KAFKA SINK") + ds_internal = ds.filter(lambda e: isinstance(e, Event)).map(lambda e: timestamp_event(e)).sink_to(self.kafka_internal_sink).name("INTERNAL KAFKA SINK") if run_async: logger.debug("FlinkRuntime starting (async)") @@ -583,7 +635,20 @@ def consume_results(self): def flush(self): self.producer.flush() - def send(self, event: Event, flush=False) -> int: + def send(self, event: Union[Event, list[Event]], flush=False) -> int: + if isinstance(event, list): + for e in event: + id = self._send(e) + else: + id = self._send(event) + + if flush: + self.producer.flush() + + return id + + + def _send(self, event: Event) -> int: """Send an event to the Kafka source and block until an EventResult is recieved. :param event: The event to send. @@ -602,8 +667,6 @@ def set_ts(ts): self._futures[event._id]["sent_t"] = ts self.producer.produce(self.input_topic, value=pickle.dumps(event), on_delivery=lambda err, msg: set_ts(msg.timestamp())) - if flush: - self.producer.flush() return event._id def close(self): diff --git a/src/cascade/runtime/python_runtime.py b/src/cascade/runtime/python_runtime.py index cf936f3..a955e9c 100644 --- a/src/cascade/runtime/python_runtime.py +++ b/src/cascade/runtime/python_runtime.py @@ -1,7 +1,8 @@ from logging import Filter import threading +from typing import Type from cascade.dataflow.operator import StatefulOperator, StatelessOperator -from cascade.dataflow.dataflow import CollectNode, Event, EventResult, InitClass, InvokeMethod, OpNode, SelectAllNode +from cascade.dataflow.dataflow import CollectNode, Event, EventResult, InitClass, InvokeMethod, OpNode, SelectAllNode, StatelessOpNode from queue import Empty, Queue class PythonStatefulOperator(): @@ -11,17 +12,15 @@ def __init__(self, operator: StatefulOperator): def process(self, event: Event): assert(isinstance(event.target, OpNode)) - assert(isinstance(event.target.operator, StatefulOperator)) - assert(event.target.operator.entity == self.operator.entity) - key_stack = event.key_stack - key = key_stack[-1] + assert(event.target.entity == self.operator.entity) - print(f"PythonStatefulOperator: {event}") + key = event.variable_map[event.target.read_key_from] + + print(f"PythonStatefulOperator[{self.operator.entity.__name__}[{key}]]: {event}") if isinstance(event.target.method_type, InitClass): result = self.operator.handle_init_class(*event.variable_map.values()) self.states[key] = result - key_stack.pop() elif isinstance(event.target.method_type, InvokeMethod): state = self.states[key] @@ -29,7 +28,6 @@ def process(self, event: Event): event.target.method_type, variable_map=event.variable_map, state=state, - key_stack=key_stack ) self.states[key] = state @@ -39,7 +37,7 @@ def process(self, event: Event): if event.target.assign_result_to is not None: event.variable_map[event.target.assign_result_to] = result - new_events = event.propogate(key_stack, result) + new_events = event.propogate(result) if isinstance(new_events, EventResult): yield new_events else: @@ -50,17 +48,14 @@ def __init__(self, operator: StatelessOperator): self.operator = operator def process(self, event: Event): - assert(isinstance(event.target, OpNode)) - assert(isinstance(event.target.operator, StatelessOperator)) - - key_stack = event.key_stack + assert(isinstance(event.target, StatelessOpNode)) + print(f"PythonStatelessOperator[{self.operator.dataflow.name}]: {event}") if isinstance(event.target.method_type, InvokeMethod): result = self.operator.handle_invoke_method( event.target.method_type, variable_map=event.variable_map, - key_stack=key_stack ) else: raise Exception(f"A StatelessOperator cannot compute event type: {event.target.method_type}") @@ -68,7 +63,7 @@ def process(self, event: Event): if event.target.assign_result_to is not None: event.variable_map[event.target.assign_result_to] = result - new_events = event.propogate(key_stack, result) + new_events = event.propogate(result) if isinstance(new_events, EventResult): yield new_events else: @@ -81,8 +76,8 @@ def __init__(self): self.events = Queue() self.results = Queue() self.running = False - self.statefuloperators: dict[StatefulOperator, PythonStatefulOperator] = {} - self.statelessoperators: dict[StatelessOperator, PythonStatelessOperator] = {} + self.statefuloperators: dict[Type, PythonStatefulOperator] = {} + self.statelessoperators: dict[str, PythonStatelessOperator] = {} def init(self): pass @@ -91,10 +86,9 @@ def _consume_events(self): self.running = True def consume_event(event: Event): if isinstance(event.target, OpNode): - if isinstance(event.target.operator, StatefulOperator): - yield from self.statefuloperators[event.target.operator].process(event) - elif isinstance(event.target.operator, StatelessOperator): - yield from self.statelessoperators[event.target.operator].process(event) + yield from self.statefuloperators[event.target.entity].process(event) + elif isinstance(event.target, StatelessOpNode): + yield from self.statelessoperators[event.target.operator.dataflow.name].process(event) elif isinstance(event.target, SelectAllNode): raise NotImplementedError() @@ -121,11 +115,11 @@ def consume_event(event: Event): def add_operator(self, op: StatefulOperator): """Add a `StatefulOperator` to the datastream.""" - self.statefuloperators[op] = PythonStatefulOperator(op) + self.statefuloperators[op.entity] = PythonStatefulOperator(op) def add_stateless_operator(self, op: StatelessOperator): """Add a `StatelessOperator` to the datastream.""" - self.statelessoperators[op] = PythonStatelessOperator(op) + self.statelessoperators[op.dataflow.name] = PythonStatelessOperator(op) def send(self, event: Event, flush=None): self.events.put(event) diff --git a/test_programs/expected/checkout_item.py b/test_programs/expected/checkout_item.py index fd256bf..75a32fa 100644 --- a/test_programs/expected/checkout_item.py +++ b/test_programs/expected/checkout_item.py @@ -1,29 +1,27 @@ from typing import Any -# from ..target.checkout_item import User, Item -# from cascade.dataflow.dataflow import DataFlow, OpNode, InvokeMethod, Edge -def buy_item_0_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.append(variable_map['item_key']) +from cascade.dataflow.dataflow import DataFlow, Edge, InvokeMethod, OpNode +from test_programs.target.checkout_item import User, Item + +def buy_item_0_compiled(variable_map: dict[str, Any], state: User) -> Any: return None -def buy_item_1_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.pop() +def buy_item_1_compiled(variable_map: dict[str, Any], state: User) -> Any: item_price_0 = variable_map['item_price_0'] state.balance -= item_price_0 return state.balance >= 0 -def get_price_0_compiled(variable_map: dict[str, Any], state: Item, key_stack: list[str]) -> Any: - key_stack.pop() +def get_price_0_compiled(variable_map: dict[str, Any], state: Item) -> Any: return state.price def user_buy_item_df(): df = DataFlow("user.buy_item") - n0 = OpNode(User, InvokeMethod("buy_item_0")) - n1 = OpNode(Item, InvokeMethod("get_price"), assign_result_to="item_price") - n2 = OpNode(User, InvokeMethod("buy_item_1")) + n0 = OpNode(User, InvokeMethod("buy_item_0"), read_key_from="user_key") + n1 = OpNode(Item, InvokeMethod("get_price"), assign_result_to="item_price", read_key_from="item_key") + n2 = OpNode(User, InvokeMethod("buy_item_1"), read_key_from="user_key") df.add_edge(Edge(n0, n1)) df.add_edge(Edge(n1, n2)) df.entry = n0 diff --git a/test_programs/expected/checkout_two_items.py b/test_programs/expected/checkout_two_items.py index 2081cd3..9849ad5 100644 --- a/test_programs/expected/checkout_two_items.py +++ b/test_programs/expected/checkout_two_items.py @@ -1,30 +1,22 @@ from typing import Any - +from cascade.dataflow.dataflow import CollectNode, CollectTarget, DataFlow, OpNode, InvokeMethod, Edge from cascade.dataflow.operator import StatefulOperator -from ..target.checkout_two_items import User, Item -from cascade.dataflow.dataflow import DataFlow, OpNode, InvokeMethod, Edge, CollectNode, CollectTarget +from test_programs.target.checkout_two_items import User, Item -def buy_two_items_0_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.append( - [variable_map["item1_key"], variable_map["item2_key"]] - ) +def buy_two_items_0_compiled(variable_map: dict[str, Any], state: User) -> Any: return None -def buy_two_items_1_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.pop() - item_price_1_0 = variable_map['item_price_1'] - item_price_2_0 = variable_map['item_price_2'] +def buy_two_items_1_compiled(variable_map: dict[str, Any], state: User) -> Any: + item_price_1_0 = variable_map['item_price_1_0'] + item_price_2_0 = variable_map['item_price_2_0'] total_price_0 = item_price_1_0 + item_price_2_0 state.balance -= total_price_0 return state.balance >= 0 - -def get_price_0_compiled(variable_map: dict[str, Any], state: Item, key_stack: list[str]) -> Any: - key_stack.pop() +def get_price_0_compiled(variable_map: dict[str, Any], state: Item) -> Any: return state.price -# An operator is defined by the underlying class and the functions that can be called user_op = StatefulOperator( User, { @@ -39,18 +31,20 @@ def get_price_0_compiled(variable_map: dict[str, Any], state: Item, key_stack: l def user_buy_two_items_df(): df = DataFlow("user.buy_2_items") - n0 = OpNode(user_op, InvokeMethod("buy_2_items_0")) + n0 = OpNode(User, InvokeMethod("buy_2_items_0"), read_key_from="user_key") n1 = OpNode( - item_op, + Item, InvokeMethod("get_price"), assign_result_to="item_price_1", + read_key_from="item1_key" ) n2 = OpNode( - item_op, + Item, InvokeMethod("get_price"), assign_result_to="item_price_2", + read_key_from="item1_key" ) - n3 = OpNode(user_op, InvokeMethod("buy_2_items_1")) + n3 = OpNode(User, InvokeMethod("buy_2_items_1"), read_key_from="user_key") df.add_edge(Edge(n0, n1)) df.add_edge(Edge(n0, n2)) df.add_edge(Edge(n1, n2)) @@ -62,21 +56,23 @@ def user_buy_two_items_df(): # For future optimizations (not used) def user_buy_two_items_df_parallelized(): df = DataFlow("user.buy_2_items") - n0 = OpNode(user_op, InvokeMethod("buy_2_items_0")) + n0 = OpNode(User, InvokeMethod("buy_2_items_0"), read_key_from="user_key") n3 = CollectNode(assign_result_to="item_prices", read_results_from="item_price") n1 = OpNode( - item_op, + Item, InvokeMethod("get_price"), assign_result_to="item_price", - collect_target=CollectTarget(n3, 2, 0) + collect_target=CollectTarget(n3, 2, 0), + read_key_from="item1_key" ) n2 = OpNode( - item_op, + Item, InvokeMethod("get_price"), assign_result_to="item_price", - collect_target=CollectTarget(n3, 2, 1) + collect_target=CollectTarget(n3, 2, 1), + read_key_from="item1_key" ) - n4 = OpNode(user_op, InvokeMethod("buy_2_items_1")) + n4 = OpNode(User, InvokeMethod("buy_2_items_1"), read_key_from="user_key") df.add_edge(Edge(n0, n1)) df.add_edge(Edge(n0, n2)) df.add_edge(Edge(n1, n3)) diff --git a/test_programs/expected/deathstar_recommendation.py b/test_programs/expected/deathstar_recommendation.py index 436aa5d..8a8a727 100644 --- a/test_programs/expected/deathstar_recommendation.py +++ b/test_programs/expected/deathstar_recommendation.py @@ -1,53 +1,45 @@ from typing import Any, Literal -from cascade.dataflow.dataflow import CollectNode, DataFlow, Edge, InvokeMethod, OpNode, SelectAllNode +from cascade.dataflow.dataflow import CollectNode, DataFlow, Edge, InvokeMethod, OpNode, SelectAllNode, StatelessOpNode from cascade.dataflow.operator import StatelessOperator -def get_recs_if_cond(variable_map: dict[str, Any], key_stack: list[str]): +def get_recs_if_cond(variable_map: dict[str, Any]): return variable_map["requirement"] == "distance" # list comprehension entry -def get_recs_if_body_0(variable_map: dict[str, Any], key_stack: list[str]): - hotel_key = key_stack[-1] - # The body will need the hotel key (actually, couldn't we just take the top of the key stack again?) - variable_map["hotel_key"] = hotel_key - # The next node (Hotel.get_geo) will need the hotel key - key_stack.append(hotel_key) +def get_recs_if_body_0(variable_map: dict[str, Any]): + pass # list comprehension body -def get_recs_if_body_1(variable_map: dict[str, Any], key_stack: list[str]): - hotel_geo: Geo = variable_map["hotel_geo"] +def get_recs_if_body_1(variable_map: dict[str, Any]): + hotel_geo = variable_map["hotel_geo"] lat, lon = variable_map["lat"], variable_map["lon"] dist = hotel_geo.distance_km(lat, lon) return (dist, variable_map["hotel_key"]) # after list comprehension -def get_recs_if_body_2(variable_map: dict[str, Any], key_stack: list[str]): +def get_recs_if_body_2(variable_map: dict[str, Any]): distances = variable_map["distances"] min_dist = min(distances, key=lambda x: x[0])[0] variable_map["res"] = [hotel for dist, hotel in distances if dist == min_dist] -def get_recs_elif_cond(variable_map: dict[str, Any], key_stack: list[str]): +def get_recs_elif_cond(variable_map: dict[str, Any]): return variable_map["requirement"] == "price" # list comprehension entry -def get_recs_elif_body_0(variable_map: dict[str, Any], key_stack: list[str]): - hotel_key = key_stack[-1] - # The body will need the hotel key (actually, couldn't we just take the top of the key stack again?) - variable_map["hotel_key"] = hotel_key - # The next node (Hotel.get_geo) will need the hotel key - key_stack.append(hotel_key) +def get_recs_elif_body_0(variable_map: dict[str, Any]): + pass # list comprehension body -def get_recs_elif_body_1(variable_map: dict[str, Any], key_stack: list[str]): +def get_recs_elif_body_1(variable_map: dict[str, Any]): return (variable_map["hotel_price"], variable_map["hotel_key"]) # after list comprehension -def get_recs_elif_body_2(variable_map: dict[str, Any], key_stack: list[str]): +def get_recs_elif_body_2(variable_map: dict[str, Any]): prices = variable_map["prices"] min_price = min(prices, key=lambda x: x[0])[0] variable_map["res"] = [hotel for price, hotel in prices if price == min_price] @@ -56,7 +48,7 @@ def get_recs_elif_body_2(variable_map: dict[str, Any], key_stack: list[str]): # a future optimization might instead duplicate this piece of code over the two # branches, in order to reduce the number of splits by one -def get_recs_final(variable_map: dict[str, Any], key_stack: list[str]): +def get_recs_final(variable_map: dict[str, Any]): return variable_map["res"] @@ -74,24 +66,24 @@ def get_recs_final(variable_map: dict[str, Any], key_stack: list[str]): def get_recommendations_df(): df = DataFlow("get_recommendations") - n1 = OpNode(recommend_op, InvokeMethod("get_recs_if_cond"), is_conditional=True) - n2 = OpNode(recommend_op, InvokeMethod("get_recs_if_body_0")) - n3 = OpNode(hotel_op, InvokeMethod("get_geo"), assign_result_to="hotel_geo") - n4 = OpNode(recommend_op, InvokeMethod("get_recs_if_body_1"), assign_result_to="distance") + n1 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_if_cond"), is_conditional=True) + n2 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_if_body_0")) + n3 = OpNode(Hotel, InvokeMethod("get_geo"), assign_result_to="hotel_geo", read_key_from="hotel_key") + n4 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_if_body_1"), assign_result_to="distance") n5 = CollectNode("distances", "distance") - n6 = OpNode(recommend_op, InvokeMethod("get_recs_if_body_2")) - ns1 = SelectAllNode(Hotel, n5) + n6 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_if_body_2")) + ns1 = SelectAllNode(Hotel, n5, assign_key_to="hotel_key") - n7 = OpNode(recommend_op, InvokeMethod("get_recs_elif_cond"), is_conditional=True) - n8 = OpNode(recommend_op, InvokeMethod("get_recs_elif_body_0")) - n9 = OpNode(hotel_op, InvokeMethod("get_price"), assign_result_to="hotel_price") - n10 = OpNode(recommend_op, InvokeMethod("get_recs_elif_body_1"), assign_result_to="price") + n7 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_elif_cond"), is_conditional=True) + n8 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_elif_body_0")) + n9 = OpNode(Hotel, InvokeMethod("get_price"), assign_result_to="hotel_price", read_key_from="hotel_key") + n10 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_elif_body_1"), assign_result_to="price") n11 = CollectNode("prices", "price") - n12 = OpNode(recommend_op, InvokeMethod("get_recs_elif_body_2")) - ns2 = SelectAllNode(Hotel, n11) + n12 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_elif_body_2")) + ns2 = SelectAllNode(Hotel, n11, assign_key_to="hotel_key") - n13 = OpNode(recommend_op, InvokeMethod("get_recs_final")) + n13 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_final")) df.add_edge(Edge(n1, ns1, if_conditional=True)) df.add_edge(Edge(n1, n7, if_conditional=False)) diff --git a/test_programs/expected/deathstar_search.py b/test_programs/expected/deathstar_search.py index 06cbec0..cd20593 100644 --- a/test_programs/expected/deathstar_search.py +++ b/test_programs/expected/deathstar_search.py @@ -1,24 +1,15 @@ from typing import Any -from cascade.dataflow.dataflow import CollectNode, DataFlow, Edge, InvokeMethod, OpNode, SelectAllNode +from cascade.dataflow.dataflow import CollectNode, DataFlow, Edge, InvokeMethod, OpNode, SelectAllNode, StatelessOpNode from cascade.dataflow.operator import StatelessOperator - # predicate 1 -def search_nearby_compiled_0(variable_map: dict[str, Any], key_stack: list[str]): - # We assume that the top of the key stack is the hotel key. - # This assumption holds if the node before this one is a correctly - # configure SelectAllNode. - - hotel_key = key_stack[-1] - # The body will need the hotel key (actually, couldn't we just take the top of the key stack again?) - variable_map["hotel_key"] = hotel_key - # The next node (Hotel.get_geo) will need the hotel key - key_stack.append(hotel_key) +def search_nearby_compiled_0(variable_map: dict[str, Any]): + pass # predicate 2 -def search_nearby_compiled_1(variable_map: dict[str, Any], key_stack: list[str]): - hotel_geo = variable_map["hotel_geo"] +def search_nearby_compiled_1(variable_map: dict[str, Any]): + hotel_geo: Geo = variable_map["hotel_geo"] lat, lon = variable_map["lat"], variable_map["lon"] dist = hotel_geo.distance_km(lat, lon) variable_map["dist"] = dist @@ -26,11 +17,11 @@ def search_nearby_compiled_1(variable_map: dict[str, Any], key_stack: list[str]) # body -def search_nearby_compiled_2(variable_map: dict[str, Any], key_stack: list[str]): +def search_nearby_compiled_2(variable_map: dict[str, Any]): return (variable_map["dist"], variable_map["hotel_key"]) # next line -def search_nearby_compiled_3(variable_map: dict[str, Any], key_stack: list[str]): +def search_nearby_compiled_3(variable_map: dict[str, Any]): distances = variable_map["distances"] hotels = [hotel for dist, hotel in sorted(distances)[:5]] return hotels @@ -45,14 +36,14 @@ def search_nearby_compiled_3(variable_map: dict[str, Any], key_stack: list[str]) def search_nearby_df(): df = DataFlow("search_nearby") - n1 = OpNode(search_op, InvokeMethod("search_nearby_compiled_0")) - n2 = OpNode(hotel_op, InvokeMethod("get_geo"), assign_result_to="hotel_geo") - n3 = OpNode(search_op, InvokeMethod("search_nearby_compiled_1"), is_conditional=True) - n4 = OpNode(search_op, InvokeMethod("search_nearby_compiled_2"), assign_result_to="search_body") + n1 = StatelessOpNode(search_op, InvokeMethod("search_nearby_compiled_0")) + n2 = OpNode(Hotel, InvokeMethod("get_geo"), assign_result_to="hotel_geo", read_key_from="hotel_key") + n3 = StatelessOpNode(search_op, InvokeMethod("search_nearby_compiled_1"), is_conditional=True) + n4 = StatelessOpNode(search_op, InvokeMethod("search_nearby_compiled_2"), assign_result_to="search_body") n5 = CollectNode("distances", "search_body") - n0 = SelectAllNode(Hotel, n5) + n0 = SelectAllNode(Hotel, n5, assign_key_to="hotel_key") - n6 = OpNode(search_op, InvokeMethod("search_nearby_compiled_3")) + n6 = StatelessOpNode(search_op, InvokeMethod("search_nearby_compiled_3")) df.add_edge(Edge(n0, n1)) df.add_edge(Edge(n1, n2)) diff --git a/test_programs/expected/deathstar_user.py b/test_programs/expected/deathstar_user.py index 5aea434..64985ea 100644 --- a/test_programs/expected/deathstar_user.py +++ b/test_programs/expected/deathstar_user.py @@ -2,24 +2,21 @@ from cascade.dataflow.dataflow import DataFlow, Edge, InvokeMethod, OpNode from cascade.dataflow.operator import StatefulOperator -def order_compiled_entry_0(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.append(variable_map["hotel"]) +def order_compiled_entry_0(variable_map: dict[str, Any], state: User) -> Any: + pass -def order_compiled_entry_1(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.append(variable_map["flight"]) +def order_compiled_entry_1(variable_map: dict[str, Any], state: User) -> Any: + pass -def order_compiled_if_cond(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: +def order_compiled_if_cond(variable_map: dict[str, Any], state: User) -> Any: return variable_map["hotel_reserve"] and variable_map["flight_reserve"] -def order_compiled_if_body(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.pop() +def order_compiled_if_body(variable_map: dict[str, Any], state: User) -> Any: return True -def order_compiled_else_body(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.pop() +def order_compiled_else_body(variable_map: dict[str, Any], state: User) -> Any: return False - user_op = StatefulOperator( User, { @@ -29,7 +26,7 @@ def order_compiled_else_body(variable_map: dict[str, Any], state: User, key_stac "order_compiled_if_body": order_compiled_if_body, "order_compiled_else_body": order_compiled_else_body }, - {} # dataflows (filled later) + {} ) # For now, the dataflow will be serial instead of parallel (calling hotel, then @@ -39,13 +36,13 @@ def order_compiled_else_body(variable_map: dict[str, Any], state: User, key_stac # before the first entity call). def user_order_df(): df = DataFlow("user_order") - n0 = OpNode(user_op, InvokeMethod("order_compiled_entry_0")) - n1 = OpNode(hotel_op, InvokeMethod("reserve"), assign_result_to="hotel_reserve") - n2 = OpNode(user_op, InvokeMethod("order_compiled_entry_1")) - n3 = OpNode(flight_op, InvokeMethod("reserve"), assign_result_to="flight_reserve") - n4 = OpNode(user_op, InvokeMethod("order_compiled_if_cond"), is_conditional=True) - n5 = OpNode(user_op, InvokeMethod("order_compiled_if_body")) - n6 = OpNode(user_op, InvokeMethod("order_compiled_else_body")) + n0 = OpNode(User, InvokeMethod("order_compiled_entry_0"), read_key_from="user_key") + n1 = OpNode(Hotel, InvokeMethod("reserve"), assign_result_to="hotel_reserve", read_key_from="hotel_key") + n2 = OpNode(User, InvokeMethod("order_compiled_entry_1"), read_key_from="user_key") + n3 = OpNode(Flight, InvokeMethod("reserve"), assign_result_to="flight_reserve", read_key_from="flight_key") + n4 = OpNode(User, InvokeMethod("order_compiled_if_cond"), is_conditional=True, read_key_from="user_key") + n5 = OpNode(User, InvokeMethod("order_compiled_if_body"), read_key_from="user_key") + n6 = OpNode(User, InvokeMethod("order_compiled_else_body"), read_key_from="user_key") df.add_edge(Edge(n0, n1)) df.add_edge(Edge(n1, n2)) diff --git a/tests/integration/flink-runtime/common.py b/tests/integration/flink-runtime/common.py index 49a0ef3..a7d7af6 100644 --- a/tests/integration/flink-runtime/common.py +++ b/tests/integration/flink-runtime/common.py @@ -39,38 +39,28 @@ def get_price(self) -> int: def __repr__(self): return f"Item(key='{self.key}', price={self.price})" -def update_balance_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.pop() # final function +def update_balance_compiled(variable_map: dict[str, Any], state: User) -> Any: state.balance += variable_map["amount"] return state.balance >= 0 -def get_balance_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.pop() # final function +def get_balance_compiled(variable_map: dict[str, Any], state: User) -> Any: return state.balance -def get_price_compiled(variable_map: dict[str, Any], state: Item, key_stack: list[str]) -> Any: - key_stack.pop() # final function +def get_price_compiled(variable_map: dict[str, Any], state: Item) -> Any: return state.price -# Items (or other operators) are passed by key always -def buy_item_0_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.append(variable_map["item_key"]) +def buy_item_0_compiled(variable_map: dict[str, Any], state: User) -> Any: return None -def buy_item_1_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.pop() +def buy_item_1_compiled(variable_map: dict[str, Any], state: User) -> Any: state.balance = state.balance - variable_map["item_price"] return state.balance >= 0 -def buy_2_items_0_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.append( - [variable_map["item1_key"], variable_map["item2_key"]] - ) +def buy_2_items_0_compiled(variable_map: dict[str, Any], state: User) -> Any: return None -def buy_2_items_1_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.pop() +def buy_2_items_1_compiled(variable_map: dict[str, Any], state: User) -> Any: state.balance -= variable_map["item_prices"][0] + variable_map["item_prices"][1] return state.balance >= 0 @@ -94,9 +84,12 @@ def buy_2_items_1_compiled(variable_map: dict[str, Any], state: User, key_stack: def user_buy_item_df(): df = DataFlow("user.buy_item") - n0 = OpNode(user_op, InvokeMethod("buy_item_0")) - n1 = OpNode(item_op, InvokeMethod("get_price"), assign_result_to="item_price") - n2 = OpNode(user_op, InvokeMethod("buy_item_1")) + n0 = OpNode(User, InvokeMethod("buy_item_0"), read_key_from="user_key") + n1 = OpNode(Item, + InvokeMethod("get_price"), + assign_result_to="item_price", + read_key_from="item_key") + n2 = OpNode(User, InvokeMethod("buy_item_1"), read_key_from="user_key") df.add_edge(Edge(n0, n1)) df.add_edge(Edge(n1, n2)) df.entry = n0 @@ -104,21 +97,23 @@ def user_buy_item_df(): def user_buy_2_items_df(): df = DataFlow("user.buy_2_items") - n0 = OpNode(user_op, InvokeMethod("buy_2_items_0")) + n0 = OpNode(User, InvokeMethod("buy_2_items_0"), read_key_from="user_key") n3 = CollectNode(assign_result_to="item_prices", read_results_from="item_price") n1 = OpNode( - item_op, + Item, InvokeMethod("get_price"), assign_result_to="item_price", - collect_target=CollectTarget(n3, 2, 0) + collect_target=CollectTarget(n3, 2, 0), + read_key_from="item1_key" ) n2 = OpNode( - item_op, + Item, InvokeMethod("get_price"), assign_result_to="item_price", - collect_target=CollectTarget(n3, 2, 1) + collect_target=CollectTarget(n3, 2, 1), + read_key_from="item2_key" ) - n4 = OpNode(user_op, InvokeMethod("buy_2_items_1")) + n4 = OpNode(User, InvokeMethod("buy_2_items_1"), read_key_from="user_key") df.add_edge(Edge(n0, n1)) df.add_edge(Edge(n0, n2)) df.add_edge(Edge(n1, n3)) diff --git a/tests/integration/flink-runtime/test_merge_operator.py b/tests/integration/flink-runtime/test_collect_operator.py similarity index 68% rename from tests/integration/flink-runtime/test_merge_operator.py rename to tests/integration/flink-runtime/test_collect_operator.py index d136d99..d14418f 100644 --- a/tests/integration/flink-runtime/test_merge_operator.py +++ b/tests/integration/flink-runtime/test_collect_operator.py @@ -1,71 +1,71 @@ -"""A test script for dataflows with merge operators""" - -from pyflink.datastream.data_stream import CloseableIterator -from common import Item, User, item_op, user_op -from cascade.dataflow.dataflow import Event, EventResult, InitClass, InvokeMethod, OpNode -from cascade.runtime.flink_runtime import FlinkOperator, FlinkRuntime -import pytest - -@pytest.mark.integration -def test_merge_operator(): - runtime = FlinkRuntime("test_merge_operator") - runtime.init() - runtime.add_operator(FlinkOperator(item_op)) - runtime.add_operator(FlinkOperator(user_op)) - - - # Create a User object - foo_user = User("foo", 100) - init_user_node = OpNode(user_op, InitClass()) - event = Event(init_user_node, ["foo"], {"key": "foo", "balance": 100}, None) - runtime.send(event) - - # Create an Item object - fork_item = Item("fork", 5) - init_item_node = OpNode(item_op, InitClass()) - event = Event(init_item_node, ["fork"], {"key": "fork", "price": 5}, None) - runtime.send(event) - - # Create another Item - spoon_item = Item("spoon", 3) - event = Event(init_item_node, ["spoon"], {"key": "spoon", "price": 3}, None) - runtime.send(event, flush=True) - - collected_iterator: CloseableIterator = runtime.run(run_async=True, collect=True) - records = [] - - def wait_for_event_id(id: int) -> EventResult: - for record in collected_iterator: - records.append(record) - print(f"Collected record: {record}") - if record.event_id == id: - return record - - # Make sure the user & items are initialised - wait_for_event_id(event._id) - - # Have the User object buy the item - foo_user.buy_2_items(fork_item, spoon_item) - df = user_op.dataflows["buy_2_items"] - - # User with key "foo" buys item with key "fork" - user_buys_cutlery = Event(df.entry, ["foo"], {"item1_key": "fork", "item2_key": "spoon"}, df) - runtime.send(user_buys_cutlery, flush=True) - - - # Check that we were able to buy the fork - buy_fork_result = wait_for_event_id(user_buys_cutlery._id) - assert buy_fork_result.result == True - - # Send an event to check if the balance was updated - user_get_balance_node = OpNode(user_op, InvokeMethod("get_balance")) - user_get_balance = Event(user_get_balance_node, ["foo"], {}, None) - runtime.send(user_get_balance, flush=True) - - # See that the user's balance has gone down - get_balance = wait_for_event_id(user_get_balance._id) - assert get_balance.result == 92 - - collected_iterator.close() - +"""A test script for dataflows with merge operators""" + +from pyflink.datastream.data_stream import CloseableIterator +from common import Item, User, item_op, user_op +from cascade.dataflow.dataflow import Event, EventResult, InitClass, InvokeMethod, OpNode +from cascade.runtime.flink_runtime import FlinkOperator, FlinkRuntime +import pytest + +@pytest.mark.integration +def test_merge_operator(): + runtime = FlinkRuntime("test_collect_operator") + runtime.init() + runtime.add_operator(item_op) + runtime.add_operator(user_op) + + + # Create a User object + foo_user = User("foo", 100) + init_user_node = OpNode(User, InitClass(), read_key_from="key") + event = Event(init_user_node, {"key": "foo", "balance": 100}, None) + runtime.send(event) + + # Create an Item object + fork_item = Item("fork", 5) + init_item_node = OpNode(Item, InitClass(), read_key_from="key") + event = Event(init_item_node, {"key": "fork", "price": 5}, None) + runtime.send(event) + + # Create another Item + spoon_item = Item("spoon", 3) + event = Event(init_item_node, {"key": "spoon", "price": 3}, None) + runtime.send(event, flush=True) + + collected_iterator: CloseableIterator = runtime.run(run_async=True, output="collect") + records = [] + + def wait_for_event_id(id: int) -> EventResult: + for record in collected_iterator: + records.append(record) + print(f"Collected record: {record}") + if record.event_id == id: + return record + + # Make sure the user & items are initialised + wait_for_event_id(event._id) + + # Have the User object buy the item + foo_user.buy_2_items(fork_item, spoon_item) + df = user_op.dataflows["buy_2_items"] + + # User with key "foo" buys item with key "fork" + user_buys_cutlery = Event(df.entry, {"user_key": "foo", "item1_key": "fork", "item2_key": "spoon"}, df) + runtime.send(user_buys_cutlery, flush=True) + + + # Check that we were able to buy the fork + buy_fork_result = wait_for_event_id(user_buys_cutlery._id) + assert buy_fork_result.result == True + + # Send an event to check if the balance was updated + user_get_balance_node = OpNode(User, InvokeMethod("get_balance"), read_key_from="key") + user_get_balance = Event(user_get_balance_node, {"key": "foo"}, None) + runtime.send(user_get_balance, flush=True) + + # See that the user's balance has gone down + get_balance = wait_for_event_id(user_get_balance._id) + assert get_balance.result == 92 + + collected_iterator.close() + print(records) \ No newline at end of file diff --git a/tests/integration/flink-runtime/test_select_all.py b/tests/integration/flink-runtime/test_select_all.py index f585092..602858d 100644 --- a/tests/integration/flink-runtime/test_select_all.py +++ b/tests/integration/flink-runtime/test_select_all.py @@ -1,5 +1,5 @@ """ -Basically we need a way to search through all state. +The select all operator is used to fetch all keys for a single entity """ import math import random @@ -8,10 +8,9 @@ from pyflink.datastream.data_stream import CloseableIterator -from cascade.dataflow.dataflow import CollectNode, DataFlow, Edge, Event, EventResult, InitClass, InvokeMethod, OpNode, SelectAllNode +from cascade.dataflow.dataflow import CollectNode, DataFlow, Edge, Event, EventResult, InitClass, InvokeMethod, OpNode, SelectAllNode, StatelessOpNode from cascade.dataflow.operator import StatefulOperator, StatelessOperator from cascade.runtime.flink_runtime import FlinkOperator, FlinkRuntime, FlinkStatelessOperator -from confluent_kafka import Producer import time import pytest @@ -35,13 +34,11 @@ def __repr__(self) -> str: return f"Hotel({self.name}, {self.loc})" -def distance_compiled(variable_map: dict[str, Any], state: Hotel, key_stack: list[str]) -> Any: - key_stack.pop() +def distance_compiled(variable_map: dict[str, Any], state: Hotel) -> Any: loc = variable_map["loc"] return math.sqrt((state.loc.x - loc.x) ** 2 + (state.loc.y - loc.y) ** 2) -def get_name_compiled(variable_map: dict[str, Any], state: Hotel, key_stack: list[str]) -> Any: - key_stack.pop() +def get_name_compiled(variable_map: dict[str, Any], state: Hotel) -> Any: return state.name hotel_op = StatefulOperator(Hotel, @@ -55,24 +52,19 @@ def get_nearby(hotels: list[Hotel], loc: Geo, dist: float): # We compile just the predicate, the select is implemented using a selectall node -def get_nearby_predicate_compiled_0(variable_map: dict[str, Any], key_stack: list[str]): - # the top of the key_stack is already the right key, so in this case we don't need to do anything - # loc = variable_map["loc"] - # we need the hotel_key for later. (body_compiled_0) - variable_map["hotel_key"] = key_stack[-1] +def get_nearby_predicate_compiled_0(variable_map: dict[str, Any]): pass -def get_nearby_predicate_compiled_1(variable_map: dict[str, Any], key_stack: list[str]) -> bool: +def get_nearby_predicate_compiled_1(variable_map: dict[str, Any]) -> bool: loc = variable_map["loc"] dist = variable_map["dist"] hotel_dist = variable_map["hotel_distance"] - # key_stack.pop() # shouldn't pop because this function is stateless return hotel_dist < dist -def get_nearby_body_compiled_0(variable_map: dict[str, Any], key_stack: list[str]): - key_stack.append(variable_map["hotel_key"]) +def get_nearby_body_compiled_0(variable_map: dict[str, Any]): + pass -def get_nearby_body_compiled_1(variable_map: dict[str, Any], key_stack: list[str]) -> str: +def get_nearby_body_compiled_1(variable_map: dict[str, Any]) -> str: return variable_map["hotel_name"] get_nearby_op = StatelessOperator({ @@ -85,13 +77,13 @@ def get_nearby_body_compiled_1(variable_map: dict[str, Any], key_stack: list[str # dataflow for getting all hotels within region df = DataFlow("get_nearby") n7 = CollectNode("get_nearby_result", "get_nearby_body") -n0 = SelectAllNode(Hotel, n7) -n1 = OpNode(get_nearby_op, InvokeMethod("get_nearby_predicate_compiled_0")) -n2 = OpNode(hotel_op, InvokeMethod("distance"), assign_result_to="hotel_distance") -n3 = OpNode(get_nearby_op, InvokeMethod("get_nearby_predicate_compiled_1"), is_conditional=True) -n4 = OpNode(get_nearby_op, InvokeMethod("get_nearby_body_compiled_0")) -n5 = OpNode(hotel_op, InvokeMethod("get_name"), assign_result_to="hotel_name") -n6 = OpNode(get_nearby_op, InvokeMethod("get_nearby_body_compiled_1"), assign_result_to="get_nearby_body") +n0 = SelectAllNode(Hotel, n7, assign_key_to="hotel_key") +n1 = StatelessOpNode(get_nearby_op, InvokeMethod("get_nearby_predicate_compiled_0")) +n2 = OpNode(Hotel, InvokeMethod("distance"), assign_result_to="hotel_distance", read_key_from="hotel_key") +n3 = StatelessOpNode(get_nearby_op, InvokeMethod("get_nearby_predicate_compiled_1"), is_conditional=True) +n4 = StatelessOpNode(get_nearby_op, InvokeMethod("get_nearby_body_compiled_0")) +n5 = OpNode(Hotel, InvokeMethod("get_name"), assign_result_to="hotel_name", read_key_from="hotel_key") +n6 = StatelessOpNode(get_nearby_op, InvokeMethod("get_nearby_body_compiled_1"), assign_result_to="get_nearby_body") df.add_edge(Edge(n0, n1)) df.add_edge(Edge(n1, n2)) @@ -107,22 +99,22 @@ def get_nearby_body_compiled_1(variable_map: dict[str, Any], key_stack: list[str def test_nearby_hotels(): runtime = FlinkRuntime("test_nearby_hotels") runtime.init() - runtime.add_operator(FlinkOperator(hotel_op)) - runtime.add_stateless_operator(FlinkStatelessOperator(get_nearby_op)) + runtime.add_operator(hotel_op) + runtime.add_stateless_operator(get_nearby_op) # Create Hotels hotels = [] - init_hotel = OpNode(hotel_op, InitClass()) + init_hotel = OpNode(Hotel, InitClass(), read_key_from="name") random.seed(42) for i in range(20): coord_x = random.randint(-10, 10) coord_y = random.randint(-10, 10) hotel = Hotel(f"h_{i}", Geo(coord_x, coord_y)) - event = Event(init_hotel, [hotel.name], {"name": hotel.name, "loc": hotel.loc}, None) + event = Event(init_hotel, {"name": hotel.name, "loc": hotel.loc}, None) runtime.send(event) hotels.append(hotel) - collected_iterator: CloseableIterator = runtime.run(run_async=True, collect=True) + collected_iterator: CloseableIterator = runtime.run(run_async=True, output='collect') records = [] def wait_for_event_id(id: int) -> EventResult: for record in collected_iterator: @@ -145,12 +137,11 @@ def wait_for_n_records(num: int) -> list[EventResult]: print("creating hotels") # Wait for hotels to be created wait_for_n_records(20) - time.sleep(3) # wait for all hotels to be registered + time.sleep(10) # wait for all hotels to be registered dist = 5 loc = Geo(0, 0) - # because of how the key stack works, we need to supply a key here - event = Event(n0, ["workaround_key"], {"loc": loc, "dist": dist}, df) + event = Event(n0, {"loc": loc, "dist": dist}, df) runtime.send(event, flush=True) nearby = [] diff --git a/tests/integration/flink-runtime/test_two_entities.py b/tests/integration/flink-runtime/test_two_entities.py index 9d2e0cf..3d89bd2 100644 --- a/tests/integration/flink-runtime/test_two_entities.py +++ b/tests/integration/flink-runtime/test_two_entities.py @@ -10,24 +10,24 @@ def test_two_entities(): runtime = FlinkRuntime("test_two_entities") runtime.init() - runtime.add_operator(FlinkOperator(item_op)) - runtime.add_operator(FlinkOperator(user_op)) + runtime.add_operator(item_op) + runtime.add_operator(user_op) # Create a User object foo_user = User("foo", 100) - init_user_node = OpNode(user_op, InitClass()) - event = Event(init_user_node, ["foo"], {"key": "foo", "balance": 100}, None) + init_user_node = OpNode(User, InitClass(), read_key_from="key") + event = Event(init_user_node, {"key": "foo", "balance": 100}, None) runtime.send(event) # Create an Item object fork_item = Item("fork", 5) - init_item_node = OpNode(item_op, InitClass()) - event = Event(init_item_node, ["fork"], {"key": "fork", "price": 5}, None) + init_item_node = OpNode(Item, InitClass(), read_key_from="key") + event = Event(init_item_node, {"key": "fork", "price": 5}, None) runtime.send(event) # Create an expensive Item house_item = Item("house", 1000) - event = Event(init_item_node, ["house"], {"key": "house", "price": 1000}, None) + event = Event(init_item_node, {"key": "house", "price": 1000}, None) runtime.send(event) # Have the User object buy the item @@ -35,10 +35,10 @@ def test_two_entities(): df = user_op.dataflows["buy_item"] # User with key "foo" buys item with key "fork" - user_buys_fork = Event(df.entry, ["foo"], {"item_key": "fork"}, df) + user_buys_fork = Event(df.entry, {"user_key": "foo", "item_key": "fork"}, df) runtime.send(user_buys_fork, flush=True) - collected_iterator: CloseableIterator = runtime.run(run_async=True, collect=True) + collected_iterator: CloseableIterator = runtime.run(run_async=True, output="collect") records = [] def wait_for_event_id(id: int) -> EventResult: @@ -53,8 +53,8 @@ def wait_for_event_id(id: int) -> EventResult: assert buy_fork_result.result == True # Send an event to check if the balance was updated - user_get_balance_node = OpNode(user_op, InvokeMethod("get_balance")) - user_get_balance = Event(user_get_balance_node, ["foo"], {}, None) + user_get_balance_node = OpNode(User, InvokeMethod("get_balance"), read_key_from="key") + user_get_balance = Event(user_get_balance_node, {"key": "foo"}, None) runtime.send(user_get_balance, flush=True) # See that the user's balance has gone down @@ -63,7 +63,7 @@ def wait_for_event_id(id: int) -> EventResult: # User with key "foo" buys item with key "house" foo_user.buy_item(house_item) - user_buys_house = Event(df.entry, ["foo"], {"item_key": "house"}, df) + user_buys_house = Event(df.entry, {"user_key": "foo", "item_key": "house"}, df) runtime.send(user_buys_house, flush=True) # Balance becomes negative when house is bought