11import datetime
22import json
3+ from concurrent .futures import ThreadPoolExecutor , as_completed
34
45import requests
56from django .conf import settings
67from django .utils import timezone
78from django .utils .timezone import make_aware
8- from requests .exceptions import ConnectTimeout , ReadTimeout
9+ from requests .exceptions import ConnectionError , ConnectTimeout , ReadTimeout
910
1011from dining .models import DiningItem , DiningMenu , DiningStation , Venue
1112from utils .errors import APIError
@@ -59,7 +60,7 @@ def get_venues(self):
5960 venues_route = OPEN_DATA_ENDPOINTS ["VENUES" ]
6061 response = self .request ("GET" , venues_route )
6162 if response .status_code != 200 :
62- raise APIError ("Dining: Error connecting to API" )
63+ raise APIError ("Dining: error connecting to API " + response . text )
6364 venues = response .json ()["result_data" ]["campuses" ]["203" ]["cafes" ]
6465 for key , value in venues .items ():
6566 # Cleaning up json response
@@ -107,21 +108,47 @@ def get_venues(self):
107108 results .append (value )
108109 return results
109110
111+ def fetch_menu (self , venue_id , date ):
112+ """
113+ Calls API to fetch menu for a given venue and date
114+ """
115+ worker = DiningAPIWrapper () # avoid shared mutable token state across threads
116+ menu_base = OPEN_DATA_ENDPOINTS ["MENUS" ]
117+ response = worker .request ("GET" , f"{ menu_base } ?cafe={ venue_id } &date={ date } " )
118+ if response .status_code != 200 :
119+ raise APIError ("Dining: error connecting to API " + response .text )
120+ return (
121+ venue_id ,
122+ response .json (),
123+ ) # also storing venue_id to later access in fetched_menus list
124+
110125 def load_menu (self , date = timezone .now ().date ()):
111126 """
112- Loads the weeks menu starting from today
127+ Loads today's menu
113128 NOTE: This method should only be used in load_next_menu.py, which is
114129 run based on a cron job every day
115130 """
116-
117131 # Venues without a menu should not be parsed
118132 skipped_venues = [747 , 1163 , 1731 , 1732 , 1733 , 1464004 , 1464009 ]
119133
120134 # TODO: Handle API responses during empty menus (holidays)
121- menu_base = OPEN_DATA_ENDPOINTS ["MENUS" ]
122135 venues = [v for v in Venue .objects .all () if v .venue_id not in skipped_venues ]
123- for venue in venues :
124- response = self .request ("GET" , f"{ menu_base } ?cafe={ venue .venue_id } &date={ date } " ).json ()
136+ venue_map = {venue .venue_id : venue for venue in venues }
137+
138+ # Fetch all menus in parallel to speed up loading time.
139+ fetched_menus = []
140+ with ThreadPoolExecutor (max_workers = 8 ) as executor : # 8 can be tuned
141+ futures = [executor .submit (self .fetch_menu , venue .venue_id , date ) for venue in venues ]
142+ for future in as_completed (futures ):
143+ try :
144+ venue_id , response_json = future .result ()
145+ fetched_menus .append ((venue_id , response_json ))
146+ except Exception as e :
147+ print (f"Error fetching menu: { e } " )
148+
149+ # Process the fetched menus and load them into the database
150+ for venue_id , response in fetched_menus :
151+ venue = venue_map [venue_id ]
125152 # Load new items into database
126153 # TODO: There is something called a "goitem" for venues like English House.
127154 # We are currently not loading them in
@@ -144,6 +171,7 @@ def load_menu(self, date=timezone.now().date()):
144171 end_time = daypart ["endtime" ],
145172 service = daypart ["label" ],
146173 )
174+
147175 # Append stations to dining menu
148176 self .load_stations (daypart ["stations" ], dining_menu )
149177
0 commit comments