Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .env_template
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ YOUTUBE_API_KEY=
MONGO_URI=
MONGO_DB=
STAGE=
DAILY_SUN_URL=
DAILY_SUN_URL=
GOOGLE_APPLICATION_CREDENTIALS=
FIREBASE_CREDENTIALS_HOST_PATH=./firebase-service-account-key.json
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ __pycache__/
.env
.envrc
.DS_Store
ca-certificate.crt
ca-certificate.crt
firebase-service-account-key.json
31 changes: 21 additions & 10 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,27 @@
from src.utils.team_loader import TeamLoader
from src.database import db

import os
import firebase_admin
from firebase_admin import credentials

SERVICE_ACCOUNT_PATH = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS")


def initialize_firebase():
if not firebase_admin._apps:
if not SERVICE_ACCOUNT_PATH:
raise ValueError(
"GOOGLE_APPLICATION_CREDENTIALS is not set. Set it to your firebase-service-account-key.json path."
)
cred = credentials.Certificate(SERVICE_ACCOUNT_PATH)
firebase_admin.initialize_app(cred)
logging.info("Firebase app initialized.")
return firebase_admin.get_app()


initialize_firebase()
Comment on lines +30 to +45
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

firebase-admin Python 7.3.0 recommended way to check if default app is initialized

💡 Result:

The recommended way to check if the default Firebase app is initialized in firebase-admin Python SDK version 7.3.0 is to attempt to retrieve it using firebase_admin.get_app and catch the ValueError exception if it does not exist. This is the idiomatic approach since the official documentation states that get_app raises ValueError if the specified app (default is '[DEFAULT]') does not exist, while initialize_app raises ValueError if it already exists. Example code: import firebase_admin from firebase_admin import credentials try: app = firebase_admin.get_app # Checks if default app exists except ValueError: cred = credentials.Certificate('path/to/serviceAccountKey.json') app = firebase_admin.initialize_app(cred) Alternative (not officially recommended, uses internal state): if not firebase_admin._apps: firebase_admin.initialize_app(cred) This checks the internal _apps dict, but _apps is an implementation detail (protected member). No public is_app_initialized function exists in v7.3.0; a PR for it remains open. Official docs emphasize initializing once and using get_app thereafter.

Citations:


🏁 Script executed:

# First, find app.py and check its content around lines 30-45
find . -name "app.py" -type f | head -5

Repository: cuappdev/score-backend

Length of output: 74


🏁 Script executed:

# Also check for docker-compose.yml
find . -name "docker-compose.yml" -type f

Repository: cuappdev/score-backend

Length of output: 86


🏁 Script executed:

# Look for test files that might import app
find . -path "*/test*" -name "*.py" -type f | head -10

Repository: cuappdev/score-backend

Length of output: 48


🏁 Script executed:

# Check app.py size and read it
wc -l ./app.py && echo "---" && cat -n ./app.py

Repository: cuappdev/score-backend

Length of output: 8210


🏁 Script executed:

# Check docker-compose.yml
cat -n ./docker-compose.yml

Repository: cuappdev/score-backend

Length of output: 843


🏁 Script executed:

# Look for test files more broadly
find . -name "*test*.py" -type f 2>/dev/null

Repository: cuappdev/score-backend

Length of output: 48


🏁 Script executed:

# Search for imports of app module to understand blast radius
rg "from app import|import app" --type py

Repository: cuappdev/score-backend

Length of output: 48


🏁 Script executed:

# Also check if scraper.py or other scripts might import app
grep -r "import app\|from app" --include="*.py" . 2>/dev/null | head -20

Repository: cuappdev/score-backend

Length of output: 48


🏁 Script executed:

# Find scraper.py and check it
find . -name "scraper.py" -type f && echo "---" && wc -l ./scraper.py

Repository: cuappdev/score-backend

Length of output: 98


🏁 Script executed:

# Check what happens when scraper.py runs without GOOGLE_APPLICATION_CREDENTIALS
head -50 ./scraper.py

Repository: cuappdev/score-backend

Length of output: 1441


🏁 Script executed:

# Also look for any pytest or test configuration
find . -name "pytest.ini" -o -name "conftest.py" -o -name "tox.ini" -o -name ".pytest_cache" 2>/dev/null

