Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions src/isp_workbook_parser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@

import pandas as pd

from .config_model import TableConfig as TableConfig
from .config_model import load_yaml as load_yaml
from .config_model import TableConfig, load_yaml
from .parser import Parser
from .read_table import read_table as read_table
from .read_table import read_table

__all__ = ["Parser", "TableConfig", "load_yaml", "read_table"]

pd.set_option("future.no_silent_downcasting", True)
pd.set_option("future.no_silent_downcasting", True) # noqa: FBT003
11 changes: 6 additions & 5 deletions src/isp_workbook_parser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def _determine_config_path(

def _check_version_is_supported(self, config_path: Path) -> None:
"""Check the default config directory contains a subdirectory that matches the workbook version number."""
versions = [dir.name for dir in config_path.iterdir()]
versions = [directory.name for directory in config_path.iterdir()]
if self.workbook_version not in versions:
msg = f"The workbook version {self.workbook_version} is not supported."
raise ValueError(msg)
Expand Down Expand Up @@ -509,7 +509,7 @@ def _postprocess_percentage_columns_between_0_and_100(
if isinstance(sr, int) and cell.row == sr:
skipped_rows += 1
continue
if isinstance(cell.value, (int, float)) and "%" in cell.number_format:
if isinstance(cell.value, int | float) and "%" in cell.number_format:
percentage_cells.append(
(
cell.row - min_row - skipped_rows,
Expand Down Expand Up @@ -553,7 +553,7 @@ def get_table_names(self) -> list[str]:
return self.table_names_by_sheet

def get_table_from_config(
self, table_config: TableConfig, config_checks: bool = True
self, table_config: TableConfig, *, config_checks: bool = True
) -> pd.DataFrame:
"""Retrieve a table from the assumptions workbook using the config provided and returns as pd.DataFrame.

Expand Down Expand Up @@ -603,7 +603,7 @@ def get_table_from_config(
self._check_table(data, table_config)
return data

def get_table(self, table_name: str, config_checks: bool = True) -> pd.DataFrame:
def get_table(self, table_name: str, *, config_checks: bool = True) -> pd.DataFrame:
"""Retrieve a table from the assumptions workbook and returns as `pd.DataFrame`.

Examples:
Expand Down Expand Up @@ -643,6 +643,7 @@ def save_tables(
self,
directory: str | Path,
tables: list[str] | str = "all",
*,
config_checks: bool = True,
) -> None:
"""Save tables from the provided workbook to the specified directory as CSV files.
Expand Down Expand Up @@ -672,7 +673,7 @@ def save_tables(
msg = "The path provided is not a directory."
raise ValueError(msg)

if not (isinstance(tables, (str, list))):
if not (isinstance(tables, str | list)):
msg = "The parameter tables must be provided as str or list[str]."
raise TypeError(msg)

Expand Down
6 changes: 5 additions & 1 deletion src/isp_workbook_parser/read_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,10 @@ def read_table(workbook_file: pd.ExcelFile, table: TableConfig) -> pd.DataFrame:
series[series != ""] = "_" + series[series != ""]
merged_headers = ffilled_initial_header.str.cat(filled_headers)
df_cleaned = _build_cleaned_dataframe(
df_initial, header_rows_in_table, merged_headers, table.forward_fill_values
df_initial,
header_rows_in_table,
merged_headers,
forward_fill_values=table.forward_fill_values,
)
if table.skip_rows:
df_cleaned = _skip_rows_in_dataframe(
Expand Down Expand Up @@ -202,6 +205,7 @@ def _build_cleaned_dataframe(
df_initial: pd.DataFrame,
header_rows_in_table: int,
new_headers: pd.Series,
*,
forward_fill_values: bool,
) -> pd.DataFrame:
"""
Expand Down
2 changes: 1 addition & 1 deletion src/isp_workbook_parser/sanitisers.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def _values_casting_and_sanitisation(df: pd.DataFrame) -> pd.DataFrame:
will return `pd.NA`
"""
df = _replace_dataframe_hyphens_with_na(df)
for object_col in df.dtypes[df.dtypes == "object"].keys():
for object_col in df.dtypes[df.dtypes == "object"].keys(): # noqa: SIM118
try:
df.loc[:, object_col] = pd.to_numeric(df[object_col])
except (ValueError, TypeError):
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def workbook_v6() -> Parser:


@pytest.fixture(scope="module")
def sample_series():
def sample_series() -> pd.Series:
return pd.Series(
[
"First line\nSecond line",
Expand Down
16 changes: 7 additions & 9 deletions tests/test_packaged_table_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@


@pytest.mark.parametrize("workbook_version_folder", list(workbook_path.iterdir()))
def test_packaged_table_configs_for_each_version(workbook_version_folder: Path):
xl_file = [file for file in workbook_version_folder.glob("[!.]*.xls*")]
def test_packaged_table_configs_for_each_version(workbook_version_folder: Path) -> None:
xl_file = list(workbook_version_folder.glob("[!.]*.xls*"))
assert len(xl_file) == 1, (
f"There should only be one Excel workbook in each version sub-directory, got {xl_file}"
)
Expand All @@ -34,9 +34,7 @@ def test_packaged_table_configs_for_each_version(workbook_version_folder: Path):
for index, value in enumerate(sheet_header_end_row_combos):
if sheet_header_end_row_combos.count(value) > 1:
duplicate_configs.append(table_names[index])
if len(duplicate_configs) > 0:
print(duplicate_configs)
assert len(duplicate_configs) == 0
assert len(duplicate_configs) == 0, duplicate_configs

save_dir = Path(f"example_output/{workbook.workbook_version}")
save_dir.mkdir(parents=True, exist_ok=True)
Expand All @@ -46,14 +44,14 @@ def test_packaged_table_configs_for_each_version(workbook_version_folder: Path):
table = workbook.get_table(table_name)
save_path = save_dir / Path(f"{table_name}.csv")
table.to_csv(save_path, index=False)
except Exception as e:
except (FileNotFoundError, PermissionError, OSError) as e:
Comment on lines -49 to +47

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these are not necessarily the right right errors to check for? An error on get_table should return a ValueError/TypeError or TableConfigError I think (not filenotfound .. )

error_tables[table_name] = e
if error_tables:
error_str = ""
for key in error_tables:
error_str += key + ":" + str(error_tables[key]) + "\n"
for key, value in error_tables.items():
error_str += key + ":" + str(value) + "\n"
raise TableLoadError(error_str)


class TableLoadError(Exception):
"""Exception to throw if table loading fails"""
"""Exception to throw if table loading fails."""
9 changes: 5 additions & 4 deletions tests/test_read_table_functionality.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
# (at your option) any later version.

from isp_workbook_parser.config_model import TableConfig
from isp_workbook_parser.parser import Parser


def test_skip_single_row_in_single_header_row_table(workbook_v6):
def test_skip_single_row_in_single_header_row_table(workbook_v6: Parser) -> None:
table_config = TableConfig(
name="build_cost_current_policies",
sheet_name="Build costs",
Expand All @@ -22,7 +23,7 @@ def test_skip_single_row_in_single_header_row_table(workbook_v6):
assert df[df.Technology.str.contains("Hydrogen")].empty


def test_skip_multiple_rows_in_single_header_row_table(workbook_v6):
def test_skip_multiple_rows_in_single_header_row_table(workbook_v6: Parser) -> None:
table_config = TableConfig(
name="existing_generator_maintenance_rates",
sheet_name="Maintenance",
Expand All @@ -37,7 +38,7 @@ def test_skip_multiple_rows_in_single_header_row_table(workbook_v6):
assert df[df["Generator type"].str.contains("Coal")].empty


def test_skip_multiple_rows_in_multiple_header_row_table(workbook_v6):
def test_skip_multiple_rows_in_multiple_header_row_table(workbook_v6: Parser) -> None:
table_config = TableConfig(
name="wind_high_capacity_factors",
sheet_name="Capacity Factors ",
Expand All @@ -52,7 +53,7 @@ def test_skip_multiple_rows_in_multiple_header_row_table(workbook_v6):
assert df[df["Wind High_REZ ID"].str.contains("V")].empty


def test_no_forward_fill_in_rows(workbook_v6):
def test_no_forward_fill_in_rows(workbook_v6: Parser) -> None:
table_config = TableConfig(
name="outages_new_entrants",
sheet_name="Generator Reliability Settings",
Expand Down
66 changes: 38 additions & 28 deletions tests/test_sanitisers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from pathlib import Path

import pandas as pd
import pytest
from pandas import Series

from isp_workbook_parser.sanitisers import (
_extract_numeric_value_millions,
Expand All @@ -25,7 +25,7 @@
)


def test_sanitisation_on_flow_path_transfer_capability():
def test_sanitisation_on_flow_path_transfer_capability() -> None:
unsanitised = pd.read_csv(Path("tests", "test_data", "unsanitised.csv"))
expected = pd.read_csv(Path("tests", "test_data", "sanitised.csv"))
# handle carriage return on Windows
Expand All @@ -38,7 +38,7 @@ def test_sanitisation_on_flow_path_transfer_capability():
pd.testing.assert_frame_equal(test_sanitised, expected, check_dtype=False)


def test_replace_series_newlines_with_whitespace(sample_series):
def test_replace_series_newlines_with_whitespace(sample_series: Series) -> None:
result = _replace_series_newlines_with_whitespace(sample_series)
expected = pd.Series(
[
Expand All @@ -63,7 +63,7 @@ def test_replace_series_newlines_with_whitespace(sample_series):
pd.testing.assert_series_equal(result, expected)


def test_extract_numeric_value_millions(sample_series):
def test_extract_numeric_value_millions(sample_series: Series) -> None:
result = _extract_numeric_value_millions(sample_series)
expected = pd.Series(
[
Expand All @@ -88,7 +88,7 @@ def test_extract_numeric_value_millions(sample_series):
pd.testing.assert_series_equal(result, expected)


def test_remove_series_double_whitespaces(sample_series):
def test_remove_series_double_whitespaces(sample_series: Series) -> None:
result = _remove_series_double_whitespaces(sample_series)
expected = pd.Series(
[
Expand All @@ -113,7 +113,7 @@ def test_remove_series_double_whitespaces(sample_series):
pd.testing.assert_series_equal(result, expected)


def test_remove_series_trailing_asterisks(sample_series):
def test_remove_series_trailing_asterisks(sample_series: Series) -> None:
result = _remove_series_trailing_asterisks(sample_series)
expected = pd.Series(
[
Expand All @@ -138,7 +138,7 @@ def test_remove_series_trailing_asterisks(sample_series):
pd.testing.assert_series_equal(result, expected)


def test_remove_series_trailing_footnotes(sample_series):
def test_remove_series_trailing_footnotes(sample_series: Series) -> None:
result = _remove_series_trailing_footnotes(sample_series)
expected = pd.Series(
[
Expand All @@ -163,7 +163,7 @@ def test_remove_series_trailing_footnotes(sample_series):
pd.testing.assert_series_equal(result, expected)


def test_strip_series_whitespaces(sample_series):
def test_strip_series_whitespaces(sample_series: Series) -> None:
result = _strip_series_whitespaces(sample_series)
expected = pd.Series(
[
Expand All @@ -188,7 +188,7 @@ def test_strip_series_whitespaces(sample_series):
pd.testing.assert_series_equal(result, expected)


def test_remove_series_thousands_commas(sample_series):
def test_remove_series_thousands_commas(sample_series: Series) -> None:
result = _remove_series_thousands_commas(sample_series)
expected = pd.Series(
[
Expand All @@ -213,7 +213,7 @@ def test_remove_series_thousands_commas(sample_series):
pd.testing.assert_series_equal(result, expected)


def test_remove_series_notes_after_values(sample_series):
def test_remove_series_notes_after_values(sample_series: Series) -> None:
result = _remove_series_notes_after_values(sample_series)
expected = pd.Series(
[
Expand All @@ -238,19 +238,25 @@ def test_remove_series_notes_after_values(sample_series):
pd.testing.assert_series_equal(result, expected)


def test_remove_series_notes_after_values_with_special_characters():
def test_remove_series_notes_after_values_with_special_characters() -> None:
unsanitised = pd.Series(
[
"4758 (Marinus Link Pty Ltd and TasNetworks have advised that $534 million, "
"in $2023, of this amount relates to approved early works and other incurred "
"costs that should be excluded from the cost estimate for the 2026 ISP in "
"accordance with the AER's CBA Guidelines. AEMO has removed this from the "
"estimate of $5035 million in $2023, and has then adjusted to $2025.)",
"7035 (Transgrid has advised $565 million of this amount relates to approved "
"early works and other incurred costs that should be excluded from the total "
"cost estimate of $7600 million for the 2026 ISP.)",
"2431 (This figure reflects the estimate from Option 2 with a portion costed "
"at Class 5b removed.)",
(
"4758 (Marinus Link Pty Ltd and TasNetworks have advised that $534 million, "
"in $2023, of this amount relates to approved early works and other incurred "
"costs that should be excluded from the cost estimate for the 2026 ISP in "
"accordance with the AER's CBA Guidelines. AEMO has removed this from the "
"estimate of $5035 million in $2023, and has then adjusted to $2025.)"
),
(
"7035 (Transgrid has advised $565 million of this amount relates to approved "
"early works and other incurred costs that should be excluded from the total "
"cost estimate of $7600 million for the 2026 ISP.)"
),
(
"2431 (This figure reflects the estimate from Option 2 with a portion costed "
"at Class 5b removed.)"
),
"1749.5 (only part of this figure is included)",
]
)
Expand All @@ -259,7 +265,7 @@ def test_remove_series_notes_after_values_with_special_characters():
pd.testing.assert_series_equal(result, expected)


def test_remove_series_bracketed_footnotes():
def test_remove_series_bracketed_footnotes() -> None:
unsanitised = pd.Series(
[
"750[footnote14]",
Expand Down Expand Up @@ -308,8 +314,10 @@ def test_remove_series_bracketed_footnotes():
("0.16 (apply from 5,400 MW)", False, "0.16"), # 6.0 Build limits, decimal value
( # 6.0 Network Capability. The digit after the note is a footnote reference, not
# a second value, so the separator between them may not contain letters.
"400 (with VNI SIPS) - Note 8 (Snowy 2.0 generation or pump load <= 660 "
"- Note 11)",
(
"400 (with VNI SIPS) - Note 8 (Snowy 2.0 generation or pump load <= 660 "
"- Note 11)"
),
False,
"400",
),
Expand Down Expand Up @@ -341,10 +349,12 @@ def test_remove_series_bracketed_footnotes():
]


def test_multiple_values_with_notes_detection_and_sanitisation():
def test_multiple_values_with_notes_detection_and_sanitisation() -> None:
"""Cells holding two values are detected and kept whole; cells holding one value
and a note are still cut down to that value."""
cells, fires, sanitised = (list(field) for field in zip(*MULTIPLE_VALUE_CASES))
cells, fires, sanitised = (
list(field) for field in zip(*MULTIPLE_VALUE_CASES, strict=True)
)
unsanitised = pd.Series(cells)
pd.testing.assert_series_equal(
_where_multiple_values_with_notes(unsanitised), pd.Series(fires)
Expand All @@ -354,7 +364,7 @@ def test_multiple_values_with_notes_detection_and_sanitisation():
)


def test_where_multiple_values_with_notes_on_mixed_and_index_input():
def test_where_multiple_values_with_notes_on_mixed_and_index_input() -> None:
"""Columns reaching the sanitisers hold a mix of strings, numbers and nulls, and
the sanitisers are also applied to a `pandas.Index` of column names."""
series = pd.Series(["250 (generation) 325 (pump)", "250 (generation)", 42.0, None])
Expand All @@ -366,7 +376,7 @@ def test_where_multiple_values_with_notes_on_mixed_and_index_input():
assert list(_where_multiple_values_with_notes(index)) == [True, False]


def test_values_casting_and_sanitisation_leaves_multi_value_column_as_text():
def test_values_casting_and_sanitisation_leaves_multi_value_column_as_text() -> None:
"""A column containing a multi-value cell cannot be cast to a numeric type, which
is the signal to consumers that the cell holds more than one value."""
df = pd.DataFrame({"capacity": ["250 (generation) 325 (pump)", "500 (generation)"]})
Expand Down
Loading
Loading