From 39949437575f558a5a16ebd7b178923f5512434d Mon Sep 17 00:00:00 2001 From: josh Date: Sun, 29 Mar 2026 09:20:21 -0400 Subject: [PATCH] good luck --- .gitea/workflows/ci.yml | 81 ++++++++++ .github/workflows/docker-deploy.yml | 23 --- Dockerfile | 29 ++-- app/__init__.py | 10 +- app/config.py | 8 + app/routes.py | 55 ++++--- app/scoreboard/get_data.py | 50 +++--- app/scoreboard/process_data.py | 144 ++++++++++++------ app/scoreboard/tasks.py | 13 +- app/scoreboard/update_nhl_standings_db.py | 46 +++--- app/templates/index.html | 2 +- docker-compose.yml | 11 ++ requirements-dev.txt | 4 + requirements.txt | 12 +- run.py | 12 +- tests/__init__.py | 0 tests/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 146 bytes .../conftest.cpython-313-pytest-8.3.4.pyc | Bin 0 -> 3241 bytes ..._process_data.cpython-313-pytest-8.3.4.pyc | Bin 0 -> 21912 bytes .../test_routes.cpython-313-pytest-8.3.4.pyc | Bin 0 -> 9691 bytes tests/conftest.py | 93 +++++++++++ tests/test_process_data.py | 103 +++++++++++++ tests/test_routes.py | 44 ++++++ 23 files changed, 579 insertions(+), 161 deletions(-) create mode 100644 .gitea/workflows/ci.yml delete mode 100644 .github/workflows/docker-deploy.yml create mode 100644 app/config.py create mode 100644 docker-compose.yml create mode 100644 requirements-dev.txt create mode 100644 tests/__init__.py create mode 100644 tests/__pycache__/__init__.cpython-313.pyc create mode 100644 tests/__pycache__/conftest.cpython-313-pytest-8.3.4.pyc create mode 100644 tests/__pycache__/test_process_data.cpython-313-pytest-8.3.4.pyc create mode 100644 tests/__pycache__/test_routes.cpython-313-pytest-8.3.4.pyc create mode 100644 tests/conftest.py create mode 100644 tests/test_process_data.py create mode 100644 tests/test_routes.py diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..200bce0 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,81 @@ +name: CI + +on: + push: + branches: + - main + tags: + - "v*" + pull_request: + branches: + - main + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install ruff + run: pip install ruff==0.8.6 + + - name: Check formatting + run: ruff format --check . + + - name: Check linting + run: ruff check . + + test: + name: Test + runs-on: ubuntu-latest + needs: lint + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install dependencies + run: pip install -r requirements-dev.txt + + - name: Run tests + run: pytest tests/ -v + + build-push: + name: Build & Push + runs-on: ubuntu-latest + needs: test + # Only build on pushes to main or version tags — not on PRs + if: github.event_name == 'push' + steps: + - uses: actions/checkout@v4 + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ gitea.server_url && '' }}${{ vars.GITEA_REGISTRY }}/nhlscoreboard + tags: | + type=semver,pattern={{version}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Log in to Gitea registry + uses: docker/login-action@v3 + with: + registry: ${{ vars.GITEA_REGISTRY }} + username: ${{ gitea.actor }} + password: ${{ secrets.GITEA_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/docker-deploy.yml b/.github/workflows/docker-deploy.yml deleted file mode 100644 index 826624f..0000000 --- a/.github/workflows/docker-deploy.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Docker Build and Push - -on: - release: - types: [published] - -jobs: - build-and-push: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - uses: mr-smithers-excellent/docker-build-push@v6.3 - name: Build & push Docker image - with: - image: joshnotwright/nhlscoreboard - tags: ${{ github.event.release.tag_name }}, latest - registry: docker.io - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - diff --git a/Dockerfile b/Dockerfile index 8b7f367..0f79154 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,25 @@ -# Use an official Python runtime as a parent image -FROM python:3.9-slim +FROM python:3.13-slim -# Set environment variables -ENV PYTHONDONTWRITEBYTECODE 1 -ENV PYTHONUNBUFFERED 1 +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + DATA_DIR=/app/app/data -# Set the working directory in the container WORKDIR /app -# Copy the current directory contents into the container at /app -COPY . /app - -# Install any needed dependencies specified in requirements.txt +COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -# Create the directory for scoreboard data -RUN mkdir -p app/data +COPY . . + +RUN mkdir -p $DATA_DIR \ + && adduser --disabled-password --gecos "" appuser \ + && chown -R appuser:appuser /app + +USER appuser -# Expose the Flask port EXPOSE 2897 -# Run the Flask application +HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:2897/')" || exit 1 + CMD ["python", "run.py"] diff --git a/app/__init__.py b/app/__init__.py index 96c8ef5..665cde7 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,5 +1,13 @@ +import logging from flask import Flask +from app.config import LOG_LEVEL + +logging.basicConfig( + level=getattr(logging, LOG_LEVEL.upper(), logging.INFO), + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", +) app = Flask(__name__) -from app import routes +from app import routes # noqa: E402, F401 diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..f9c5799 --- /dev/null +++ b/app/config.py @@ -0,0 +1,8 @@ +import os + +DATA_DIR = os.environ.get("DATA_DIR", "app/data") +PORT = int(os.environ.get("PORT", 2897)) +LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO") + +SCOREBOARD_DATA_FILE = os.path.join(DATA_DIR, "scoreboard_data.json") +DB_PATH = os.path.join(DATA_DIR, "nhl_standings.db") diff --git a/app/routes.py b/app/routes.py index dc39732..da876be 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,32 +1,51 @@ -from app import app -from flask import render_template, jsonify -from app.scoreboard.process_data import extract_game_info import json -SCOREBOARD_DATA_FILE = 'app/data/scoreboard_data.json' +from flask import render_template, jsonify -@app.route('/') +from app import app +from app.config import SCOREBOARD_DATA_FILE +from app.scoreboard.process_data import extract_game_info + + +@app.route("/") def index(): - return render_template('index.html') - -@app.route('/scoreboard') + return render_template("index.html") + + +@app.route("/scoreboard") def get_scoreboard(): try: - with open(SCOREBOARD_DATA_FILE, 'r') as json_file: + with open(SCOREBOARD_DATA_FILE, "r") as json_file: scoreboard_data = json.load(json_file) except FileNotFoundError: return jsonify({"error": "Failed to retrieve scoreboard data. File not found."}) except json.JSONDecodeError: - return jsonify({"error": "Failed to retrieve scoreboard data. Invalid JSON format."}) + return jsonify( + {"error": "Failed to retrieve scoreboard data. Invalid JSON format."} + ) if scoreboard_data: - live_games = [game for game in extract_game_info(scoreboard_data) if game["Game State"] == "LIVE"] - pre_games = [game for game in extract_game_info(scoreboard_data) if game["Game State"] == "PRE"] - final_games = [game for game in extract_game_info(scoreboard_data) if game["Game State"] == "FINAL"] - return jsonify({ - "live_games": live_games, - "pre_games": pre_games, - "final_games": final_games - }) + live_games = [ + game + for game in extract_game_info(scoreboard_data) + if game["Game State"] == "LIVE" + ] + pre_games = [ + game + for game in extract_game_info(scoreboard_data) + if game["Game State"] == "PRE" + ] + final_games = [ + game + for game in extract_game_info(scoreboard_data) + if game["Game State"] == "FINAL" + ] + return jsonify( + { + "live_games": live_games, + "pre_games": pre_games, + "final_games": final_games, + } + ) else: return jsonify({"error": "Failed to retrieve scoreboard data"}) diff --git a/app/scoreboard/get_data.py b/app/scoreboard/get_data.py index 41fe7c6..702714c 100644 --- a/app/scoreboard/get_data.py +++ b/app/scoreboard/get_data.py @@ -1,34 +1,40 @@ -import requests -from datetime import datetime import json +import logging +from datetime import datetime +from zoneinfo import ZoneInfo + +import requests + +from app.config import SCOREBOARD_DATA_FILE + +logger = logging.getLogger(__name__) + +EASTERN = ZoneInfo("America/New_York") -SCOREBOARD_DATA_FILE = 'app/data/scoreboard_data.json' def get_scoreboard_data(): - now = datetime.now() - start_time_evening = now.replace(hour=19, minute=00, second=0, microsecond=0) # 7:00 PM EST - end_time_evening = now.replace(hour=3, minute=00, second=0, microsecond=0) # 3:00 AM EST + now = datetime.now(EASTERN) + start_time_evening = now.replace(hour=19, minute=0, second=0, microsecond=0) + end_time_morning = now.replace(hour=3, minute=0, second=0, microsecond=0) - if now >= start_time_evening or now < end_time_evening: - # Use now URL + if now >= start_time_evening or now < end_time_morning: nhle_api_url = "https://api-web.nhle.com/v1/score/now" - else: - # Use current data URL nhle_api_url = f"https://api-web.nhle.com/v1/score/{now.strftime('%Y-%m-%d')}" - - response = requests.get(nhle_api_url) - if response.status_code == 200: - return response.json() - else: - print("Error:", response.status_code) -# Store scoreboard data locally + try: + response = requests.get(nhle_api_url, timeout=10) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + logger.error("Failed to fetch scoreboard data from %s: %s", nhle_api_url, e) + return None + + def store_scoreboard_data(): - scoreboard_data = get_scoreboard_data() + scoreboard_data = get_scoreboard_data() if scoreboard_data: - with open(SCOREBOARD_DATA_FILE, 'w') as json_file: + with open(SCOREBOARD_DATA_FILE, "w") as json_file: json.dump(scoreboard_data, json_file) - return scoreboard_data - else: - return None \ No newline at end of file + return scoreboard_data + return None diff --git a/app/scoreboard/process_data.py b/app/scoreboard/process_data.py index 06e20a7..a86bc29 100644 --- a/app/scoreboard/process_data.py +++ b/app/scoreboard/process_data.py @@ -1,6 +1,12 @@ +import logging import sqlite3 from datetime import datetime, timedelta +from app.config import DB_PATH + +logger = logging.getLogger(__name__) + + def process_record(record): if record == "N/A": return "N/A" @@ -9,6 +15,7 @@ def process_record(record): formatted_parts = [part.zfill(2) for part in parts] return "-".join(formatted_parts) + def extract_game_info(scoreboard_data): if not scoreboard_data: return [] @@ -16,36 +23,60 @@ def extract_game_info(scoreboard_data): extracted_info = [] for game in scoreboard_data.get("games", []): game_state = convert_game_state(game["gameState"]) - extracted_info.append({ - "Home Team": game["homeTeam"]["name"]["default"], - "Home Score": game["homeTeam"]["score"] if game_state != "PRE" else "N/A", - "Away Team": game["awayTeam"]["name"]["default"], - "Away Score": game["awayTeam"]["score"] if game_state != "PRE" else "N/A", - "Home Logo": game["homeTeam"]["logo"], - "Away Logo": game["awayTeam"]["logo"], - "Game State": game_state, - "Period": process_period(game), - "Time Remaining": process_time_remaining(game), - "Time Running": game["clock"]["running"] if game_state == "LIVE" else "N/A", - "Intermission": game["clock"]["inIntermission"] if game_state == "LIVE" else "N/A", - "Priority": calculate_game_priority(game), - "Start Time": process_start_time(game), - "Home Record": process_record(game["homeTeam"]["record"]) if game["gameState"] in ["PRE", "FUT"] else "N/A", - "Away Record": process_record(game["awayTeam"]["record"]) if game["gameState"] in ["PRE", "FUT"] else "N/A", - "Home Shots": game["homeTeam"]["sog"] if game["gameState"] not in ["PRE", "FUT"] else 0, - "Away Shots": game["awayTeam"]["sog"] if game["gameState"] not in ["PRE", "FUT"] else 0, - "Home Power Play": get_power_play_info(game, game["homeTeam"]["name"]["default"]), - "Away Power Play": get_power_play_info(game, game["awayTeam"]["name"]["default"]), - "Last Period Type": get_game_outcome(game, game_state) - }) + extracted_info.append( + { + "Home Team": game["homeTeam"]["name"]["default"], + "Home Score": game["homeTeam"]["score"] + if game_state != "PRE" + else "N/A", + "Away Team": game["awayTeam"]["name"]["default"], + "Away Score": game["awayTeam"]["score"] + if game_state != "PRE" + else "N/A", + "Home Logo": game["homeTeam"]["logo"], + "Away Logo": game["awayTeam"]["logo"], + "Game State": game_state, + "Period": process_period(game), + "Time Remaining": process_time_remaining(game), + "Time Running": game["clock"]["running"] + if game_state == "LIVE" + else "N/A", + "Intermission": game["clock"]["inIntermission"] + if game_state == "LIVE" + else "N/A", + "Priority": calculate_game_priority(game), + "Start Time": process_start_time(game), + "Home Record": process_record(game["homeTeam"]["record"]) + if game["gameState"] in ["PRE", "FUT"] + else "N/A", + "Away Record": process_record(game["awayTeam"]["record"]) + if game["gameState"] in ["PRE", "FUT"] + else "N/A", + "Home Shots": game["homeTeam"]["sog"] + if game["gameState"] not in ["PRE", "FUT"] + else 0, + "Away Shots": game["awayTeam"]["sog"] + if game["gameState"] not in ["PRE", "FUT"] + else 0, + "Home Power Play": get_power_play_info( + game, game["homeTeam"]["name"]["default"] + ), + "Away Power Play": get_power_play_info( + game, game["awayTeam"]["name"]["default"] + ), + "Last Period Type": get_game_outcome(game, game_state), + } + ) # Sort games based on priority return sorted(extracted_info, key=lambda x: x["Priority"], reverse=True) + def convert_game_state(game_state): state_mapping = {"OFF": "FINAL", "CRIT": "LIVE", "FUT": "PRE"} return state_mapping.get(game_state, game_state) + def process_period(game): if game["gameState"] in ["PRE", "FUT"]: return 0 @@ -54,6 +85,7 @@ def process_period(game): else: return game["periodDescriptor"]["number"] + def process_time_remaining(game): if game["gameState"] in ["PRE", "FUT"]: return "20:00" @@ -63,17 +95,16 @@ def process_time_remaining(game): time_remaining = game["clock"]["timeRemaining"] return "END" if time_remaining == "00:00" else time_remaining + def process_start_time(game): if game["gameState"] in ["PRE", "FUT"]: utc_time = game["startTimeUTC"] est_time = utc_to_est_time(utc_time) - # Check if the hour starts with a zero - if est_time.startswith("0"): - est_time = est_time[1:] # Drop the leading zero - return est_time + return est_time.lstrip("0") else: return "N/A" + def get_power_play_info(game, team_name): if "situation" in game and "situationDescriptions" in game["situation"]: for situation in game["situation"]["situationDescriptions"]: @@ -83,14 +114,16 @@ def get_power_play_info(game, team_name): return f"PP {game['situation']['timeRemaining']}" return "" + def get_game_outcome(game, game_state): return game["gameOutcome"]["lastPeriodType"] if game_state == "FINAL" else "N/A" + def calculate_game_priority(game): # Return 0 if game is in certain states if game["gameState"] in ["FINAL", "OFF", "PRE", "FUT"]: return 0 - + # Get period, time remaining, scores, and other relevant data period = game.get("periodDescriptor", {}).get("number", 0) time_remaining = game.get("clock", {}).get("secondsRemaining", 0) @@ -104,8 +137,14 @@ def calculate_game_priority(game): away_team_standings = get_team_standings(game["awayTeam"]["name"]["default"]) # Calculate total values of leagueSequence + leagueL10Sequence for each team - home_total = home_team_standings["league_sequence"] + home_team_standings["league_l10_sequence"] - away_total = away_team_standings["league_sequence"] + away_team_standings["league_l10_sequence"] + home_total = ( + home_team_standings["league_sequence"] + + home_team_standings["league_l10_sequence"] + ) + away_total = ( + away_team_standings["league_sequence"] + + away_team_standings["league_l10_sequence"] + ) # Calculate the matchup adjustment factor matchup_multiplier = {5: 1, 4: 1, 3: 1.50, 2: 1.65, 1: 2}.get(period) @@ -123,11 +162,11 @@ def calculate_game_priority(game): score_differential_adjustment += 350 elif score_difference > 1: score_differential_adjustment += 100 - + if period == 3 and time_remaining <= 300: score_differential_adjustment = score_differential_adjustment * 2 - - base_priority -= score_differential_adjustment + + base_priority -= score_differential_adjustment # Adjust base priority based on certain conditions if period == 3 and time_remaining <= 720: @@ -142,40 +181,51 @@ def calculate_game_priority(game): elif score_difference == 1: base_priority += 30 - # Calculate time priority time_multiplier = {4: 2, 3: 2, 2: 1.5}.get(period, 0.75) - time_priority = ((1200 - time_remaining) / 20) * time_multiplier - print(base_priority) - print(time_priority) - print(matchup_adjustment) - print(score_total) + logger.debug( + "priority components — base: %s, time: %s, matchup: %s, score_total: %s", + base_priority, + time_priority, + matchup_adjustment, + score_total, + ) # Calculate the final priority - final_priority = int(base_priority + time_priority - matchup_adjustment + score_total) + final_priority = int( + base_priority + time_priority - matchup_adjustment + score_total + ) # Pushes the games that are in intermission to the bottom, but retains their sort if game["clock"]["inIntermission"]: - return (-2000 - time_remaining) + return -2000 - time_remaining return final_priority + def get_team_standings(team_name): - conn = sqlite3.connect("app/data/nhl_standings.db") + conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() - cursor.execute(""" - SELECT league_sequence, league_l10_sequence - FROM standings + cursor.execute( + """ + SELECT league_sequence, league_l10_sequence + FROM standings WHERE team_common_name = ? - """, (team_name,)) + """, + (team_name,), + ) result = cursor.fetchone() conn.close() - return {"league_sequence": result[0] if result else 0, "league_l10_sequence": result[1] if result else 0} + return { + "league_sequence": result[0] if result else 0, + "league_l10_sequence": result[1] if result else 0, + } + def utc_to_est_time(utc_time): utc_datetime = datetime.strptime(utc_time, "%Y-%m-%dT%H:%M:%SZ") est_offset = timedelta(hours=-4) est_datetime = utc_datetime + est_offset - return est_datetime.strftime("%#I:%M %p") + return est_datetime.strftime("%I:%M %p") diff --git a/app/scoreboard/tasks.py b/app/scoreboard/tasks.py index 4fae985..566c947 100644 --- a/app/scoreboard/tasks.py +++ b/app/scoreboard/tasks.py @@ -1,13 +1,18 @@ -import schedule +import logging import time -from app.scoreboard.update_nhl_standings_db import update_nhl_standings + +import schedule + from app.scoreboard.get_data import store_scoreboard_data +from app.scoreboard.update_nhl_standings_db import update_nhl_standings + +logger = logging.getLogger(__name__) + def schedule_tasks(): schedule.every(600).seconds.do(update_nhl_standings) schedule.every(10).seconds.do(store_scoreboard_data) + logger.info("Background scheduler started") while True: schedule.run_pending() time.sleep(1) - - diff --git a/app/scoreboard/update_nhl_standings_db.py b/app/scoreboard/update_nhl_standings_db.py index c1e4455..b0d18a6 100644 --- a/app/scoreboard/update_nhl_standings_db.py +++ b/app/scoreboard/update_nhl_standings_db.py @@ -1,6 +1,13 @@ +import logging import sqlite3 + import requests +from app.config import DB_PATH + +logger = logging.getLogger(__name__) + + def create_standings_table(conn): cursor = conn.cursor() cursor.execute(""" @@ -12,54 +19,55 @@ def create_standings_table(conn): """) conn.commit() + def truncate_standings_table(conn): cursor = conn.cursor() cursor.execute("DELETE FROM standings") conn.commit() + def insert_standings_info(conn, standings_info): cursor = conn.cursor() for team in standings_info: - cursor.execute(""" + cursor.execute( + """ INSERT INTO standings (team_common_name, league_sequence, league_l10_sequence) VALUES (?, ?, ?) - """, (team["team_common_name"], team["league_sequence"], team["league_l10_sequence"])) + """, + ( + team["team_common_name"], + team["league_sequence"], + team["league_l10_sequence"], + ), + ) conn.commit() + def extract_standings_info(): url = "https://api-web.nhle.com/v1/standings/now" - response = requests.get(url) - if response.status_code == 200: + try: + response = requests.get(url, timeout=10) + response.raise_for_status() standings_data = response.json() standings_info = [] for team in standings_data.get("standings", []): team_info = { "team_common_name": team["teamCommonName"]["default"], "league_sequence": team["leagueSequence"], - "league_l10_sequence": team["leagueL10Sequence"] + "league_l10_sequence": team["leagueL10Sequence"], } standings_info.append(team_info) return standings_info - else: - print("Error:", response.status_code) + except requests.RequestException as e: + logger.error("Failed to fetch standings: %s", e) return None + def update_nhl_standings(): - # Connect to SQLite database - conn = sqlite3.connect("app/data/nhl_standings.db") - - # Create standings table if it doesn't exist + conn = sqlite3.connect(DB_PATH) create_standings_table(conn) - - # Truncate standings table before inserting new data truncate_standings_table(conn) - - # Extract standings info standings_info = extract_standings_info() - - # Insert standings info into the database if standings_info: insert_standings_info(conn, standings_info) - - # Close database connection conn.close() diff --git a/app/templates/index.html b/app/templates/index.html index 9dbb0c3..f0b7068 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -3,7 +3,7 @@ NHL Scoreboard - +
diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ed96485 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +services: + nhl-scoreboard: + build: . + ports: + - "2897:2897" + volumes: + - ./data:/app/app/data + environment: + - DATA_DIR=/app/app/data + - LOG_LEVEL=INFO + restart: unless-stopped diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..34cbd13 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,4 @@ +-r requirements.txt +pytest==8.3.4 +pytest-mock==3.14.0 +ruff==0.8.6 diff --git a/requirements.txt b/requirements.txt index c35fa70..3eaa9ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -Flask==3.0.2 -Jinja2==3.1.3 -requests==2.31.0 -Werkzeug==3.0.1 -waitress==3.0.0 -schedule==1.2.1 +Flask==3.1.0 +Jinja2==3.1.4 +requests==2.32.3 +Werkzeug==3.1.3 +waitress==3.0.1 +schedule==1.2.2 diff --git a/run.py b/run.py index 5cc4ddf..9c8e54e 100644 --- a/run.py +++ b/run.py @@ -1,13 +1,13 @@ -from app import app -from waitress import serve import threading +from app import app +from app.config import PORT from app.scoreboard.tasks import schedule_tasks from app.scoreboard.get_data import store_scoreboard_data from app.scoreboard.update_nhl_standings_db import update_nhl_standings +from waitress import serve -if __name__ == '__main__': +if __name__ == "__main__": store_scoreboard_data() update_nhl_standings() - threading.Thread(target=schedule_tasks).start() - serve(app, host="0.0.0.0", port=2897) - + threading.Thread(target=schedule_tasks, daemon=True).start() + serve(app, host="0.0.0.0", port=PORT) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/__pycache__/__init__.cpython-313.pyc b/tests/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1f5d8db15c9039985e65bf1480a8ec4dbfcb1889 GIT binary patch literal 146 zcmey&%ge<81P4@4W`gL)AOZ#$p^VQgK*m&tbOudEzm*I{OhDdekklqSU1P#G(}alGNgoV*U8|%)HE!_;|g7%3B;Zx%nxjIjMF< VtUwb$HWY&xADI~$8H<>KECB9nA-@0s literal 0 HcmV?d00001 diff --git a/tests/__pycache__/conftest.cpython-313-pytest-8.3.4.pyc b/tests/__pycache__/conftest.cpython-313-pytest-8.3.4.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a1aeb510f2e0004daab8ce42554b1f9249dfe5c5 GIT binary patch literal 3241 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 A=>Px# literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_routes.cpython-313-pytest-8.3.4.pyc b/tests/__pycache__/test_routes.cpython-313-pytest-8.3.4.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3947e898ec71aac5a380cabcbb61a3f8fd921899 GIT binary patch literal 9691 zcmeGi+in}jb(iFl!;3}R^euKQ5t1y6PAp2YWV?wKRFNIuWq>ONZq#D27FQAzYL}T^ z)wWWjDSCm@m-@wk`qHuwMXO)v7qoArq=3SrMGExEZ>fzSfS-EK?CvZ{Q?ioQE{bZf zIy>jgnahr5&XrI1?1>3bzUcc)F&Gwve<0$OkXv~=36)2JEXd+@fr{)MxE3@6Y2jLk zN@=GhOv6WnZb1$l73AP(!0AgPj1~f##A#$s%!D5!!{m&^=0MR!o~EJlNccXRiw0zo z260pxl7j#xIYh&AQYLJ7OlhWdRWE4wa>k;iJ;wFUxD&IXA^?N%_jCh>ekA0@oHqq) zc7QC7x>P-o59AvAyIeXW^!9Bf`|fz1f!r4dmM-5wbU zjJmwCG)+9t9NT;kbKc#;s7rVo-i2dLvm}~%5$hK60q+aEwW)`idUb5y>y`*>^1q}# zchH;Tg`6ReQw|RVPs6_GVL2j`e0We~SY&#ix1NTv_Iot{4PO@V#`4}89KSJ94~~iD zsW~ANowUX5<3B^6Rq9I7{1~cY>X{pwtsmvG!E%qQi z<}{3EOq)>6EE~G1tp@EN@FEySF#4=RHBF6LY>acj3dD1(F*`V?S$4c(Ks)x`@_h?V zq@q%LpE9GCN($A=bjDaHt5mZS(5&29ES9XIZrb6J0UV}%L^0=$#ZrN>Dsv^{j#^T5 zbwRVEimA?OID*|V&X!O#^h=Z))J`e023=4sMY~rnsk&-G%PO%$rdFD@6SF1N{79K8 z6*b+Wcwp=}PVIcgP$M$Zh@5c{XXLYu$axbFY}zgMNHz}}VrD-y%=zpDY-M)x%8k?V zj6t*Dy=-f!W-U_PRE7r!?XIb&>pRfOAi<>O&jEZYY^J*E zsh(P@r!ujg>R%?C$;0(zPc7L~C8sJT1GVI-I&q&ha;lo_;iyyjoSa&|$7wYmg9gsM zj^lAAUfCe0p29-5kS=RGlrA7ehTF*(lr4DI|9#ogaG$u0!F4+Y?i0VoaO1h`$+eX# z+eo4I65>TtsQrp~uNEoPp8M6x7K#eQ#IJ=Dp7u}ps%Qs_dPZD&69vmq#!MG=CLchQ zIARAXuGl#PVyB>5YU}~Kiy0@R_gN!D)nwX1HyY}k0f?0=9IN@*=QtE^;1%Gf z8Cb=tFqw(iL}4;iQS6waEEt8wk_PpJqI|rlmQcC^mQ5 z2NeYcdvQilEsGZKpbP_s5oEwg*ltnOm<(XW#J&H8@FWnB-uQ|Hq+y2`_0;fxMI*Bt*8b@K2JYQX?EPtO?dR~O zwiL9i+u8M=Ez10Yi}_%S?*2X4-duaNxA3`NFyZ?J4Sn!G*iv9EzV4HzxdzUm4rwWE zJP3b1?d@!C7xcShvCHK5YQG}h3w1YG`+>IB{y}ef4PS`bt)*;j?UygfQDb+XVZUDb zKF4S!{sYpsiROt2JSqyiR&HlSiwh zd!kJ9hW^RqS& zjCEo$wxt*&yY#A>NUt#_Z8qCwp|3 zUWQpSiI%{vV^UAzs<>TEuOZrtpa(#snLXNwoMYifFGGWGC!p(?+`fmQ%&XvRQ0?Qz zA_aqcwy2c~+nL-i=!v0d^8?uJ9huzm3?nW$Ov|SxaQAsS^&m(NJGUZqCIk z!185MNOU@W2`2Hq!jt*}96b0{0wjd53)dheoW^h+io}+J9t8X%!5ss*#2k*%Qe!AkO#*<_|JBH%grOZ6pq)W17%;qH)7QE|O_smCmLD)_X=Of^wL~d&h64~$5K6DrM z=9S4b=8ss(wGFU6m(ugc#{5!Cf$!x0kM|YKNAY{LUlH#Gwlk5VuJk8zOpfP?EB&L> znAT#Y!L!1&R!f0Za82TUsq3T6f!t3_GHGugkSA{CF1>qeJaoc1$go2b?^%9d_RlQv+3CQ5IIA zS76EXDguns>^P6(oe&JiCU04lO)fae%XjDuut01} mFw+3O42q)og&_P)_-Xv>a6}ybrvTvV1D#^m69E8Q