HatmanStack commited on
Commit
56bab24
·
1 Parent(s): fd53f23

ci(hooks): add pre-commit config with ruff and mypy hooks

Browse files
.pre-commit-config.yaml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.15.7
4
+ hooks:
5
+ - id: ruff
6
+ args: [--fix]
7
+ - id: ruff-format
8
+ - repo: https://github.com/pre-commit/mirrors-mypy
9
+ rev: v1.19.1
10
+ hooks:
11
+ - id: mypy
12
+ additional_dependencies: [pandas-stubs, pydantic>=2.5.0]
13
+ args: [--config-file=pyproject.toml]
14
+ pass_filenames: false
15
+ entry: mypy src/
app.py CHANGED
@@ -12,5 +12,3 @@ safe_paragraph(
12
  "career stats to compete with a Computer",
13
  color="white",
14
  )
15
-
16
-
 
12
  "career stats to compete with a Computer",
13
  color="white",
14
  )
 
 
docs/plans/2026-03-25-audit-streamlit-nba/Phase-4.md CHANGED
@@ -92,9 +92,9 @@ ci(deps): consolidate to pyproject.toml, remove requirements files
92
  - Note: Use the latest stable versions of ruff and mypy that are compatible. Check PyPI for current versions.
93
 
94
  **Verification Checklist:**
95
- - [ ] `.pre-commit-config.yaml` exists at project root
96
- - [ ] `pre-commit` is in dev dependencies
97
- - [ ] `uvx pre-commit run --all-files` passes
98
 
99
  **Testing Instructions:** Run `uvx pre-commit run --all-files` and verify all hooks pass.
100
 
 
92
  - Note: Use the latest stable versions of ruff and mypy that are compatible. Check PyPI for current versions.
93
 
94
  **Verification Checklist:**
95
+ - [x] `.pre-commit-config.yaml` exists at project root
96
+ - [x] `pre-commit` is in dev dependencies
97
+ - [x] `uvx pre-commit run --all-files` passes
98
 
99
  **Testing Instructions:** Run `uvx pre-commit run --all-files` and verify all hooks pass.
100
 
pages/1_home_team.py CHANGED
@@ -25,6 +25,7 @@ configure_page()
25
  def _load_nba_data() -> pd.DataFrame:
26
  return load_data()
27
 
 
28
  # Initialize session state before any access
29
  init_session_state()
30
 
@@ -53,7 +54,9 @@ def find_player(search_term: str) -> list[str]:
53
  # Validate input
54
  validated_term = validate_search_term(search_term)
55
  if validated_term is None:
56
- st.warning("Invalid search term. Please use only letters, numbers, and basic punctuation.")
 
 
57
  return []
58
 
59
  try:
@@ -104,7 +107,9 @@ home_team_df = find_home_team()
104
 
105
  # Combine search results with current team and current unsaved selections
106
  # This ensures that selections don't disappear when the search term changes
107
- current_team_names = home_team_df["FULL_NAME"].tolist() if not home_team_df.empty else []
 
 
108
  current_selections = st.session_state.get("player_selector", [])
109
 
110
  # Merge all into options list, maintaining uniqueness
@@ -125,7 +130,9 @@ def save_state() -> None:
125
 
126
  col1, col2 = st.columns([7, 1])
127
  with col1:
128
- default_selection = home_team_df["FULL_NAME"].tolist() if not home_team_df.empty else []
 
 
129
  player_selected = st.multiselect(
130
  "Search Results:",
131
  player_search,
 
25
  def _load_nba_data() -> pd.DataFrame:
26
  return load_data()
27
 
28
+
29
  # Initialize session state before any access
30
  init_session_state()
31
 
 
54
  # Validate input
55
  validated_term = validate_search_term(search_term)
56
  if validated_term is None:
57
+ st.warning(
58
+ "Invalid search term. Please use only letters, numbers, and basic punctuation."
59
+ )
60
  return []
61
 
62
  try:
 
107
 
108
  # Combine search results with current team and current unsaved selections
109
  # This ensures that selections don't disappear when the search term changes
110
+ current_team_names = (
111
+ home_team_df["FULL_NAME"].tolist() if not home_team_df.empty else []
112
+ )
113
  current_selections = st.session_state.get("player_selector", [])
114
 
115
  # Merge all into options list, maintaining uniqueness
 
130
 
131
  col1, col2 = st.columns([7, 1])
132
  with col1:
133
+ default_selection = (
134
+ home_team_df["FULL_NAME"].tolist() if not home_team_df.empty else []
135
+ )
136
  player_selected = st.multiselect(
137
  "Search Results:",
138
  player_search,
pages/2_play_game.py CHANGED
@@ -45,6 +45,7 @@ def _load_nba_data() -> pd.DataFrame:
45
  def _get_model(): # type: ignore[no-untyped-def]
46
  return get_winner_model()
47
 
 
48
  # Initialize session state BEFORE any access
49
  init_session_state()
50
 
@@ -135,7 +136,10 @@ if home_team_df.empty or home_team_df.shape[0] != TEAM_SIZE:
135
  box_score = pd.DataFrame()
136
  else:
137
  # Only generate away team if we don't have one or it's empty
138
- if st.session_state.get("away_team_df") is None or st.session_state.away_team_df.empty:
 
 
 
139
  st.session_state.away_team_df = find_away_team(stats)
140
 
141
  away_data = st.session_state.away_team_df
@@ -207,9 +211,11 @@ if teams_good and winner_label:
207
  safe_heading("Away Team", level=1, color="steelblue")
208
  st.dataframe(st.session_state.away_team_df)
209
 
 
210
  def play_new_team() -> None:
211
  """Clear cached away team and rerun."""
212
  logger.info("New Team requested")
213
  st.session_state.away_team_df = pd.DataFrame()
214
 
 
215
  st.button("Play New Team", on_click=play_new_team)
 
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
 
 
136
  box_score = pd.DataFrame()
137
  else:
138
  # Only generate away team if we don't have one or it's empty
139
+ if (
140
+ st.session_state.get("away_team_df") is None
141
+ or st.session_state.away_team_df.empty
142
+ ):
143
  st.session_state.away_team_df = find_away_team(stats)
144
 
145
  away_data = st.session_state.away_team_df
 
211
  safe_heading("Away Team", level=1, color="steelblue")
212
  st.dataframe(st.session_state.away_team_df)
213
 
214
+
215
  def play_new_team() -> None:
216
  """Clear cached away team and rerun."""
217
  logger.info("New Team requested")
218
  st.session_state.away_team_df = pd.DataFrame()
219
 
220
+
221
  st.button("Play New Team", on_click=play_new_team)
pyproject.toml CHANGED
@@ -19,6 +19,7 @@ dev = [
19
  "mypy>=1.7.0",
20
  "ruff>=0.1.6",
21
  "pandas-stubs>=2.0.0",
 
22
  ]
23
 
24
  [tool.mypy]
 
19
  "mypy>=1.7.0",
20
  "ruff>=0.1.6",
21
  "pandas-stubs>=2.0.0",
22
+ "pre-commit>=3.0.0",
23
  ]
24
 
25
  [tool.mypy]
scripts/compile_model.py CHANGED
@@ -70,9 +70,7 @@ EPOCHS: list[int] = [500, 1000, 1500]
70
  BATCH_SIZES: list[int] = [50, 100, 200]
71
 
72
 
73
- def create_stats(
74
- roster: pd.DataFrame, schedule: pd.DataFrame
75
- ) -> list[np.ndarray]:
76
  """Create feature arrays from roster and schedule data.
77
 
78
  Args:
 
70
  BATCH_SIZES: list[int] = [50, 100, 200]
71
 
72
 
73
+ def create_stats(roster: pd.DataFrame, schedule: pd.DataFrame) -> list[np.ndarray]:
 
 
74
  """Create feature arrays from roster and schedule data.
75
 
76
  Args:
src/config.py CHANGED
@@ -91,7 +91,6 @@ def setup_logging(level: int = logging.INFO) -> logging.Logger:
91
  return logger
92
 
93
 
94
-
95
  def configure_page() -> None:
96
  """Configure Streamlit page settings and logging."""
97
  setup_logging()
 
91
  return logger
92
 
93
 
 
94
  def configure_page() -> None:
95
  """Configure Streamlit page settings and logging."""
96
  setup_logging()
src/database/queries.py CHANGED
@@ -30,9 +30,7 @@ def search_player_by_name(df: pd.DataFrame, name: str) -> list[tuple[str]]:
30
  return [(player_name,) for player_name in results]
31
 
32
 
33
- def get_players_by_full_names(
34
- df: pd.DataFrame, names: list[str]
35
- ) -> pd.DataFrame:
36
  """Get multiple players' records in a single batch query.
37
 
38
  Args:
 
30
  return [(player_name,) for player_name in results]
31
 
32
 
33
+ def get_players_by_full_names(df: pd.DataFrame, names: list[str]) -> pd.DataFrame:
 
 
34
  """Get multiple players' records in a single batch query.
35
 
36
  Args:
src/ml/model.py CHANGED
@@ -64,9 +64,7 @@ def predict_winner(combined_stats: np.ndarray) -> tuple[float, int]:
64
  ValueError: If input shape is invalid
65
  """
66
  if combined_stats.shape != (1, 100):
67
- raise ValueError(
68
- f"Expected input shape (1, 100), got {combined_stats.shape}"
69
- )
70
 
71
  model = get_winner_model()
72
  sigmoid_output = model.predict(combined_stats, verbose=0)
 
64
  ValueError: If input shape is invalid
65
  """
66
  if combined_stats.shape != (1, 100):
67
+ raise ValueError(f"Expected input shape (1, 100), got {combined_stats.shape}")
 
 
68
 
69
  model = get_winner_model()
70
  sigmoid_output = model.predict(combined_stats, verbose=0)
src/utils/html.py CHANGED
@@ -64,7 +64,6 @@ def safe_paragraph(
64
  safe_align = escape_html(align)
65
 
66
  st.markdown(
67
- f"<p style='text-align: {safe_align}; color: {safe_color};'>"
68
- f"{safe_text}</p>",
69
  unsafe_allow_html=True,
70
  )
 
64
  safe_align = escape_html(align)
65
 
66
  st.markdown(
67
+ f"<p style='text-align: {safe_align}; color: {safe_color};'>{safe_text}</p>",
 
68
  unsafe_allow_html=True,
69
  )
tests/test_database.py CHANGED
@@ -47,7 +47,9 @@ class TestLoadData:
47
  @patch("src.database.connection.pd.read_csv")
48
  @patch("src.database.connection.CSV_PATH")
49
  def test_load_data_parser_error_raises_connection_error(
50
- self, mock_path, mock_read_csv # type: ignore[no-untyped-def]
 
 
51
  ) -> None:
52
  """Test that CSV parse errors raise DatabaseConnectionError."""
53
  mock_path.exists.return_value = True
@@ -120,10 +122,12 @@ class TestGetAwayTeamByStats:
120
  def test_max_attempts_raises_error(self) -> None:
121
  """Test that max_attempts limit works when population is too small."""
122
  # Create a DF with only 2 players
123
- df = pd.DataFrame([
124
- {"FULL_NAME": "P1", "PTS": 1001, "REB": 501, "AST": 301, "STL": 101},
125
- {"FULL_NAME": "P2", "PTS": 1001, "REB": 501, "AST": 301, "STL": 101},
126
- ])
 
 
127
  # Add missing columns to avoid errors if needed, though queries only use these
128
  for col in PLAYER_COLUMNS:
129
  if col not in df.columns:
@@ -146,10 +150,15 @@ class TestGetAwayTeamByStats:
146
  # Create a DF with 10 players meeting criteria
147
  data = []
148
  for i in range(10):
149
- data.append({
150
- "FULL_NAME": f"Player{i}",
151
- "PTS": 2000, "REB": 1000, "AST": 500, "STL": 200
152
- })
 
 
 
 
 
153
  df = pd.DataFrame(data)
154
  for col in PLAYER_COLUMNS:
155
  if col not in df.columns:
 
47
  @patch("src.database.connection.pd.read_csv")
48
  @patch("src.database.connection.CSV_PATH")
49
  def test_load_data_parser_error_raises_connection_error(
50
+ self,
51
+ mock_path,
52
+ mock_read_csv, # type: ignore[no-untyped-def]
53
  ) -> None:
54
  """Test that CSV parse errors raise DatabaseConnectionError."""
55
  mock_path.exists.return_value = True
 
122
  def test_max_attempts_raises_error(self) -> None:
123
  """Test that max_attempts limit works when population is too small."""
124
  # Create a DF with only 2 players
125
+ df = pd.DataFrame(
126
+ [
127
+ {"FULL_NAME": "P1", "PTS": 1001, "REB": 501, "AST": 301, "STL": 101},
128
+ {"FULL_NAME": "P2", "PTS": 1001, "REB": 501, "AST": 301, "STL": 101},
129
+ ]
130
+ )
131
  # Add missing columns to avoid errors if needed, though queries only use these
132
  for col in PLAYER_COLUMNS:
133
  if col not in df.columns:
 
150
  # Create a DF with 10 players meeting criteria
151
  data = []
152
  for i in range(10):
153
+ data.append(
154
+ {
155
+ "FULL_NAME": f"Player{i}",
156
+ "PTS": 2000,
157
+ "REB": 1000,
158
+ "AST": 500,
159
+ "STL": 200,
160
+ }
161
+ )
162
  df = pd.DataFrame(data)
163
  for col in PLAYER_COLUMNS:
164
  if col not in df.columns:
tests/test_ml.py CHANGED
@@ -32,9 +32,7 @@ class TestAnalyzeTeamStats:
32
  home_stats = [[float(i * 10 + j) for j in range(10)] for i in range(5)]
33
  away_stats = [[float(50 + i * 10 + j) for j in range(10)] for i in range(5)]
34
 
35
- _home_array, _away_array, combined = analyze_team_stats(
36
- home_stats, away_stats
37
- )
38
 
39
  # Combined should have 100 values: 50 home + 50 away
40
  assert combined.shape == (1, 100)
@@ -100,9 +98,7 @@ class TestPredictWinner:
100
  assert prediction in (0, 1)
101
 
102
  @patch("src.ml.model.get_winner_model")
103
- def test_high_probability_predicts_win(
104
- self, mock_get_model: MagicMock
105
- ) -> None:
106
  """Test that high probability (>0.5) predicts home win (1)."""
107
  mock_model = MagicMock()
108
  mock_model.predict.return_value = np.array([[0.8]])
@@ -115,9 +111,7 @@ class TestPredictWinner:
115
  assert prediction == 1
116
 
117
  @patch("src.ml.model.get_winner_model")
118
- def test_low_probability_predicts_loss(
119
- self, mock_get_model: MagicMock
120
- ) -> None:
121
  """Test that low probability (<0.5) predicts home loss (0)."""
122
  mock_model = MagicMock()
123
  mock_model.predict.return_value = np.array([[0.3]])
@@ -130,9 +124,7 @@ class TestPredictWinner:
130
  assert prediction == 0
131
 
132
  @patch("src.ml.model.get_winner_model")
133
- def test_invalid_shape_raises_error(
134
- self, mock_get_model: MagicMock
135
- ) -> None:
136
  """Test that invalid input shape raises ValueError."""
137
  mock_model = MagicMock()
138
  mock_get_model.return_value = mock_model
@@ -146,9 +138,7 @@ class TestPredictWinner:
146
  assert "Expected input shape (1, 100)" in str(exc_info.value)
147
 
148
  @patch("src.ml.model.get_winner_model")
149
- def test_model_called_with_verbose_zero(
150
- self, mock_get_model: MagicMock
151
- ) -> None:
152
  """Test that model.predict is called with verbose=0."""
153
  mock_model = MagicMock()
154
  mock_model.predict.return_value = np.array([[0.5]])
 
32
  home_stats = [[float(i * 10 + j) for j in range(10)] for i in range(5)]
33
  away_stats = [[float(50 + i * 10 + j) for j in range(10)] for i in range(5)]
34
 
35
+ _home_array, _away_array, combined = analyze_team_stats(home_stats, away_stats)
 
 
36
 
37
  # Combined should have 100 values: 50 home + 50 away
38
  assert combined.shape == (1, 100)
 
98
  assert prediction in (0, 1)
99
 
100
  @patch("src.ml.model.get_winner_model")
101
+ def test_high_probability_predicts_win(self, mock_get_model: MagicMock) -> None:
 
 
102
  """Test that high probability (>0.5) predicts home win (1)."""
103
  mock_model = MagicMock()
104
  mock_model.predict.return_value = np.array([[0.8]])
 
111
  assert prediction == 1
112
 
113
  @patch("src.ml.model.get_winner_model")
114
+ def test_low_probability_predicts_loss(self, mock_get_model: MagicMock) -> None:
 
 
115
  """Test that low probability (<0.5) predicts home loss (0)."""
116
  mock_model = MagicMock()
117
  mock_model.predict.return_value = np.array([[0.3]])
 
124
  assert prediction == 0
125
 
126
  @patch("src.ml.model.get_winner_model")
127
+ def test_invalid_shape_raises_error(self, mock_get_model: MagicMock) -> None:
 
 
128
  """Test that invalid input shape raises ValueError."""
129
  mock_model = MagicMock()
130
  mock_get_model.return_value = mock_model
 
138
  assert "Expected input shape (1, 100)" in str(exc_info.value)
139
 
140
  @patch("src.ml.model.get_winner_model")
141
+ def test_model_called_with_verbose_zero(self, mock_get_model: MagicMock) -> None:
 
 
142
  """Test that model.predict is called with verbose=0."""
143
  mock_model = MagicMock()
144
  mock_model.predict.return_value = np.array([[0.5]])