Repository: cuappdev/score-backend

Length of output: 48


Firebase initialization at import time will hard-fail any process that imports app.

The code captures SERVICE_ACCOUNT_PATH at module load (line 30) and invokes initialize_firebase() unconditionally at line 45, before logging.basicConfig() (line 115). This creates multiple failure modes:

  • The scraper service in docker-compose.yml (line 18) does not set GOOGLE_APPLICATION_CREDENTIALS, so starting it will crash with ValueError, taking down the container.
  • If the credentials file is missing or unreadable, credentials.Certificate() also fails with the same blast radius.
  • The logging.info("Firebase app initialized.") call at line 41 executes before logging.basicConfig(), so the message goes to the root logger with default configuration and is effectively swallowed.

Move initialization out of module scope — either into an app factory, a request context, or behind an explicit initialization call. When credentials are absent, degrade gracefully (log a warning, skip init) rather than crashing. Only the mutations that actually require auth need Firebase.

Also, relying on the private firebase_admin._apps attribute is brittle. Use the documented approach:

try:
    app = firebase_admin.get_app()
except ValueError:
    # App not initialized yet
    cred = credentials.Certificate(service_account_path)
    app = firebase_admin.initialize_app(cred)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app.py` around lines 30 - 45, The module currently reads SERVICE_ACCOUNT_PATH
at import and calls initialize_firebase() unconditionally which causes hard
failures and premature logging; change initialize_firebase() to not run at
import (remove the top-level initialize_firebase() call), refactor it to attempt
firebase_admin.get_app() in a try/except instead of using firebase_admin._apps,
only initialize with credentials.Certificate(SERVICE_ACCOUNT_PATH) if
SERVICE_ACCOUNT_PATH is present, and when credentials are missing or unreadable
log a warning (using a module logger or after logging.basicConfig()) and return
None so callers can opt into Firebase; ensure any code that needs Firebase calls
initialize_firebase() explicitly (or use an app factory/request hook) rather
than relying on import-time side effects.


app = Flask(__name__)

# CORS: allow frontend (different origin) to call this API
Expand Down Expand Up @@ -143,16 +164,6 @@ def signal_handler(sig, frame):
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)

# Only parse arguments when running directly (not when imported by gunicorn)
if __name__ == "__main__":
args = parse_args()
else:
# Default args when imported by gunicorn
class DefaultArgs:
no_scrape = False
no_daily_sun = False
args = DefaultArgs()

# Only run scraping tasks if not disabled
if not args.no_scrape:
from flask_apscheduler import APScheduler
Expand Down
3 changes: 3 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ services:
app:
image: cornellappdev/score-dev:${IMAGE_TAG}
env_file: .env
environment:
GOOGLE_APPLICATION_CREDENTIALS: /app/secrets/firebase.json
ports:
- "8000:8000"
volumes:
- ./ca-certificate.crt:/etc/ssl/ca-certificate.crt:ro # Mount MongoDB cert inside the container, ro for read only
- ${FIREBASE_CREDENTIALS_HOST_PATH:-./firebase-service-account-key.json}:/app/secrets/firebase.json:ro

scraper:
image: cornellappdev/score-dev:${IMAGE_TAG}
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ Flask-APScheduler
python-dotenv
pytz
gunicorn
firebase-admin==7.3.0
10 changes: 10 additions & 0 deletions src/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def setup_database_indexes():
game_collection.create_index([("date", -1)], background=True)

try:
# Ensure doubleheaders on the same day remain distinct by including `time`.
game_collection.create_index(
[
("sport", 1),
Expand All @@ -79,8 +80,10 @@ def setup_database_indexes():
("city", 1),
("state", 1),
("location", 1),
("time", 1),
],
unique=True,
name="uniq_game_key_with_time",
background=True
)
except (DuplicateKeyError, OperationFailure) as e:
Expand All @@ -101,6 +104,13 @@ def setup_database_indexes():
# JWT blocklist: fast lookup by jti
db["token_blocklist"].create_index([("jti", 1)], background=True)

try:
db["users"].create_index(
[("firebase_uid", 1)], unique=True, sparse=True, background=True
)
except (DuplicateKeyError, OperationFailure) as e:
print(f"Warning: Could not create unique index on users.firebase_uid: {e}")

print("✅ MongoDB indexes created successfully")
except Exception as e:
print(f"❌ Failed to create MongoDB indexes: {e}")
Expand Down
15 changes: 15 additions & 0 deletions src/models/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ class Game:
- `box_score` The scoring summary of the game (optional)
- `score_breakdown` The scoring breakdown of the game (optional)
- 'ticket_link' The ticket link for the game (optional)
- 'recap_link' The recap/details link for the game (optional)
- 'recap_article_title' Title from the recap/story page when scraped (optional)
- 'recap_published_at' Published date/time string from the recap page (optional)
"""

def __init__(
Expand All @@ -37,6 +40,9 @@ def __init__(
team=None,
utc_date=None,
ticket_link=None,
recap_link=None,
recap_article_title=None,
recap_published_at=None,
):
self.id = id if id else str(ObjectId())
self.city = city
Expand All @@ -53,6 +59,9 @@ def __init__(
self.team = team
self.utc_date = utc_date
self.ticket_link = ticket_link
self.recap_link = recap_link
self.recap_article_title = recap_article_title
self.recap_published_at = recap_published_at

def to_dict(self):
"""
Expand All @@ -74,6 +83,9 @@ def to_dict(self):
"team": self.team,
"utc_date": self.utc_date,
"ticket_link": self.ticket_link,
"recap_link": self.recap_link,
"recap_article_title": self.recap_article_title,
"recap_published_at": self.recap_published_at,
}

@staticmethod
Expand All @@ -97,4 +109,7 @@ def from_dict(data) -> None:
team=data.get("team"),
utc_date=data.get("utc_date"),
ticket_link=data.get("ticket_link"),
recap_link=data.get("recap_link"),
recap_article_title=data.get("recap_article_title"),
recap_published_at=data.get("recap_published_at"),
)
13 changes: 11 additions & 2 deletions src/mutations/create_game.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ class Arguments:
score_breakdown = String(required=False)
utc_date = String(required=False)
ticket_link = String(required=False)
recap_link = String(required=False)
recap_article_title = String(required=False)
recap_published_at = String(required=False)

game = Field(lambda: GameType)

Expand All @@ -36,7 +39,10 @@ def mutate(
box_score=None,
score_breakdown=None,
utc_date=None,
ticket_link=None
ticket_link=None,
recap_link=None,
recap_article_title=None,
recap_published_at=None,
):
game_data = {
"city": city,
Expand All @@ -51,7 +57,10 @@ def mutate(
"box_score": box_score,
"score_breakdown": score_breakdown,
"utc_date": utc_date,
"ticket_link": ticket_link
"ticket_link": ticket_link,
"recap_link": recap_link,
"recap_article_title": recap_article_title,
"recap_published_at": recap_published_at,
}
new_game = GameService.create_game(game_data)
return CreateGame(game=new_game)
25 changes: 21 additions & 4 deletions src/mutations/login_user.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,36 @@
from graphql import GraphQLError
from graphene import Mutation, String, Field
from graphene import Mutation, String

from firebase_admin import auth as firebase_auth
from flask_jwt_extended import create_access_token, create_refresh_token
from src.database import db

_TOKEN_ERRORS = (
firebase_auth.InvalidIdTokenError,
firebase_auth.ExpiredIdTokenError,
firebase_auth.RevokedIdTokenError,
)


class LoginUser(Mutation):
class Arguments:
net_id = String(required=True, description="User's net ID (e.g. Cornell netid).")
id_token = String(required=True, description="Firebase ID token from the client.")

access_token = String()
refresh_token = String()

def mutate(self, info, net_id):
user = db["users"].find_one({"net_id": net_id})
def mutate(self, info, id_token):
try:
decoded = firebase_auth.verify_id_token(id_token)
except _TOKEN_ERRORS as err:
raise GraphQLError("Invalid or expired token.") from err
except ValueError as err:
raise GraphQLError("Invalid or expired token.") from err

firebase_uid = decoded.get("uid")
if not firebase_uid:
raise GraphQLError("Invalid or expired token.")
user = db["users"].find_one({"firebase_uid": firebase_uid})
Comment on lines +8 to +33
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

firebase_admin auth verify_id_token check_revoked parameter Python SDK

💡 Result:

In the Firebase Admin Python SDK (firebase_admin.auth), the verify_id_token method includes a check_revoked parameter (named check_revoked, boolean, default False). Usage: from firebase_admin import auth try: decoded_token = auth.verify_id_token(id_token, check_revoked=True) uid = decoded_token['uid'] # Token valid and not revoked except auth.RevokedIdTokenError: # Token revoked - force reauthentication pass except auth.UserDisabledError: # User disabled pass except auth.InvalidIdTokenError: # Invalid token pass When check_revoked=True, it fetches the UserRecord for the token's uid and compares the token's iat (issued at) against user.tokens_valid_after_timestamp (milliseconds since epoch; None if no revocation). If iat * 1000 < tokens_valid_after_timestamp, raises RevokedIdTokenError. Also checks if user is disabled (raises UserDisabledError). This requires an extra network call to Firebase Auth backend, making it slower. Basic verification (check_revoked=False) only checks signature, expiration, issuer, audience. Official docs confirm this behavior with Python examples using check_revoked=True. Note: Early bug fixed in 2018 where None handling caused TypeError.

Citations:


🏁 Script executed:

# First, let's check the actual code in the file
cat -n src/mutations/login_user.py | head -50

Repository: cuappdev/score-backend

Length of output: 1702


🌐 Web query:

firebase_admin.auth CertificateFetchError exception Python SDK

💡 Result:

The firebase_admin.auth.CertificateFetchError is a specific exception raised by the Firebase Admin Python SDK when it fails to fetch public key certificates needed to verify Firebase ID tokens or session cookies. This occurs during calls to auth.verify_id_token or auth.verify_session_cookie. Key details: - Bases: UnknownError (subtype of FirebaseError) - Trigger: Network or connectivity issues preventing access to Google's certificate endpoint (typically https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com). - Common causes: Firewalls blocking outbound HTTPS to Google APIs, proxy misconfigurations (especially on macOS where system proxies intercept localhost but also affect external calls), network timeouts, ISP outages, SSL/TLS issues (outdated OpenSSL/LibreSSL), or corporate network restrictions. - The SDK caches certificates after successful fetch to reduce repeated calls. Solutions and workarounds: - Verify network connectivity: Test direct access to the certs URL using requests.get('https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'). - Check proxies: Set NO_PROXY='www.googleapis.com,.googleapis.com' or os.environ['NO_PROXY'] = 'localhost,127.0.0.1,.googleapis.com'. - Update environment: Ensure recent Python (3.9+), OpenSSL, and firebase-admin SDK versions. Check ssl.OPENSSL_VERSION. - Disable proxy trust if using requests: session.trust_env = False (though SDK manages this internally). - Firewall/VPN: Temporarily disable to test; configure exceptions for Google domains. - Retry logic: Implement exponential backoff around verify_id_token calls. - Ensure latest SDK: Uses HTTP/2, improved timeouts, and caching. Handle in code: try: claims = auth.verify_id_token(id_token) except auth.CertificateFetchError as exc: # Log cause: exc.cause may contain underlying requests/urllib error raise # or handle with retry

Citations:


🌐 Web query:

firebase_admin.auth exceptions complete list verify_id_token

💡 Result:

The complete list of exceptions raised by firebase_admin.auth.verify_id_token(id_token, check_revoked=False, clock_skew_seconds=0) is: - ValueError: If id_token is not a string or is empty. - InvalidIdTokenError: If id_token is not a valid Firebase ID token. - ExpiredIdTokenError: If the specified ID token has expired. - RevokedIdTokenError: If check_revoked is True and the ID token has been revoked. - TenantIdMismatchError: If id_token belongs to a tenant that is different than this Client instance. - CertificateFetchError: If an error occurs while fetching the public key certificates required to verify the ID token. - UserDisabledError: If check_revoked is True and the corresponding user record is disabled. These are the specific exceptions documented for this method. Note that some older versions raised more generic ValueError for token issues, but specific auth exceptions were added later.

Citations:


Remove RevokedIdTokenError from caught exceptions—it's not reachable without check_revoked=True.

firebase_auth.verify_id_token(id_token) is called without check_revoked=True, so RevokedIdTokenError will never be raised. Remove it from _TOKEN_ERRORS.

Additionally, handle firebase_auth.CertificateFetchError separately. This transient network error (fetching Google's public key certificates) currently bubbles up unhandled, when it should be distinguished from authentication failures since it indicates an upstream infrastructure issue, not an invalid token.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mutations/login_user.py` around lines 8 - 33, Remove
firebase_auth.RevokedIdTokenError from the _TOKEN_ERRORS tuple since
verify_id_token(id_token) is called without check_revoked=True; then in
LoginUser.mutate add a separate except block for
firebase_auth.CertificateFetchError (imported from firebase_auth) to catch
transient certificate fetch/network errors and raise a distinct GraphQLError
like "Authentication service unavailable, please try again later." from err so
certificate fetch failures are distinguished from token validation failures;
keep the existing ValueError and other token-related exception handling around
verify_id_token.

