Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
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
21 changes: 21 additions & 0 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, auth
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

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 +26 to +45
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Mar 25, 2026

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

🏁 Script executed:

#!/bin/bash
# Check if .env_template includes the Firebase credential variable
echo "=== Checking .env_template ==="
cat .env_template 2>/dev/null || echo ".env_template not found"

echo ""
echo "=== Checking docker-compose.yml for volume mounts ==="
cat docker-compose.yml 2>/dev/null | grep -A5 "volumes:" || echo "No volumes section found"

Repository: cuappdev/score-backend

Length of output: 498


🏁 Script executed:

# Check file size and read app.py to verify all claims
wc -l app.py
echo "=== Full app.py contents ===" 
cat -n app.py

Repository: cuappdev/score-backend

Length of output: 8592


🏁 Script executed:

# Check if Dockerfile exists and examine it
if [ -f Dockerfile ]; then
  echo "=== Dockerfile found ==="
  cat -n Dockerfile
else
  echo "Dockerfile not found"
fi

Repository: cuappdev/score-backend

Length of output: 391


Deployment configuration incomplete for Firebase.

The Firebase initialization requires GOOGLE_APPLICATION_CREDENTIALS but the deployment setup is missing key configurations:

  1. .env_template doesn't document this required variable
  2. docker-compose.yml doesn't mount the Firebase key file into containers
  3. Dockerfile doesn't configure the environment variable

This will cause startup failures in containerized deployments.

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

In `@app.py` around lines 26 - 45, The deployment is missing configuration for the
Firebase service account used by initialize_firebase: ensure
SERVICE_ACCOUNT_PATH (GOOGLE_APPLICATION_CREDENTIALS) is documented in
.env_template, make the Firebase key file available to containers by mounting it
in docker-compose.yml (add a volume mapping and set the container env
GOOGLE_APPLICATION_CREDENTIALS to the mounted path), and update the Dockerfile
or the docker-compose service env to set the GOOGLE_APPLICATION_CREDENTIALS
environment variable (or use Docker secrets) so that SERVICE_ACCOUNT_PATH is
present when initialize_firebase() runs; keep references to SERVICE_ACCOUNT_PATH
and initialize_firebase in the changes so reviewers can find the initialization
logic.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are these set in both dev and prod servers @claiireyu ?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


app = Flask(__name__)

# CORS: allow frontend (different origin) to call this API
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
3 changes: 3 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 Down
15 changes: 11 additions & 4 deletions src/mutations/login_user.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
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


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 Exception:
raise GraphQLError("Invalid or expired token.")

firebase_uid = decoded["uid"]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
user = db["users"].find_one({"firebase_uid": firebase_uid})
if not user:
raise GraphQLError("User not found.")
identity = str(user["_id"])
Expand Down
24 changes: 16 additions & 8 deletions src/mutations/signup_user.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,38 @@
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 src.database import db


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 Exception:
raise GraphQLError("Invalid or expired token.")

