Spaces:
Sleeping
fix: address critical and medium post-audit findings
Browse filesCritical/High:
- Remove dead _get_model() cached wrapper (never called)
- Add [build-system] to pyproject.toml for reliable editable installs
- Fix devcontainer.json to use pyproject.toml instead of deleted requirements.txt
- Add scikit-learn and scikeras as [train] optional deps
Medium:
- Move streamlit import in config.py to lazy import inside configure_page()
- Remove unreachable QueryExecutionError catch blocks in 1_home_team.py
- Add uv cache step to CI workflow
- Fix actions/checkout and actions/setup-python to stable v4/v5
Low:
- Remove duplicated --cov-fail-under from CI (pyproject.toml is single source)
- Fix SIM105 indentation in pyproject.toml
- Fix compile_model.py arr type to match str|float sliced values
- Add dist/ and build/ to .gitignore
- .devcontainer/devcontainer.json +1 -1
- .github/workflows/ci.yml +10 -3
- .gitignore +2 -0
- pages/1_home_team.py +4 -13
- pages/2_play_game.py +0 -6
- pyproject.toml +10 -1
- scripts/compile_model.py +1 -1
- src/config.py +2 -2
|
@@ -17,7 +17,7 @@
|
|
| 17 |
]
|
| 18 |
}
|
| 19 |
},
|
| 20 |
-
"updateContentCommand": "[ -f packages.txt ] && sudo apt update && sudo apt upgrade -y && sudo xargs apt install -y <packages.txt;
|
| 21 |
"postAttachCommand": {
|
| 22 |
"server": "streamlit run app.py"
|
| 23 |
},
|
|
|
|
| 17 |
]
|
| 18 |
}
|
| 19 |
},
|
| 20 |
+
"updateContentCommand": "[ -f packages.txt ] && sudo apt update && sudo apt upgrade -y && sudo xargs apt install -y <packages.txt; pip3 install --user -e '.[dev]'; echo 'Packages installed and requirements met'",
|
| 21 |
"postAttachCommand": {
|
| 22 |
"server": "streamlit run app.py"
|
| 23 |
},
|
|
@@ -11,21 +11,28 @@ jobs:
|
|
| 11 |
runs-on: ubuntu-latest
|
| 12 |
|
| 13 |
steps:
|
| 14 |
-
- uses: actions/checkout@
|
| 15 |
|
| 16 |
- name: Set up Python
|
| 17 |
-
uses: actions/setup-python@
|
| 18 |
with:
|
| 19 |
python-version: "3.11"
|
| 20 |
|
| 21 |
- name: Install uv
|
| 22 |
run: pip install uv
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
- name: Install dependencies
|
| 25 |
run: uv pip install -e ".[dev]" --system
|
| 26 |
|
| 27 |
- name: Run tests
|
| 28 |
-
run: pytest --cov=src --cov-report=term-missing
|
| 29 |
|
| 30 |
- name: Run ruff
|
| 31 |
run: ruff check src/ tests/
|
|
|
|
| 11 |
runs-on: ubuntu-latest
|
| 12 |
|
| 13 |
steps:
|
| 14 |
+
- uses: actions/checkout@v4
|
| 15 |
|
| 16 |
- name: Set up Python
|
| 17 |
+
uses: actions/setup-python@v5
|
| 18 |
with:
|
| 19 |
python-version: "3.11"
|
| 20 |
|
| 21 |
- name: Install uv
|
| 22 |
run: pip install uv
|
| 23 |
|
| 24 |
+
- name: Cache uv packages
|
| 25 |
+
uses: actions/cache@v4
|
| 26 |
+
with:
|
| 27 |
+
path: ~/.cache/uv
|
| 28 |
+
key: uv-${{ runner.os }}-${{ hashFiles('pyproject.toml') }}
|
| 29 |
+
restore-keys: uv-${{ runner.os }}-
|
| 30 |
+
|
| 31 |
- name: Install dependencies
|
| 32 |
run: uv pip install -e ".[dev]" --system
|
| 33 |
|
| 34 |
- name: Run tests
|
| 35 |
+
run: pytest --cov=src --cov-report=term-missing
|
| 36 |
|
| 37 |
- name: Run ruff
|
| 38 |
run: ruff check src/ tests/
|
|
@@ -18,6 +18,8 @@ htmlcov/
|
|
| 18 |
|
| 19 |
# Packaging
|
| 20 |
*.egg-info/
|
|
|
|
|
|
|
| 21 |
|
| 22 |
# uv
|
| 23 |
uv.lock
|
|
|
|
| 18 |
|
| 19 |
# Packaging
|
| 20 |
*.egg-info/
|
| 21 |
+
dist/
|
| 22 |
+
build/
|
| 23 |
|
| 24 |
# uv
|
| 25 |
uv.lock
|
|
@@ -8,7 +8,6 @@ import streamlit as st
|
|
| 8 |
from src.config import DIFFICULTY_PRESETS, PLAYER_COLUMNS, configure_page
|
| 9 |
from src.database.connection import (
|
| 10 |
DatabaseConnectionError,
|
| 11 |
-
QueryExecutionError,
|
| 12 |
load_data,
|
| 13 |
)
|
| 14 |
from src.database.queries import get_players_by_full_names, search_player_by_name
|
|
@@ -64,12 +63,8 @@ def find_player(search_term: str) -> list[str]:
|
|
| 64 |
results = search_player_by_name(data, validated_term)
|
| 65 |
return [player[0] for player in results]
|
| 66 |
except DatabaseConnectionError as e:
|
| 67 |
-
st.error("Could not
|
| 68 |
-
logger.error("
|
| 69 |
-
return []
|
| 70 |
-
except QueryExecutionError as e:
|
| 71 |
-
st.error("Error searching for players. Please try again.")
|
| 72 |
-
logger.error("Query error: %s", e)
|
| 73 |
return []
|
| 74 |
|
| 75 |
|
|
@@ -92,12 +87,8 @@ def find_home_team() -> pd.DataFrame:
|
|
| 92 |
st.session_state.home_team_df = df
|
| 93 |
return df
|
| 94 |
except DatabaseConnectionError as e:
|
| 95 |
-
st.error("Could not
|
| 96 |
-
logger.error("
|
| 97 |
-
return pd.DataFrame(columns=PLAYER_COLUMNS)
|
| 98 |
-
except QueryExecutionError as e:
|
| 99 |
-
st.error("Error loading team data. Please try again.")
|
| 100 |
-
logger.error("Query error: %s", e)
|
| 101 |
return pd.DataFrame(columns=PLAYER_COLUMNS)
|
| 102 |
|
| 103 |
|
|
|
|
| 8 |
from src.config import DIFFICULTY_PRESETS, PLAYER_COLUMNS, configure_page
|
| 9 |
from src.database.connection import (
|
| 10 |
DatabaseConnectionError,
|
|
|
|
| 11 |
load_data,
|
| 12 |
)
|
| 13 |
from src.database.queries import get_players_by_full_names, search_player_by_name
|
|
|
|
| 63 |
results = search_player_by_name(data, validated_term)
|
| 64 |
return [player[0] for player in results]
|
| 65 |
except DatabaseConnectionError as e:
|
| 66 |
+
st.error("Could not load player data. Please try again later.")
|
| 67 |
+
logger.error("Data load error: %s", e)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
return []
|
| 69 |
|
| 70 |
|
|
|
|
| 87 |
st.session_state.home_team_df = df
|
| 88 |
return df
|
| 89 |
except DatabaseConnectionError as e:
|
| 90 |
+
st.error("Could not load player data. Please try again later.")
|
| 91 |
+
logger.error("Data load error: %s", e)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
return pd.DataFrame(columns=PLAYER_COLUMNS)
|
| 93 |
|
| 94 |
|
|
@@ -25,7 +25,6 @@ from src.database.queries import get_away_team_by_stats
|
|
| 25 |
from src.ml.model import (
|
| 26 |
ModelLoadError,
|
| 27 |
analyze_team_stats,
|
| 28 |
-
get_winner_model,
|
| 29 |
predict_winner,
|
| 30 |
)
|
| 31 |
from src.state.session import get_away_stats, get_home_team_df, init_session_state
|
|
@@ -41,11 +40,6 @@ def _load_nba_data() -> pd.DataFrame:
|
|
| 41 |
return load_data()
|
| 42 |
|
| 43 |
|
| 44 |
-
@st.cache_resource
|
| 45 |
-
def _get_model(): # type: ignore[no-untyped-def]
|
| 46 |
-
return get_winner_model()
|
| 47 |
-
|
| 48 |
-
|
| 49 |
# Initialize session state BEFORE any access
|
| 50 |
init_session_state()
|
| 51 |
|
|
|
|
| 25 |
from src.ml.model import (
|
| 26 |
ModelLoadError,
|
| 27 |
analyze_team_stats,
|
|
|
|
| 28 |
predict_winner,
|
| 29 |
)
|
| 30 |
from src.state.session import get_away_stats, get_home_team_df, init_session_state
|
|
|
|
| 40 |
return load_data()
|
| 41 |
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
# Initialize session state BEFORE any access
|
| 44 |
init_session_state()
|
| 45 |
|
|
@@ -1,3 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
[project]
|
| 2 |
name = "streamlit-nba"
|
| 3 |
version = "1.1.0"
|
|
@@ -21,6 +25,10 @@ dev = [
|
|
| 21 |
"pandas-stubs>=2.0.0",
|
| 22 |
"pre-commit>=3.0.0",
|
| 23 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
[tool.mypy]
|
| 26 |
python_version = "3.11"
|
|
@@ -75,7 +83,7 @@ select = [
|
|
| 75 |
ignore = [
|
| 76 |
"S101", # assert used (ok in tests)
|
| 77 |
"PLR0913", # too many arguments
|
| 78 |
-
"SIM105", # prefer explicit try-except over contextlib.suppress
|
| 79 |
"PLR2004", # magic numbers ok in game logic
|
| 80 |
"S311", # standard pseudo-random generators ok for game logic
|
| 81 |
"E501", # line length (handled by formatter or ignored)
|
|
@@ -83,6 +91,7 @@ ignore = [
|
|
| 83 |
|
| 84 |
[tool.ruff.lint.per-file-ignores]
|
| 85 |
"tests/*" = ["S101", "ARG001", "ARG002", "PLR2004", "PLC0415"]
|
|
|
|
| 86 |
|
| 87 |
[tool.pytest.ini_options]
|
| 88 |
testpaths = ["tests"]
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=68.0"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
[project]
|
| 6 |
name = "streamlit-nba"
|
| 7 |
version = "1.1.0"
|
|
|
|
| 25 |
"pandas-stubs>=2.0.0",
|
| 26 |
"pre-commit>=3.0.0",
|
| 27 |
]
|
| 28 |
+
train = [
|
| 29 |
+
"scikit-learn>=1.3.0",
|
| 30 |
+
"scikeras>=0.12.0",
|
| 31 |
+
]
|
| 32 |
|
| 33 |
[tool.mypy]
|
| 34 |
python_version = "3.11"
|
|
|
|
| 83 |
ignore = [
|
| 84 |
"S101", # assert used (ok in tests)
|
| 85 |
"PLR0913", # too many arguments
|
| 86 |
+
"SIM105", # prefer explicit try-except over contextlib.suppress
|
| 87 |
"PLR2004", # magic numbers ok in game logic
|
| 88 |
"S311", # standard pseudo-random generators ok for game logic
|
| 89 |
"E501", # line length (handled by formatter or ignored)
|
|
|
|
| 91 |
|
| 92 |
[tool.ruff.lint.per-file-ignores]
|
| 93 |
"tests/*" = ["S101", "ARG001", "ARG002", "PLR2004", "PLC0415"]
|
| 94 |
+
"src/config.py" = ["PLC0415"] # lazy import of streamlit in configure_page()
|
| 95 |
|
| 96 |
[tool.pytest.ini_options]
|
| 97 |
testpaths = ["tests"]
|
|
@@ -95,7 +95,7 @@ def create_stats(roster: pd.DataFrame, schedule: pd.DataFrame) -> list[np.ndarra
|
|
| 95 |
|
| 96 |
# Combine home and away stats for each game
|
| 97 |
for i in range(len(home_stats)):
|
| 98 |
-
arr: list[float] = []
|
| 99 |
|
| 100 |
for row in home_stats[i]:
|
| 101 |
arr.extend(row[1:]) # Skip team name column
|
|
|
|
| 95 |
|
| 96 |
# Combine home and away stats for each game
|
| 97 |
for i in range(len(home_stats)):
|
| 98 |
+
arr: list[str | float] = []
|
| 99 |
|
| 100 |
for row in home_stats[i]:
|
| 101 |
arr.extend(row[1:]) # Skip team name column
|
|
@@ -3,8 +3,6 @@
|
|
| 3 |
import logging
|
| 4 |
from typing import Final
|
| 5 |
|
| 6 |
-
import streamlit as st
|
| 7 |
-
|
| 8 |
# Database column names for player data
|
| 9 |
PLAYER_COLUMNS: Final[list[str]] = [
|
| 10 |
"FULL_NAME",
|
|
@@ -93,5 +91,7 @@ def setup_logging(level: int = logging.INFO) -> logging.Logger:
|
|
| 93 |
|
| 94 |
def configure_page() -> None:
|
| 95 |
"""Configure Streamlit page settings and logging."""
|
|
|
|
|
|
|
| 96 |
setup_logging()
|
| 97 |
st.set_page_config(layout="wide")
|
|
|
|
| 3 |
import logging
|
| 4 |
from typing import Final
|
| 5 |
|
|
|
|
|
|
|
| 6 |
# Database column names for player data
|
| 7 |
PLAYER_COLUMNS: Final[list[str]] = [
|
| 8 |
"FULL_NAME",
|
|
|
|
| 91 |
|
| 92 |
def configure_page() -> None:
|
| 93 |
"""Configure Streamlit page settings and logging."""
|
| 94 |
+
import streamlit as st
|
| 95 |
+
|
| 96 |
setup_logging()
|
| 97 |
st.set_page_config(layout="wide")
|