11from dataclasses import asdict
22import json
33import os
4+ import time
5+ import threading
46import requests
57from firebase_functions import https_fn
68from firebase_functions .params import SecretParam , StringParam
9294)
9395
9496
97+ # ---------------------------------------------------------------------------
98+ # In-memory spreadsheet cache – avoids hitting Google Sheets on every login.
99+ # Cloud Functions reuse warm instances, so the cache survives across
100+ # invocations on the same instance. A TTL (default 5 min) keeps it fresh.
101+ # ---------------------------------------------------------------------------
102+
103+ SHEET_CACHE_TTL_SECONDS = 300 # 5 minutes
104+
105+ _sheet_cache_lock = threading .Lock ()
106+ _sheet_cache : dict | None = (
107+ None # {"ts": float, "reg_headers": [...], "reg_rows": [...], "checkin_headers": [...], "checkin_rows": [...]}
108+ )
109+
110+
111+ def _fetch_sheets_data () -> dict :
112+ """Fetch both worksheets from Google Sheets and return them as a dict."""
113+ import gspread
114+
115+ creds , _ = google .auth .default (
116+ scopes = [
117+ "https://www.googleapis.com/auth/spreadsheets.readonly" ,
118+ "https://www.googleapis.com/auth/drive" ,
119+ ]
120+ )
121+ gc = gspread .authorize (creds )
122+ spreadsheet = gc .open_by_url (SPREADSHEET_URL .value )
123+
124+ reg_sheet = spreadsheet .worksheet ("Registration Submissions" )
125+ checkin_sheet = spreadsheet .worksheet ("Check-in" )
126+
127+ # Batch-fetch all values in two calls (instead of per-row later)
128+ reg_all = reg_sheet .get_all_values ()
129+ checkin_all = checkin_sheet .get_all_values ()
130+
131+ return {
132+ "ts" : time .monotonic (),
133+ "reg_headers" : reg_all [0 ] if reg_all else [],
134+ "reg_rows" : reg_all [1 :] if len (reg_all ) > 1 else [],
135+ "checkin_headers" : checkin_all [0 ] if checkin_all else [],
136+ "checkin_rows" : checkin_all [1 :] if len (checkin_all ) > 1 else [],
137+ }
138+
139+
140+ def _get_cached_sheets () -> dict :
141+ """Return cached sheet data, refreshing if stale or missing."""
142+ global _sheet_cache
143+ with _sheet_cache_lock :
144+ now = time .monotonic ()
145+ if _sheet_cache is None or (now - _sheet_cache ["ts" ]) > SHEET_CACHE_TTL_SECONDS :
146+ print ("[sheet-cache] cache miss – fetching from Google Sheets" )
147+ _sheet_cache = _fetch_sheets_data ()
148+ else :
149+ age = round (now - _sheet_cache ["ts" ], 1 )
150+ print (f"[sheet-cache] cache hit (age { age } s)" )
151+ return _sheet_cache
152+
153+
95154def _get_col_index_from_headers (headers : list [str ], name : str ) -> int :
96155 """Return 1-based column index for a given header name.
97156
@@ -116,49 +175,40 @@ def _get_registration(uid: str, username: str) -> User:
116175 Organizers will not need to be present on the spreadsheet
117176 """
118177
119- import gspread
120-
121178 is_organizer = uid in [
122179 o .strip () for o in ORGANIZERS_LIST .value .split ("," ) if o .strip ()
123180 ]
124181
125182 if is_organizer :
126183 return User (id = uid , role = "organizer" , username = username )
127184 else :
128- creds , _ = google .auth .default (
129- scopes = [
130- "https://www.googleapis.com/auth/spreadsheets.readonly" ,
131- "https://www.googleapis.com/auth/drive" ,
132- ]
133- )
134- gc = gspread .authorize (creds )
135-
136- spreadsheet = gc .open_by_url (
137- SPREADSHEET_URL .value ,
138- )
139-
140- print (spreadsheet .worksheets ())
141-
142- reg_sheet = spreadsheet .worksheet ("Registration Submissions" )
143- checkin_sheet = spreadsheet .worksheet ("Check-in" )
144-
145- reg_headers = reg_sheet .row_values (1 )
146- checkin_headers = checkin_sheet .row_values (1 )
185+ sheets = _get_cached_sheets ()
186+ reg_headers = sheets ["reg_headers" ]
187+ checkin_headers = sheets ["checkin_headers" ]
147188
148189 username_col_idx = _get_col_index_from_headers (
149190 reg_headers , USERNAME_COL_R .value
150191 )
151192
152- cell = reg_sheet .find (
153- username , in_column = username_col_idx , case_sensitive = False
154- )
193+ # Search for the user in cached registration rows
194+ username_lower = username .lower ()
195+ matched_row_idx : int | None = None
196+ for i , row in enumerate (sheets ["reg_rows" ]):
197+ cell_val = _get_cell_from_row (row , username_col_idx )
198+ if cell_val .strip ().lower () == username_lower :
199+ matched_row_idx = i
200+ break
155201
156202 # participant not registered
157- if not cell :
203+ if matched_row_idx is None :
158204 raise ValueError ("participant_not_found" )
159205
160- reg_row = reg_sheet .row_values (cell .row )
161- checkin_row = checkin_sheet .row_values (cell .row )
206+ reg_row = sheets ["reg_rows" ][matched_row_idx ]
207+ checkin_row = (
208+ sheets ["checkin_rows" ][matched_row_idx ]
209+ if matched_row_idx < len (sheets ["checkin_rows" ])
210+ else []
211+ )
162212
163213 checked_in_idx = _get_col_index_from_headers (
164214 checkin_headers , CHECKED_IN_COL_C .value
@@ -259,7 +309,7 @@ def _generate_login_token(uid: str, username: str) -> str:
259309 user = _get_registration (uid , username )
260310
261311 # If login is disabled for participants, reject them
262- if user .role != "organizer" and os .getenv ("FUNCTIONS_EMULATOR" ) == "false " :
312+ if user .role != "organizer" and os .getenv ("FUNCTIONS_EMULATOR" ) != "true " :
263313 db = firestore .client ()
264314 event_doc = db .document ("event/main" ).get ()
265315 if event_doc .exists :
0 commit comments