firebase_uid = decoded["uid"]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
if db["users"].find_one({"firebase_uid": firebase_uid}):
raise GraphQLError("User already exists.")

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)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
identity = str(result.inserted_id)
return SignupUser(
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
32 changes: 30 additions & 2 deletions src/scrapers/game_details_scrape.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,34 @@ def extract_teams_and_scores(box_score_section, sport):

return team_names, period_scores

def softball_summary(box_score_section):
summary = []
scoring_section = box_score_section.find(TAG_SECTION, {ATTR_ARIA_LABEL: LABEL_SCORING_SUMMARY})
if scoring_section:
scoring_rows = scoring_section.find(TAG_TBODY)
if scoring_rows:
for row in scoring_rows.find_all(TAG_TR):
team = row.find_all(TAG_TD)[0].find(TAG_IMG)[ATTR_ALT]
inning = row.find_all(TAG_TD)[3].text.strip()
desc_cell = row.find_all(TAG_TD)[4]
span = desc_cell.find(TAG_SPAN)
if span:
span.extract()
desc = desc_cell.get_text(strip=True)
cornell_score = int(row.find_all(TAG_TD)[5].get_text(strip=True) or 0)
opp_score = int(row.find_all(TAG_TD)[6].get_text(strip=True) or 0)
Comment on lines +63 to +71
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

Harden softball row parsing to avoid scraper crashes on markup variance.

Line 63-Line 71 assumes every row has all expected cells/image and strictly numeric score cells. A single malformed row can raise and terminate parsing for this game.

🛠️ Proposed defensive fix
 def softball_summary(box_score_section):
     summary = []
     scoring_section = box_score_section.find(TAG_SECTION, {ATTR_ARIA_LABEL: LABEL_SCORING_SUMMARY})
     if scoring_section:
         scoring_rows = scoring_section.find(TAG_TBODY)
         if scoring_rows:
             for row in scoring_rows.find_all(TAG_TR):
-                team = row.find_all(TAG_TD)[0].find(TAG_IMG)[ATTR_ALT]
-                inning = row.find_all(TAG_TD)[3].text.strip()
-                desc_cell = row.find_all(TAG_TD)[4]
+                cells = row.find_all(TAG_TD)
+                if len(cells) < 7:
+                    continue
+
+                team_img = cells[0].find(TAG_IMG)
+                team = team_img.get(ATTR_ALT, "").strip() if team_img else cells[0].get_text(strip=True)
+                inning = cells[3].get_text(strip=True)
+                desc_cell = cells[4]
                 span = desc_cell.find(TAG_SPAN)
                 if span:
                     span.extract()
                 desc = desc_cell.get_text(strip=True)
-                cornell_score = int(row.find_all(TAG_TD)[5].get_text(strip=True) or 0)
-                opp_score = int(row.find_all(TAG_TD)[6].get_text(strip=True) or 0)
+
+                cor_raw = cells[5].get_text(strip=True)
+                opp_raw = cells[6].get_text(strip=True)
+                cornell_score = int(cor_raw) if cor_raw.isdigit() else 0
+                opp_score = int(opp_raw) if opp_raw.isdigit() else 0
                 summary.append({
                         'team': team,
                         'period': inning,
                         'inning': inning,
                         'description': desc,
                         'cor_score': cornell_score,
                         'opp_score': opp_score
                     })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/scrapers/game_details_scrape.py` around lines 63 - 71, The parser assumes
every row cell and image/score text exists; make it defensive by null-checking
and safe-accessing before indexing/fetching: use row.find_all(TAG_TD) and assign
to a local cells list, verify len(cells) > N before accessing cells[0],
cells[3], cells[4], cells[5], cells[6]; for the team image use
cells[0].find(TAG_IMG) and read img.get(ATTR_ALT, '') instead of direct
indexing; for the description use cells[4] only if present, guard span =
desc_cell.find(TAG_SPAN) with if desc_cell and span, and for
cornell_score/opp_score wrap int conversion in a safe parse (try/except
ValueError) or use a helper that returns 0 for non-numeric text (apply to
variables cornell_score and opp_score). Ensure any missing fields fall back to
sensible defaults and continue parsing rather than raising.

summary.append({
'team': team,
'inning': inning,
'description': desc,
'cor_score': cornell_score,
'opp_score': opp_score
})
if not summary:
summary = [{"message": "No scoring events in this game."}]
return summary


def soccer_summary(box_score_section):
summary = []
scoring_section = box_score_section.find(TAG_SECTION, {ATTR_ARIA_LABEL: LABEL_SCORING_SUMMARY})
Expand Down Expand Up @@ -124,14 +152,13 @@ def hockey_summary(box_score_section):
scorer = row.find_all(TAG_TD)[4].text.strip()
assist = row.find_all(TAG_TD)[5].text.strip()

if team == "COR" or team == "CU" or team == "Cornell":
if team == "COR" or team == "CU" or team == "Cornell" or team == "CORNELL":
cornell_score += 1
else:
opp_score += 1

summary.append({
'team': team,
'period': period,
'time': time,
'scorer': scorer,
'assist': assist,
Expand Down Expand Up @@ -272,6 +299,7 @@ def scrape_game(url, sport):
'field hockey': (lambda: extract_teams_and_scores(box_score_section, 'field hockey'), field_hockey_summary),
'lacrosse': (lambda: extract_teams_and_scores(box_score_section, 'lacrosse'), lacrosse_summary),
'baseball': (lambda: extract_teams_and_scores(box_score_section, 'baseball'), baseball_summary),
'softball': (lambda: extract_teams_and_scores(box_score_section, 'softball'), softball_summary),
'basketball': (lambda: extract_teams_and_scores(box_score_section, 'basketball'), lambda _: []),
}

Expand Down
13 changes: 7 additions & 6 deletions src/scrapers/games_scraper.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ def parse_schedule_page(url, sport, gender):

result_tag = game_item.select_one(RESULT_TAG)
if result_tag:
game_data["result"] = result_tag.text.strip().replace("\n", "")
#game_data["result"] = result_tag.get_text(" ", strip=True)
game_data["result"] = result_tag.text.strip().replace("\n", " ")
else:
game_data["result"] = None

Expand Down Expand Up @@ -241,17 +242,16 @@ def process_game_data(game_data):
if str(final_box_cor_score) != str(cor_final) or str(final_box_opp_score) != str(opp_final):
game_data["score_breakdown"] = game_data["score_breakdown"][::-1]

# Try to find by tournament key fields to handle placeholder teams
# Try to find an existing game record to update.
curr_game = GameService.get_game_by_tournament_key_fields(
city,
game_data["date"],
game_data["gender"],
location,
game_data["sport"],
state
state,
)

# If no tournament game found, try the regular lookup with opponent_id

if not curr_game:
curr_game = GameService.get_game_by_key_fields(
city,
Expand All @@ -260,14 +260,15 @@ def process_game_data(game_data):
location,
team.id,
game_data["sport"],
state
state,
)

if isinstance(curr_game, list):
if curr_game:
curr_game = curr_game[0]
else:
curr_game = None

if curr_game:
updates = {
"time": game_time,
Expand Down
1 change: 1 addition & 0 deletions src/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class BoxScoreEntryType(ObjectType):

team = String(required=False)
period = String(required=False)
inning = String(required=False)
time = String(required=False)
description = String(required=False)
scorer = String(required=False)
Expand Down
Loading