Skip to content

Commit 1bcf70a

Browse files
committed
resolve conflicts
2 parents cc9ebd1 + a64620b commit 1bcf70a

26 files changed

Lines changed: 764 additions & 150 deletions

.github/CONTRIBUTING.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,37 @@
1-
# Contributing to Ithaca Transit Backend
1+
# Contributing to Score Backend
2+
23
👍🎉 First off, congrats on getting put on this pod 😂🎉👍
34

45
The following is a set of guidelines for contributing to our backend. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
56

67
# Versioning
8+
79
This is _extremely_ important as handling our backend can get quite messy.
810

911
# Making PRs
12+
1013
We want our PRs to be concise but informative. Some pointers as per our PR template:
14+
1115
### Title
16+
1217
Summarize what changes you've made in one sentence. For example: "Exclude staff from the check for follows". For stacked PRs, please indicate clearly in the title where in the stack you are. For example: "[Eatery Refactor][4/5] Converted all files to MVP model."
18+
1319
### Overview
20+
1421
Summarize generally what the purpose of this PR is.
22+
1523
### Changes
24+
1625
Include details of what your changes actually are and how it is intended to work.
26+
1727
### Test Coverage
28+
1829
Describe how you tested this feature: manual testing and/or unit testing. Please include repro steps and/or how to turn the feature on if applicable. In the context of this repo, add a plan for how you intend to test on [integration](https://github.com/cuappdev/integration), with your newly created issue linked.
30+
1931
### Next Steps
32+
2033
If this applies, describe how you plan on addressing future PRs if this is a part of a multi-PR change.
34+
2135
### Related PRs or Issues
22-
If this applies, list related PRs against other branches or repositories.
2336

37+
If this applies, list related PRs against other branches or repositories.

.github/workflows/deploy-dev.yml

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,39 +6,49 @@ on:
66
workflow_dispatch:
77

88
jobs:
9-
path-context:
9+
build:
1010
runs-on: ubuntu-latest
11+
outputs:
12+
sha_short: ${{ steps.vars.outputs.sha_short }}
1113
steps:
1214
- name: Checkout
13-
uses: actions/checkout@v2
15+
uses: actions/checkout@v4
1416
- name: Set up Docker Buildx
15-
uses: docker/setup-buildx-action@v1
17+
uses: docker/setup-buildx-action@v3
1618
- name: Login to DockerHub
17-
uses: docker/login-action@v1
19+
uses: docker/login-action@v3
1820
with:
1921
username: ${{ secrets.DOCKER_USERNAME }}
2022
password: ${{ secrets.DOCKER_PASSWORD }}
2123
- name: Get SHA
2224
id: vars
23-
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
25+
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
2426
- name: Docker Build & Push
25-
uses: docker/build-push-action@v2
27+
uses: docker/build-push-action@v6
2628
with:
2729
context: ./
2830
file: ./Dockerfile
2931
push: true
3032
tags: cornellappdev/score-dev:${{ steps.vars.outputs.sha_short }}
33+
cache-from: type=registry,ref=cornellappdev/score-dev:cache
34+
cache-to: type=registry,ref=cornellappdev/score-dev:cache,mode=max
35+
36+
deploy:
37+
runs-on: ubuntu-latest
38+
needs: build
39+
steps:
40+
- name: Checkout
41+
uses: actions/checkout@v4
3142
- name: Remote SSH and Deploy
32-
uses: appleboy/ssh-action@master
43+
uses: appleboy/ssh-action@v1.0.3
3344
env:
34-
IMAGE_TAG: ${{ steps.vars.outputs.sha_short }}
45+
IMAGE_TAG: ${{ needs.build.outputs.sha_short }}
3546
with:
3647
host: ${{ secrets.DEV_SERVER_HOST }}
3748
username: ${{ secrets.SERVER_USERNAME }}
3849
key: ${{ secrets.DEV_SERVER_KEY }}
50+
envs: IMAGE_TAG
3951
script: |
40-
export IMAGE_TAG=${{ steps.vars.outputs.sha_short }}
41-
docker stack rm thestack
42-
sleep 20s
4352
docker stack deploy -c docker-compose.yml thestack
44-
yes | docker system prune -a
53+
docker service update --image cornellappdev/score-dev:${IMAGE_TAG} thestack_app
54+
docker system prune -af

.github/workflows/deploy-prod.yml

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,39 +6,49 @@ on:
66
workflow_dispatch:
77

88
jobs:
9-
path-context:
9+
build:
1010
runs-on: ubuntu-latest
11+
outputs:
12+
sha_short: ${{ steps.vars.outputs.sha_short }}
1113
steps:
1214
- name: Checkout
13-
uses: actions/checkout@v2
15+
uses: actions/checkout@v4
1416
- name: Set up Docker Buildx
15-
uses: docker/setup-buildx-action@v1
17+
uses: docker/setup-buildx-action@v3
1618
- name: Login to DockerHub
17-
uses: docker/login-action@v1
19+
uses: docker/login-action@v3
1820
with:
1921
username: ${{ secrets.DOCKER_USERNAME }}
2022
password: ${{ secrets.DOCKER_PASSWORD }}
2123
- name: Get SHA
2224
id: vars
23-
run: echo "::set-output name=sha_short::$(git rev-parse --short release)"
25+
run: echo "sha_short=$(git rev-parse --short release)" >> $GITHUB_OUTPUT
2426
- name: Docker Build & Push
25-
uses: docker/build-push-action@v2
27+
uses: docker/build-push-action@v6
2628
with:
2729
context: ./
2830
file: ./Dockerfile
2931
push: true
3032
tags: cornellappdev/score:${{ steps.vars.outputs.sha_short }}
33+
cache-from: type=registry,ref=cornellappdev/score:cache
34+
cache-to: type=registry,ref=cornellappdev/score:cache,mode=max
35+
36+
deploy:
37+
runs-on: ubuntu-latest
38+
needs: build
39+
steps:
40+
- name: Checkout
41+
uses: actions/checkout@v4
3142
- name: Remote SSH and Deploy
32-
uses: appleboy/ssh-action@master
43+
uses: appleboy/ssh-action@v1.0.3
3344
env:
34-
IMAGE_TAG: ${{ steps.vars.outputs.sha_short }}
45+
IMAGE_TAG: ${{ needs.build.outputs.sha_short }}
3546
with:
3647
host: ${{ secrets.PROD_SERVER_HOST }}
3748
username: ${{ secrets.SERVER_USERNAME }}
3849
key: ${{ secrets.PROD_SERVER_KEY }}
50+
envs: IMAGE_TAG
3951
script: |
40-
export IMAGE_TAG=${{ steps.vars.outputs.sha_short }}
41-
docker stack rm thestack
42-
sleep 20s
4352
docker stack deploy -c docker-compose.yml thestack
44-
docker system prune -a
53+
docker service update --image cornellappdev/score:${IMAGE_TAG} thestack_app
54+
docker system prune -af

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
FROM python:3.9
1+
FROM python:3.9-slim
22
RUN mkdir -p /usr/src/app
33
WORKDIR /usr/src/app
44
COPY . .
55
RUN pip3 install --upgrade pip
66
RUN pip install -r requirements.txt
7-
CMD python app.py
7+
CMD gunicorn app:app -b 0.0.0.0:8000 --workers 1 --timeout 60 --max-requests 1000 --max-requests-jitter 200

app.py

Lines changed: 78 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,70 @@
11
import logging
22
import argparse
3-
from flask import Flask
3+
from flask import Flask, request, g
4+
import time
45
from flask_graphql import GraphQLView
5-
from flask_apscheduler import APScheduler
66
from graphene import Schema
77
from src.schema import Query, Mutation
88
from src.scrapers.games_scraper import fetch_game_schedule
99
from src.scrapers.youtube_stats import fetch_videos
1010
from src.scrapers.daily_sun_scrape import fetch_news
1111
from src.services.article_service import ArticleService
1212
from src.utils.team_loader import TeamLoader
13+
import signal
14+
import sys
15+
from dotenv import load_dotenv
16+
17+
load_dotenv()
1318

1419
app = Flask(__name__)
1520

16-
# Set up the scheduler
17-
scheduler = APScheduler()
18-
scheduler.init_app(app)
19-
scheduler.start()
21+
22+
@app.before_request
23+
def start_timer():
24+
g.start = time.time()
25+
26+
if request.path == "/graphql" and request.method == "POST":
27+
try:
28+
# Try to extract the GraphQL query name for better logging
29+
query_data = request.get_json()
30+
if query_data and "query" in query_data:
31+
g.query = query_data["query"].split("{", 1)[0].strip()
32+
logging.info(
33+
f"[{time.strftime('%H:%M:%S')}] --> GraphQL {g.query} started"
34+
)
35+
except:
36+
pass
37+
38+
logging.info(
39+
f"[{time.strftime('%H:%M:%S')}] --> {request.method} {request.path} started"
40+
)
41+
42+
43+
@app.after_request
44+
def log_response_time(response):
45+
if hasattr(g, "start"):
46+
duration = time.time() - g.start
47+
48+
if duration > 5.0: # Flag slow requests
49+
if hasattr(g, "query"):
50+
logging.warning(
51+
f"[{time.strftime('%H:%M:%S')}] <-- SLOW GraphQL {g.query} ({duration:.2f}s)"
52+
)
53+
else:
54+
logging.warning(
55+
f"[{time.strftime('%H:%M:%S')}] <-- SLOW {request.method} {request.path} ({duration:.2f}s)"
56+
)
57+
else:
58+
if hasattr(g, "query"):
59+
logging.info(
60+
f"[{time.strftime('%H:%M:%S')}] <-- GraphQL {g.query} finished in {duration:.2f}s"
61+
)
62+
else:
63+
logging.info(
64+
f"[{time.strftime('%H:%M:%S')}] <-- {request.method} {request.path} finished in {duration:.2f}s"
65+
)
66+
return response
67+
2068

2169
# Configure logging
2270
logging.basicConfig(
@@ -27,13 +75,16 @@
2775

2876
schema = Schema(query=Query, mutation=Mutation)
2977

78+
3079
def create_context():
31-
return {
32-
"team_loader": TeamLoader()
33-
}
80+
return {"team_loader": TeamLoader()}
81+
3482

3583
app.add_url_rule(
36-
"/graphql", view_func=GraphQLView.as_view("graphql", schema=schema, graphiql=True, get_context=create_context)
84+
"/graphql",
85+
view_func=GraphQLView.as_view(
86+
"graphql", schema=schema, graphiql=True, get_context=create_context
87+
),
3788
)
3889

3990
# Setup command line arguments
@@ -52,22 +103,35 @@ def parse_args():
52103
return parser.parse_args()
53104

54105
args = parse_args()
106+
107+
def signal_handler(sig, frame):
108+
sys.exit(0)
109+
110+
111+
signal.signal(signal.SIGINT, signal_handler)
112+
signal.signal(signal.SIGTERM, signal_handler)
113+
114+
# Only run scraping tasks if not disabled
55115
if not args.no_scrape:
56-
@scheduler.task("interval", id="scrape_schedules", seconds=3600)
116+
from flask_apscheduler import APScheduler
117+
scheduler = APScheduler()
118+
scheduler.init_app(app)
119+
scheduler.start()
57120

121+
@scheduler.task("interval", id="scrape_schedules", seconds=43200) # 12 hours
58122
def scrape_schedules():
59123
logging.info("Scraping game schedules...")
60124
fetch_game_schedule()
61125

62-
@scheduler.task("interval", id="scrape_schedules", seconds=43200) # 12 hours
126+
@scheduler.task("interval", id="scrape_videos", seconds=43200) # 12 hours
63127
def scrape_videos():
64128
logging.info("Scraping YouTube videos...")
65129
fetch_videos()
66130

67131
scrape_schedules()
68132
scrape_videos()
69133

70-
if not args.no_daily_sun:
134+
if not args.no_daily_sun and not args.no_scrape:
71135
@scheduler.task("interval", id="scrape_daily_sun", seconds=3600)
72136
def scrape_daily_sun():
73137
logging.info("Getting Daily Sun Sports News...")
@@ -83,4 +147,4 @@ def cleanse_daily_sun_db():
83147

84148

85149
if __name__ == "__main__":
86-
app.run(debug=True, host="0.0.0.0", port=8000)
150+
app.run(debug=True, host="0.0.0.0", port=8000)

docker-compose.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,10 @@ services:
88
- "8000:8000"
99
volumes:
1010
- ./ca-certificate.crt:/etc/ssl/ca-certificate.crt:ro # Mount MongoDB cert inside the container, ro for read only
11+
12+
scraper:
13+
image: cornellappdev/score-dev:${IMAGE_TAG}
14+
env_file: .env
15+
command: python scraper.py
16+
volumes:
17+
- ./ca-certificate.crt:/etc/ssl/ca-certificate.crt:ro

requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ beautifulsoup4
66
requests
77
pillow
88
Flask-APScheduler
9-
python-dotenv
9+
python-dotenv
10+
pytz
11+
gunicorn

scraper.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import logging
2+
import time
3+
import signal
4+
import sys
5+
from apscheduler.schedulers.background import BackgroundScheduler
6+
from src.scrapers.games_scraper import fetch_game_schedule
7+
from src.scrapers.youtube_stats import fetch_videos
8+
9+
logging.basicConfig(
10+
format="%(asctime)s %(levelname)-8s %(message)s",
11+
level=logging.INFO,
12+
datefmt="%Y-%m-%d %H:%M:%S",
13+
)
14+
15+
scheduler = BackgroundScheduler(daemon=True)
16+
17+
18+
def scrape_schedules():
19+
start_time = time.time()
20+
logging.info("Starting scraping games")
21+
fetch_game_schedule()
22+
elapsed_time = time.time() - start_time
23+
logging.info(f"Completed scraping games in {elapsed_time:.2f} seconds")
24+
25+
26+
def scrape_videos():
27+
start_time = time.time()
28+
logging.info("Scraping YouTube videos")
29+
fetch_videos()
30+
elapsed_time = time.time() - start_time
31+
logging.info(f"Completed scraping videos in {elapsed_time:.2f} seconds")
32+
33+
34+
def signal_handler(sig, frame):
35+
logging.info("Shutting down scheduler...")
36+
scheduler.shutdown(wait=True)
37+
sys.exit(0)
38+
39+
40+
signal.signal(signal.SIGINT, signal_handler)
41+
signal.signal(signal.SIGTERM, signal_handler)
42+
43+
if __name__ == "__main__":
44+
scheduler.add_job(
45+
scrape_schedules, "interval", seconds=60 * 60, id="scrape_schedules"
46+
)
47+
scheduler.add_job(
48+
scrape_videos, "interval", seconds=60 * 60 * 24, id="scrape_videos"
49+
)
50+
scheduler.start()
51+
scrape_schedules()
52+
scrape_videos()
53+
54+
signal.pause()

0 commit comments

Comments
 (0)