if not user:
raise GraphQLError("User not found.")
identity = str(user["_id"])
Expand Down
38 changes: 29 additions & 9 deletions src/mutations/signup_user.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,51 @@
from graphql import GraphQLError
from graphene import Mutation, String

from firebase_admin import auth as firebase_auth
from flask_jwt_extended import create_access_token, create_refresh_token
from pymongo.errors import DuplicateKeyError
from src.database import db

_TOKEN_ERRORS = (
firebase_auth.InvalidIdTokenError,
firebase_auth.ExpiredIdTokenError,
firebase_auth.RevokedIdTokenError,
)


class SignupUser(Mutation):
class Arguments:
net_id = String(required=True, description="User's net ID (e.g. Cornell netid).")
id_token = String(required=True, description="Firebase ID token from the client.")
name = String(required=False, description="Display name.")
email = String(required=False, description="Email address.")
email = String(required=False, description="Email (overrides token email if provided).")

access_token = String()
refresh_token = String()

def mutate(self, info, net_id, name=None, email=None):
if db["users"].find_one({"net_id": net_id}):
raise GraphQLError("Net ID already exists.")
def mutate(self, info, id_token, name=None, email=None):
try:
decoded = firebase_auth.verify_id_token(id_token)
except _TOKEN_ERRORS as err:
raise GraphQLError("Invalid or expired token.") from err
except ValueError as err:
raise GraphQLError("Invalid or expired token.") from err

