Browse Source

first budget template parser

pull/1/head
Holger Frey 5 years ago
parent
commit
de324dcd9c
  1. 4
      superx_budget/__init__.py
  2. 59
      superx_budget/budget.py
  3. 13
      superx_budget/exceptions.py
  4. 16
      superx_budget/superx.py
  5. BIN
      test data/test export data.numbers
  6. 5
      tests/conftest.py
  7. 77
      tests/test_budget_parser.py
  8. 24
      tests/test_superx_parser.py

4
superx_budget/__init__.py

@ -4,3 +4,7 @@ Creating a budget overview from a SuperX export
""" """
__version__ = "0.0.1" __version__ = "0.0.1"
from .superx import parse_export_data # noqa: F401
from .exceptions import BudgetParserError, SuperXParserError # noqa: F401

59
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))

13
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

16
superx_budget/superx.py

@ -3,6 +3,8 @@
from datetime import datetime from datetime import datetime
from collections import namedtuple from collections import namedtuple
from .exceptions import SuperXParserError
EXPECTED_HEADLINE = "Verwendungsnachweis und Kassenstand SAP" EXPECTED_HEADLINE = "Verwendungsnachweis und Kassenstand SAP"
EXPECTED_METADATA_KEYS = {"Haushaltsjahr", "Stand", "Gruppierung"} EXPECTED_METADATA_KEYS = {"Haushaltsjahr", "Stand", "Gruppierung"}
EXPECTED_EXPORT_GROUPING = "automatisch" EXPECTED_EXPORT_GROUPING = "automatisch"
@ -29,15 +31,11 @@ SuperXData = namedtuple(
) )
class SuperXError(ValueError):
pass
def _check_export_headline(row): def _check_export_headline(row):
""" checks the first line of the excel data if it's what we'd expect """ """ checks the first line of the excel data if it's what we'd expect """
headline = row[0] headline = row[0]
if headline != EXPECTED_HEADLINE: if headline != EXPECTED_HEADLINE:
raise SuperXError(f"unexpected headline: '{headline}'") raise SuperXParserError(f"unexpected headline: '{headline}'")
def _get_export_metadata(row): def _get_export_metadata(row):
@ -47,9 +45,11 @@ def _get_export_metadata(row):
parts = [entry.split(":", 1) for entry in entries] parts = [entry.split(":", 1) for entry in entries]
metadata = {key.strip(): value.strip() for key, value in parts} metadata = {key.strip(): value.strip() for key, value in parts}
if EXPECTED_METADATA_KEYS - set(metadata.keys()): 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: if metadata["Gruppierung"] != EXPECTED_EXPORT_GROUPING:
raise SuperXError(f"unexpected grouping: {metadata['Gruppierung']}") raise SuperXParserError(
f"unexpected grouping: {metadata['Gruppierung']}"
)
return SuperXResult( return SuperXResult(
metadata["Haushaltsjahr"], metadata["Haushaltsjahr"],
datetime.strptime(metadata["Stand"], "%d.%m.%Y"), 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: if first_cell == EXPECTED_DATA_TABLE_HEADER:
break break
else: else:
raise SuperXError("could not find table header") raise SuperXParserError("could not find table header")
def _parse_data_table(rows): def _parse_data_table(rows):

BIN
test data/test export data.numbers

Binary file not shown.

5
tests/conftest.py

@ -15,7 +15,6 @@ class MockWorkbookSheet:
@pytest.fixture @pytest.fixture
def example_file(request): def example_root(request):
root_dir = Path(request.config.rootdir) root_dir = Path(request.config.rootdir)
data_dir = root_dir / "test data" yield root_dir / "test data"
return data_dir / "Verwendungsnachweis_und_Kassenstand_SAP_Zahlen.xlsx"

77
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"

24
tests/test_superx_parser.py

@ -4,19 +4,25 @@
import pytest import pytest
@pytest.fixture
def example_file(example_root):
return example_root / "Verwendungsnachweis_und_Kassenstand_SAP_Zahlen.xlsx"
@pytest.fixture @pytest.fixture
def example_workbook(example_file): def example_workbook(example_file):
import openpyxl import openpyxl
return openpyxl.open(example_file) yield openpyxl.open(example_file)
def test_check_export_headline(): 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"] row = ["nomatching header"]
with pytest.raises(SuperXError): with pytest.raises(SuperXParserError):
_check_export_headline(row) _check_export_headline(row)
@ -48,7 +54,9 @@ def test_get_export_metadata_raises_error(faulty_data):
row = [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) _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(): def test_skip_export_data_until_table_header_raises_error():
from superx_budget.superx import ( from superx_budget.superx import _skip_export_data_until_table_header
_skip_export_data_until_table_header, from superx_budget.exceptions import SuperXParserError
SuperXError,
)
rows = [ rows = [
[""], [""],
@ -81,7 +87,7 @@ def test_skip_export_data_until_table_header_raises_error():
["Daten"], ["Daten"],
] ]
iterator = iter(rows) iterator = iter(rows)
with pytest.raises(SuperXError): with pytest.raises(SuperXParserError):
_skip_export_data_until_table_header(iterator) _skip_export_data_until_table_header(iterator)

Loading…
Cancel
Save