diff --git a/superx_budget/__init__.py b/superx_budget/__init__.py index 76d8d7c..c92f0d0 100644 --- a/superx_budget/__init__.py +++ b/superx_budget/__init__.py @@ -4,3 +4,7 @@ Creating a budget overview from a SuperX export """ __version__ = "0.0.1" + + +from .superx import parse_export_data # noqa: F401 +from .exceptions import BudgetParserError, SuperXParserError # noqa: F401 diff --git a/superx_budget/budget.py b/superx_budget/budget.py new file mode 100644 index 0000000..4b0d087 --- /dev/null +++ b/superx_budget/budget.py @@ -0,0 +1,59 @@ +""" Budget Parser """ + +from collections import namedtuple + +from .exceptions import BudgetParserError + +EXPECTED_TABLE_HEADERS = [ + "Nr.", + "Projekt", + "Laufzeit", + "BA", + "Budget", + "Ausgaben", + "Rest", +] + +BudgetData = namedtuple( + "BudgetData", + [ + "row", + "number", + "project_name", + "period", + "project", + "budget", + "expenses", + "rest", + ], +) + + +def _check_table_header(row): + fields = [c.strip() for c in row[:7]] + print(fields) + print(EXPECTED_TABLE_HEADERS) + if fields != EXPECTED_TABLE_HEADERS: + raise BudgetParserError(f"unexpected headers: '{row}'") + + +def _skip_empty_lines(rows, start=0): + for i, row in enumerate(rows, start=start): + first_cell = row[0] + if first_cell is None: + continue + if isinstance(first_cell, str) and first_cell.strip() == "": + continue + yield i, row + + +def _parse_data_table(rows, start=2): + for i, data in _skip_empty_lines(rows, start): + yield BudgetData(i, *data[:7]) + + +def parse_budget_data(xls_sheet): + """ parses the budget data """ + rows = xls_sheet.values + _check_table_header(next(rows)) + return list(_parse_data_table(rows, start=2)) diff --git a/superx_budget/exceptions.py b/superx_budget/exceptions.py new file mode 100644 index 0000000..a3ddfa5 --- /dev/null +++ b/superx_budget/exceptions.py @@ -0,0 +1,13 @@ +""" Exceptions used in the Project """ + + +class SuperXBudgetError(ValueError): + """ Base class for project errors """ + + +class SuperXParserError(SuperXBudgetError): + pass + + +class BudgetParserError(SuperXBudgetError): + pass diff --git a/superx_budget/superx.py b/superx_budget/superx.py index 951cfc7..4356b6f 100644 --- a/superx_budget/superx.py +++ b/superx_budget/superx.py @@ -3,6 +3,8 @@ from datetime import datetime from collections import namedtuple +from .exceptions import SuperXParserError + EXPECTED_HEADLINE = "Verwendungsnachweis und Kassenstand SAP" EXPECTED_METADATA_KEYS = {"Haushaltsjahr", "Stand", "Gruppierung"} EXPECTED_EXPORT_GROUPING = "automatisch" @@ -29,15 +31,11 @@ SuperXData = namedtuple( ) -class SuperXError(ValueError): - pass - - def _check_export_headline(row): """ checks the first line of the excel data if it's what we'd expect """ headline = row[0] if headline != EXPECTED_HEADLINE: - raise SuperXError(f"unexpected headline: '{headline}'") + raise SuperXParserError(f"unexpected headline: '{headline}'") def _get_export_metadata(row): @@ -47,9 +45,11 @@ def _get_export_metadata(row): parts = [entry.split(":", 1) for entry in entries] metadata = {key.strip(): value.strip() for key, value in parts} if EXPECTED_METADATA_KEYS - set(metadata.keys()): - raise SuperXError(f"unexpected metadata: '{data}'") + raise SuperXParserError(f"unexpected metadata: '{data}'") if metadata["Gruppierung"] != EXPECTED_EXPORT_GROUPING: - raise SuperXError(f"unexpected grouping: {metadata['Gruppierung']}") + raise SuperXParserError( + f"unexpected grouping: {metadata['Gruppierung']}" + ) return SuperXResult( metadata["Haushaltsjahr"], datetime.strptime(metadata["Stand"], "%d.%m.%Y"), @@ -64,7 +64,7 @@ def _skip_export_data_until_table_header(rows): if first_cell == EXPECTED_DATA_TABLE_HEADER: break else: - raise SuperXError("could not find table header") + raise SuperXParserError("could not find table header") def _parse_data_table(rows): diff --git a/test data/test export data.numbers b/test data/test export data.numbers new file mode 100755 index 0000000..74e7891 Binary files /dev/null and b/test data/test export data.numbers differ diff --git a/tests/conftest.py b/tests/conftest.py index 540d81f..0ebb54b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,7 +15,6 @@ class MockWorkbookSheet: @pytest.fixture -def example_file(request): +def example_root(request): root_dir = Path(request.config.rootdir) - data_dir = root_dir / "test data" - return data_dir / "Verwendungsnachweis_und_Kassenstand_SAP_Zahlen.xlsx" + yield root_dir / "test data" diff --git a/tests/test_budget_parser.py b/tests/test_budget_parser.py new file mode 100644 index 0000000..49980bd --- /dev/null +++ b/tests/test_budget_parser.py @@ -0,0 +1,77 @@ +import pytest + + +@pytest.fixture +def example_file(example_root): + return example_root / "Verbrauchsmittel-Toto-2020.xlsx" + + +@pytest.fixture +def example_workbook(example_file): + import openpyxl + + yield openpyxl.open(example_file) + + +@pytest.fixture +def example_sheet(example_workbook): + sheets = example_workbook.sheetnames + first = sheets[0] + yield example_workbook[first] + + +def test_check_table_header_raises_error(): + from superx_budget.budget import _check_table_header + from superx_budget.exceptions import BudgetParserError + + row = ["not", "the", "expected", "row"] + + with pytest.raises(BudgetParserError): + _check_table_header(row) + + +def test_skip_empty_lines(): + from superx_budget.budget import _skip_empty_lines + + rows = [[""], ["one"], [None], [""], ["two"], [""]] + + result = _skip_empty_lines(rows) + + assert list(result) == [(1, ["one"]), (4, ["two"])] + + +def test_parse_data_table(): + from superx_budget.budget import _parse_data_table + + rows = [ + list("ABCDEFG"), + [None for i in range(7)], + list("tuvwxyzX"), # one item more + ] + + result = _parse_data_table(rows) + first, last = list(result) + + assert first.row == 2 + assert first.number == "A" + assert first.rest == "G" + assert last.row == 4 + assert last.number == "t" + assert last.rest == "z" + + +def test_parse_budget_data(example_sheet): + from superx_budget.budget import parse_budget_data + + result = parse_budget_data(example_sheet) + first, last = result[0], result[-1] + + assert len(result) == 18 + assert first.row == 3 + assert first.number == 1 + assert first.project_name == "Safegurard I (neu)" + assert first.rest == "=E3-F3" + assert last.row == 54 + assert last.number == "=A51+1" + assert last.project_name == "ZIM Microcoat II" + assert last.rest == "=E54-F54" diff --git a/tests/test_superx_parser.py b/tests/test_superx_parser.py index 0d11da3..0cd3dab 100644 --- a/tests/test_superx_parser.py +++ b/tests/test_superx_parser.py @@ -4,19 +4,25 @@ import pytest +@pytest.fixture +def example_file(example_root): + return example_root / "Verwendungsnachweis_und_Kassenstand_SAP_Zahlen.xlsx" + + @pytest.fixture def example_workbook(example_file): import openpyxl - return openpyxl.open(example_file) + yield openpyxl.open(example_file) def test_check_export_headline(): - from superx_budget.superx import _check_export_headline, SuperXError + from superx_budget.superx import _check_export_headline + from superx_budget.exceptions import SuperXParserError row = ["nomatching header"] - with pytest.raises(SuperXError): + with pytest.raises(SuperXParserError): _check_export_headline(row) @@ -48,7 +54,9 @@ def test_get_export_metadata_raises_error(faulty_data): row = [faulty_data] - with pytest.raises(ValueError): # SuperXError is a subclass of ValueError + with pytest.raises( + ValueError + ): # SuperXParserError is a subclass of ValueError _get_export_metadata(row) @@ -69,10 +77,8 @@ def test_skip_export_data_until_table_header_ok(): def test_skip_export_data_until_table_header_raises_error(): - from superx_budget.superx import ( - _skip_export_data_until_table_header, - SuperXError, - ) + from superx_budget.superx import _skip_export_data_until_table_header + from superx_budget.exceptions import SuperXParserError rows = [ [""], @@ -81,7 +87,7 @@ def test_skip_export_data_until_table_header_raises_error(): ["Daten"], ] iterator = iter(rows) - with pytest.raises(SuperXError): + with pytest.raises(SuperXParserError): _skip_export_data_until_table_header(iterator)