firebase_uid = decoded.get("uid")
if firebase_uid is None:
raise GraphQLError("Token missing uid") from KeyError("uid")

email = email or decoded.get("email")
user_doc = {
"net_id": net_id,
"firebase_uid": firebase_uid,
"email": email,
"favorite_game_ids": [],
}
if name is not None:
user_doc["name"] = name
if email is not None:
user_doc["email"] = email
result = db["users"].insert_one(user_doc)
try:
result = db["users"].insert_one(user_doc)
except DuplicateKeyError as err:
raise GraphQLError("User already exists.") from err
Comment on lines +33 to +48
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor: inconsistency with login_user.py and synthetic KeyError chain.

Two small nits:

  1. When uid is missing from the decoded token, this raises GraphQLError("Token missing uid"), but login_user.py raises GraphQLError("Invalid or expired token.") for the same condition. Consider aligning the user-facing message across both mutations so clients can handle auth failures uniformly.
  2. raise GraphQLError("Token missing uid") from KeyError("uid") constructs a throwaway KeyError solely for chaining. A bare raise GraphQLError(...) (no from) is more idiomatic here since there is no original exception to chain.
💡 Proposed tweak
-        firebase_uid = decoded.get("uid")
-        if firebase_uid is None:
-            raise GraphQLError("Token missing uid") from KeyError("uid")
+        firebase_uid = decoded.get("uid")
+        if not firebase_uid:
+            raise GraphQLError("Invalid or expired token.")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
firebase_uid = decoded.get("uid")
if firebase_uid is None:
raise GraphQLError("Token missing uid") from KeyError("uid")
email = email or decoded.get("email")
user_doc = {
"net_id": net_id,
"firebase_uid": firebase_uid,
"email": email,
"favorite_game_ids": [],
}
if name is not None:
user_doc["name"] = name
if email is not None:
user_doc["email"] = email
result = db["users"].insert_one(user_doc)
try:
result = db["users"].insert_one(user_doc)
except DuplicateKeyError as err:
raise GraphQLError("User already exists.") from err
firebase_uid = decoded.get("uid")
if not firebase_uid:
raise GraphQLError("Invalid or expired token.")
email = email or decoded.get("email")
user_doc = {
"firebase_uid": firebase_uid,
"email": email,
"favorite_game_ids": [],
}
if name is not None:
user_doc["name"] = name
try:
result = db["users"].insert_one(user_doc)
except DuplicateKeyError as err:
raise GraphQLError("User already exists.") from err
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mutations/signup_user.py` around lines 33 - 48, The uid-check in
signup_user.py currently raises GraphQLError("Token missing uid") and chains a
synthetic KeyError("uid"); change the GraphQLError message to match
login_user.py ("Invalid or expired token.") for consistent client-facing auth
errors, and remove the artificial exception chaining (drop the "from
KeyError('uid')" so you simply raise GraphQLError(...)) at the firebase_uid
check in the signup flow where firebase_uid = decoded.get("uid") is validated.

identity = str(result.inserted_id)
return SignupUser(
access_token=create_access_token(identity=identity),
Expand Down
46 changes: 30 additions & 16 deletions src/repositories/game_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@
logger = logging.getLogger(__name__)


def _time_for_lookup(time):
"""True when `time` should be included in a query (aligned with uniq_game_key_with_time)."""
if time is None:
return False
s = str(time).strip()
if not s or s in ("TBD", "TBA"):
return False
return True


class GameRepository:
@staticmethod
def find_all(limit=100, offset=0):
Expand Down Expand Up @@ -103,24 +113,26 @@ def find_by_data(city, date, gender, location, opponent_id, sport, state, time):
return Game.from_dict(game_data) if game_data else None

@staticmethod
def find_by_key_fields(city, date, gender, location, opponent_id, sport, state):
def find_by_key_fields(city, date, gender, location, opponent_id, sport, state, time=None):
"""
Find games without time for duplicate games
Find games by key fields. When `time` is a concrete value (not TBD/TBA), the query
includes it so doubleheaders resolve to a single row. Otherwise falls back to the
legacy filter without time (multiple rows possible).
"""
game_collection = db["game"]
games = list(
game_collection.find(
{
"city": city,
"date": date,
"gender": gender,
"location": location,
"opponent_id": opponent_id,
"sport": sport,
"state": state,
}
)
)
base = {
"city": city,
"date": date,
"gender": gender,
"location": location,
"opponent_id": opponent_id,
"sport": sport,
"state": state,
}
if _time_for_lookup(time):
games = list(game_collection.find({**base, "time": time}))
else:
games = list(game_collection.find(base))

if not games:
return None
Expand All @@ -131,7 +143,7 @@ def find_by_key_fields(city, date, gender, location, opponent_id, sport, state):
return [Game.from_dict(game) for game in games]

@staticmethod
def find_by_tournament_key_fields(city, date, gender, location, sport, state):
def find_by_tournament_key_fields(city, date, gender, location, sport, state, time=None):
"""
Find tournament games by location and date (excluding opponent_id).
This is used when we need to find a tournament game that might have a placeholder team.
Expand All @@ -145,6 +157,8 @@ def find_by_tournament_key_fields(city, date, gender, location, sport, state):
"gender": gender,
"sport": sport,
}
if _time_for_lookup(time):
query["time"] = time

# For city, state, and location, use flexible matching
# This allows finding games even when TBD/TBA values change to real values
Expand Down
6 changes: 4 additions & 2 deletions src/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ class Mutation(ObjectType):
create_team = CreateTeam.Field(description="Creates a new team.")
create_youtube_video = CreateYoutubeVideo.Field(description="Creates a new youtube video.")
create_article = CreateArticle.Field(description="Creates a new article.")
login_user = LoginUser.Field(description="Login by net_id; returns access_token and refresh_token.")
login_user = LoginUser.Field(
description="Login with Firebase ID token; returns access_token and refresh_token.",
)
signup_user = SignupUser.Field(
description="Create a new user by net_id; returns access_token and refresh_token (no separate login needed).",
description="Create a new user with Firebase ID token; returns access_token and refresh_token.",
)
refresh_access_token = RefreshAccessToken.Field(
description="Exchange a valid refresh token (in Authorization header) for a new access_token.",
Expand Down
Loading
Loading