|
8 | 8 | from pathlib import Path |
9 | 9 | from io import BytesIO |
10 | 10 | from typing import Dict, Any |
| 11 | +import logging |
| 12 | +import requests |
11 | 13 |
|
12 | 14 | import pandas as pd |
13 | 15 | import pytest |
@@ -168,6 +170,21 @@ def test_make_request_all_failures_returns_none(self, client: BPAClient): |
168 | 170 | # should have tried max_retries times |
169 | 171 | assert client.session.get.call_count == client.config.max_retries |
170 | 172 |
|
| 173 | + def test_make_request_handles_request_exception(self, client, caplog): |
| 174 | + client.config.max_retries = 2 |
| 175 | + |
| 176 | + # avoid real sleep between retries |
| 177 | + with patch("time.sleep", autospec=True) as _sleep: |
| 178 | + client.session.get = Mock(side_effect=requests.RequestException("boom")) # type: ignore[assignment] |
| 179 | + |
| 180 | + caplog.set_level(logging.ERROR) |
| 181 | + content = client._make_request("https://example.com/file.xlsx") |
| 182 | + |
| 183 | + assert content is None |
| 184 | + assert "Request error:" in caplog.text |
| 185 | + assert client.session.get.call_count == 2 |
| 186 | + _sleep.assert_called_once() # since retries=2 => one sleep between attempts |
| 187 | + |
171 | 188 |
|
172 | 189 | # --------------------------------------------------------------------------- |
173 | 190 | # Excel parsing tests |
@@ -216,6 +233,21 @@ def test_parse_excel_file_failure_returns_none(self, client: BPAClient): |
216 | 233 | df = client._parse_excel_file(b"not-an-excel-file", BPADataType.WIND_GEN_TOTAL_LOAD) |
217 | 234 | assert df is None |
218 | 235 |
|
| 236 | + def test_parse_excel_file_datetime_parse_exception_logs_warning(self, client, caplog): |
| 237 | + # Build a simple df that _parse_excel_file will produce after read_excel |
| 238 | + df = pd.DataFrame({"Date": ["2024-01-01"], "Time": ["00:05"], "Value": [1.0]}) |
| 239 | + |
| 240 | + caplog.set_level(logging.WARNING) |
| 241 | + |
| 242 | + with ( |
| 243 | + patch("lib.iso.bpa.pd.read_excel", return_value=df), |
| 244 | + patch("lib.iso.bpa.pd.to_datetime", side_effect=Exception("bad dt")), |
| 245 | + ): |
| 246 | + out = client._parse_excel_file(b"fake-excel-bytes", data_type=None) # type: ignore[arg-type] |
| 247 | + |
| 248 | + assert isinstance(out, pd.DataFrame) |
| 249 | + assert "Could not parse datetime column" in caplog.text |
| 250 | + |
219 | 251 |
|
220 | 252 | # --------------------------------------------------------------------------- |
221 | 253 | # Date-range filtering tests |
@@ -319,6 +351,40 @@ def test_request_failure_returns_false( |
319 | 351 | output_file = client.config.data_dir / "2024_BPA_WindGenTotalLoad.csv" |
320 | 352 | assert not output_file.exists() |
321 | 353 |
|
| 354 | + @patch.object(BPAClient, "_make_request", return_value=b"excel-content") |
| 355 | + @patch.object(BPAClient, "_parse_excel_file", return_value=pd.DataFrame()) |
| 356 | + def test_wind_parse_returns_empty_df_returns_false( |
| 357 | + self, mock_parse, mock_req, client, tmp_path |
| 358 | + ): |
| 359 | + client.config.data_dir = tmp_path |
| 360 | + assert client.get_wind_gen_total_load(2024) is False |
| 361 | + |
| 362 | + @patch.object(BPAClient, "_make_request", return_value=b"excel-content") |
| 363 | + @patch.object(BPAClient, "_parse_excel_file", return_value=None) |
| 364 | + def test_wind_parse_returns_none_returns_false(self, mock_parse, mock_req, client, tmp_path): |
| 365 | + client.config.data_dir = tmp_path |
| 366 | + assert client.get_wind_gen_total_load(2024) is False |
| 367 | + |
| 368 | + @patch.object(BPAClient, "_make_request", return_value=b"excel-content") |
| 369 | + @patch.object(BPAClient, "_parse_excel_file") |
| 370 | + @patch.object(BPAClient, "_filter_by_date_range", return_value=pd.DataFrame()) |
| 371 | + def test_wind_date_filter_makes_empty_returns_false( |
| 372 | + self, mock_filter, mock_parse, mock_req, client, tmp_path |
| 373 | + ): |
| 374 | + client.config.data_dir = tmp_path |
| 375 | + mock_parse.return_value = pd.DataFrame( |
| 376 | + {"DateTime": pd.date_range("2024-01-01", periods=3, freq="h"), "Value": [1, 2, 3]} |
| 377 | + ) |
| 378 | + |
| 379 | + assert client.get_wind_gen_total_load(2024, start_date=date(2024, 1, 2)) is False |
| 380 | + |
| 381 | + @patch.object(BPAClient, "_make_request", return_value=b"excel-content") |
| 382 | + @patch.object(BPAClient, "_parse_excel_file", side_effect=RuntimeError("boom")) |
| 383 | + def test_wind_processing_exception_returns_false(self, mock_parse, mock_req, client, caplog): |
| 384 | + caplog.set_level(logging.ERROR) |
| 385 | + assert client.get_wind_gen_total_load(2024) is False |
| 386 | + assert "Error processing data:" in caplog.text |
| 387 | + |
322 | 388 |
|
323 | 389 | class TestReservesDeployed: |
324 | 390 | @patch.object(BPAClient, "_make_request") |
@@ -361,6 +427,34 @@ def test_request_failure_returns_false( |
361 | 427 | output_file = client.config.data_dir / "2024_BPA_Reserves_Deployed.csv" |
362 | 428 | assert not output_file.exists() |
363 | 429 |
|
| 430 | + @patch.object(BPAClient, "_make_request", return_value=b"excel-content") |
| 431 | + @patch.object(BPAClient, "_parse_excel_file", return_value=pd.DataFrame()) |
| 432 | + def test_reserves_parse_empty_returns_false(self, mock_parse, mock_req, client, tmp_path): |
| 433 | + client.config.data_dir = tmp_path |
| 434 | + assert client.get_reserves_deployed(2024) is False |
| 435 | + |
| 436 | + @patch.object(BPAClient, "_make_request", return_value=b"excel-content") |
| 437 | + @patch.object(BPAClient, "_parse_excel_file") |
| 438 | + @patch.object(BPAClient, "_filter_by_date_range", return_value=pd.DataFrame()) |
| 439 | + def test_reserves_date_filter_makes_empty_returns_false( |
| 440 | + self, mock_filter, mock_parse, mock_req, client, tmp_path |
| 441 | + ): |
| 442 | + client.config.data_dir = tmp_path |
| 443 | + mock_parse.return_value = pd.DataFrame( |
| 444 | + {"DateTime": pd.date_range("2024-01-01", periods=3, freq="h"), "Value": [1, 2, 3]} |
| 445 | + ) |
| 446 | + |
| 447 | + assert client.get_reserves_deployed(2024, start_date=date(2024, 1, 2)) is False |
| 448 | + |
| 449 | + @patch.object(BPAClient, "_make_request", return_value=b"excel-content") |
| 450 | + @patch.object(BPAClient, "_parse_excel_file", side_effect=RuntimeError("boom")) |
| 451 | + def test_reserves_processing_exception_returns_false( |
| 452 | + self, mock_parse, mock_req, client, caplog |
| 453 | + ): |
| 454 | + caplog.set_level(logging.ERROR) |
| 455 | + assert client.get_reserves_deployed(2024) is False |
| 456 | + assert "Error processing data:" in caplog.text |
| 457 | + |
364 | 458 |
|
365 | 459 | class TestGetAllData: |
366 | 460 | def test_get_all_data_combines_results(self, client: BPAClient): |
|
0 commit comments