diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..ef9877f --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +per-file-ignores = tests/*:S101 diff --git a/.gitignore b/.gitignore index cb962b8..1d77a72 100644 --- a/.gitignore +++ b/.gitignore @@ -45,7 +45,6 @@ htmlcov/ nosetests.xml coverage.xml *,cover -mail/ # Translations *.mo @@ -63,5 +62,3 @@ target/ # Mac Stuff .DS_Store -# Pyramid -production.ini diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7387404..fe4af8a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ Types of Contributions ### Report Bugs -Report bugs at https://github.com/holgi/superx_budget/issues. +Report bugs at https://git.cpi.imtek.uni-freiburg.de/CPI/superx-budget-overview.git/issues. If you are reporting a bug, please include: @@ -31,13 +31,13 @@ and "help wanted" is open to whoever wants to implement it. ### Write Documentation -superx_budget could always use more documentation, whether as part of the -official superx_budget docs, in docstrings, or even on the web in blog posts, +SuperX Budget could always use more documentation, whether as part of the +official SuperX Budget docs, in docstrings, or even on the web in blog posts, articles, and such. ### Submit Feedback -The best way to send feedback is to file an issue at https://github.com/holgi/superx_budget/issues. +The best way to send feedback is to file an issue at https://git.cpi.imtek.uni-freiburg.de/CPI/superx-budget-overview.git/issues. If you are proposing a feature: diff --git a/Makefile b/Makefile index 56dac2b..5b350ed 100644 --- a/Makefile +++ b/Makefile @@ -57,10 +57,13 @@ lint: ## reformat with black and check style with flake8 flake8 superx_budget tests test: lint ## run tests quickly with the default Python - pytest tests -x --disable-warnings -m "not app" + pytest tests -x --disable-warnings -m "not fun" + +testall: lint ## run tests quickly with the default Python + pytest tests coverage: lint ## full test suite, check code coverage and open coverage report - pytest tests --cov=superx_budget + pytest tests --cov=superx_budget -m "fun" coverage html $(BROWSER) htmlcov/index.html @@ -73,12 +76,15 @@ install: ## install updated project.toml with flint devenv: ## setup development environment python3 -m venv --prompt superx_budget .venv .venv/bin/pip3 install --upgrade pip - .venv/bin/pip3 install flit + .venv/bin/pip3 install "flit>3.2" .venv/bin/flit install --pth-file repo: devenv ## complete project setup with development environment and git repo git init . + git branch -m main git add . git commit -m "import of project template" - + git remote add origin https://git.cpi.imtek.uni-freiburg.de/CPI/superx-budget-overview.git + git push -u origin main --no-verify + .venv/bin/pre-commit install --install-hooks diff --git a/pyproject.toml b/pyproject.toml index 35d0237..2b458be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,14 +4,17 @@ requires = ["flit"] build-backend = "flit.buildapi" -[tool.flit.metadata] -module = "superx_budget" -dist-name = "superx_budget" -author = "Holger Frey" -author-email = "frey@imtek.de" -home-page = "https://github.com/holgi/superx_budget" -description-file = "README.md" -license = "Beerware" +[project] +name = "superx_budget" +readme = "README.md" +description = "Creating a budget overview from a SuperX export." +license = { file = "LICENSE" } +requires-python = ">=3.7" +dynamic = ["version"] + +authors = [ + {name = "Holger Frey", email = "frey@imtek.de"}, +] # see https://pypi.org/classifiers/ classifiers = [ @@ -23,7 +26,7 @@ classifiers = [ "License :: Freely Distributable", ] -requires = [ +dependencies = [ "openpyxl >= 3.0.0", "pyramid >= 1.10", "pyramid_jinja2 >= 2.7", @@ -31,39 +34,43 @@ requires = [ "waitress >= 1.4.3", "passlib[argon2] >= 1.7.2", ] -requires-python = ">=3.7" -[tool.flit.metadata.requires-extra] +[project.entrypoints."paste.app_factory"] +main = "superx_budget:main" + +[project.urls] +Source = "https://git.cpi.imtek.uni-freiburg.de/CPI/superx-budget-overview.git" + +[project.optional-dependencies] test = [ "pytest >=4.0.0", "pytest-cov", "pytest-mock", + "pytest-randomly", "tox", ] dev = [ "black", "flake8", "flake8-comprehensions", + "flake8-bandit", "isort >= 5.0.0", "keyring", "pre-commit", ] -[tool.flit.entrypoints."paste.app_factory"] -main = "superx_budget:main" - [tool.black] line-length = 79 -py37 = true +target-version = ['py37','py38', 'py39'] include = '\.pyi?$' -exclude = ''' -/( - \.git - | \.tox - | \.venv - | build - | dist -)/ +extend-exclude = ''' +# A regex preceded with ^/ will apply only to files and directories +# in the root of the project. +^/.git +^/.tox +^/.venv +^/.build +^/.dist ''' [tool.isort] @@ -71,3 +78,11 @@ line_length=79 multi_line_output=3 length_sort="True" include_trailing_comma="True" + +[tool.pytest.ini_options] +markers = [ + "fun: marks tests as functional (deselect with '-m \"not fun\"')", +] +addopts = [ + "--strict-markers", +] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index fc38fee..0000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -openpyxl==3.0.5 -pyramid==1.10.4 -pyramid-jinja2==2.8 -pyramid-mailer==0.15.1 -waitress==1.4.4 -passlib==1.7.2 \ No newline at end of file diff --git a/superx_budget/budget.py b/superx_budget/budget.py index 5d1f6e2..5342850 100644 --- a/superx_budget/budget.py +++ b/superx_budget/budget.py @@ -56,13 +56,13 @@ def _parse_data_table(rows): def parse_budget_data(xls_sheet): - """ parses the budget data """ + """parses the budget data""" rows = (ExcelRow(i, v) for i, v in enumerate(xls_sheet.values, start=1)) _check_table_header(next(rows)) return list(_parse_data_table(rows)) def parse_budget_file(file_path): - """ parses the budget file """ + """parses the budget file""" sheet = get_sheet_of_file(file_path, sheet=None) return parse_budget_data(sheet) diff --git a/superx_budget/exceptions.py b/superx_budget/exceptions.py index a3ddfa5..0d880ea 100644 --- a/superx_budget/exceptions.py +++ b/superx_budget/exceptions.py @@ -2,7 +2,7 @@ class SuperXBudgetError(ValueError): - """ Base class for project errors """ + """Base class for project errors""" class SuperXParserError(SuperXBudgetError): diff --git a/superx_budget/helpers.py b/superx_budget/helpers.py index 07d5c24..eceaca1 100644 --- a/superx_budget/helpers.py +++ b/superx_budget/helpers.py @@ -19,7 +19,7 @@ def excel_value_as_number(value): def get_sheet_of_file(excel_file, sheet=None): - """ returns a sheet from an excel FileCache + """returns a sheet from an excel FileCache if name is set to None, the function returns the first sheet """ @@ -31,7 +31,7 @@ def get_sheet_of_file(excel_file, sheet=None): def is_empty_excel_value(value): - """ is the cell value considered empty """ + """is the cell value considered empty""" if value is None: return True if isinstance(value, str) and value.strip() == "": @@ -40,14 +40,14 @@ def is_empty_excel_value(value): def strip_excel_value(value): - """ remove whitespace from an excel value if it is a string """ + """remove whitespace from an excel value if it is a string""" if isinstance(value, str): return value.strip() return value def is_budget_file_name(path_or_name): - """ checks if a filename has the format "budget[...]-.xlsx """ + """checks if a filename has the format "budget[...]-.xlsx""" path = Path(path_or_name) if not path.suffix.lower() == ".xlsx": return False @@ -61,21 +61,21 @@ def is_budget_file_name(path_or_name): def list_budget_files(folder): - """ lists all files with the name "budget[...]-.xlsx """ + """lists all files with the name "budget[...]-.xlsx""" files = (i for i in Path(folder).iterdir() if i.is_file()) visible = (i for i in files if not i.name.startswith(".")) return [i for i in visible if is_budget_file_name(i)] def find_budget_file(folder, year): - """ searches for a file with the name "budget[...]-.xlsx """ + """searches for a file with the name "budget[...]-.xlsx""" for path in list_budget_files(folder): if path.stem.endswith(f"-{year}"): return path def find_recipients(folder, filename="recipients.txt"): - """ finds the recipients of the budget list """ + """finds the recipients of the budget list""" file_path = folder / filename if file_path.is_file(): with file_path.open() as filehandle: diff --git a/superx_budget/overview.py b/superx_budget/overview.py index 4386451..eee06d1 100644 --- a/superx_budget/overview.py +++ b/superx_budget/overview.py @@ -19,41 +19,41 @@ OverviewBudgetEntry = namedtuple( class ProjectOverview: def __init__(self, budget_data): - """ initializes the class """ + """initializes the class""" self.budget_data = budget_data self.entries = [] self.found = False @property def project(self): - """ returns the project number """ + """returns the project number""" return self.budget_data.project @property def row(self): - """ returns the excel row number """ + """returns the excel row number""" return self.budget_data.row @property def expenses(self): - """ returns the accumulated expenses """ + """returns the accumulated expenses""" numbers = (excel_value_as_number(e.amount) for e in self.entries) values = (abs(entry) for entry in numbers) return sum(values) @property def available(self): - """ returns the still available budget """ + """returns the still available budget""" return self.budget_data.budget - self.expenses def add(self, description, kind, amount): - """ adds an entry that modifies the available budget """ + """adds an entry that modifies the available budget""" entry = OverviewBudgetEntry(description, kind, amount) self.entries.append(entry) def _create_overview_map(budget_list): - """ returns a dictonary with project as key and overview as value """ + """returns a dictonary with project as key and overview as value""" map = {} for budget_data in budget_list: overview = ProjectOverview(budget_data) @@ -62,12 +62,12 @@ def _create_overview_map(budget_list): def _filter_superx_material_expenses(superx_export): - """ filters superx data to only contain material entries """ + """filters superx data to only contain material entries""" return (i for i in superx_export.data if i.kind in VALID_MATERIAL_IDS) def _create_entries_from_superx(overview_map, superx_export_data): - """ adds overview entries from superx data """ + """adds overview entries from superx data""" for line in superx_export_data: if line.project in overview_map: overview = overview_map[line.project] @@ -85,7 +85,7 @@ def _set_found_state(overview_map, superx_export): def create_overview(budget_list, superx_export): - """ create a overview map with budget entries from the parsed raw data """ + """create a overview map with budget entries from the parsed raw data""" tmp_map = _create_overview_map(budget_list) overview_map = _set_found_state(tmp_map, superx_export) material_expenses = _filter_superx_material_expenses(superx_export) diff --git a/superx_budget/pyramid/__init__.py b/superx_budget/pyramid/__init__.py index 635ee15..cf6f81c 100644 --- a/superx_budget/pyramid/__init__.py +++ b/superx_budget/pyramid/__init__.py @@ -5,7 +5,7 @@ from pathlib import Path from pyramid.view import notfound_view_config from pyramid.config import Configurator from pyramid.session import JSONSerializer, SignedCookieSessionFactory -from pyramid.security import Allow, Everyone, Authenticated +from pyramid.authorization import Allow, Everyone, Authenticated from pyramid.httpexceptions import HTTPFound from ..overview import create_overview # noqa: F401 @@ -23,7 +23,7 @@ class Root: def main(global_config, **settings): - """ This function returns a Pyramid WSGI application. """ + """This function returns a Pyramid WSGI application.""" with Configurator(settings=settings) as config: session_factory = SignedCookieSessionFactory( diff --git a/superx_budget/superx.py b/superx_budget/superx.py index dc7da87..f998cca 100644 --- a/superx_budget/superx.py +++ b/superx_budget/superx.py @@ -33,14 +33,14 @@ SuperXData = namedtuple( 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] if headline != EXPECTED_HEADLINE: raise SuperXParserError(f"unexpected headline: '{headline}'") def _get_export_metadata(row): - """ extracts the metadata from the second row of the excel sheet """ + """extracts the metadata from the second row of the excel sheet""" data = row[0] entries = data.split(";") parts = [entry.split(":", 1) for entry in entries] @@ -59,7 +59,7 @@ def _get_export_metadata(row): def _skip_export_data_until_table_header(rows): - """ skip rows until data table headers """ + """skip rows until data table headers""" for line in rows: first_cell = line[0] if first_cell == EXPECTED_DATA_TABLE_HEADER: @@ -69,7 +69,7 @@ def _skip_export_data_until_table_header(rows): def _parse_data_table(rows): - """ parses non-empty lines of the data table """ + """parses non-empty lines of the data table""" for line in rows: if not line[0]: continue @@ -78,7 +78,7 @@ def _parse_data_table(rows): def parse_export_data(xls_sheet): - """ parses the exported superx data """ + """parses the exported superx data""" rows = xls_sheet.values _check_export_headline(next(rows)) metadata = _get_export_metadata(next(rows)) @@ -88,6 +88,6 @@ def parse_export_data(xls_sheet): def parse_exported_file(file_path): - """ parses the budget file """ + """parses the budget file""" sheet = get_sheet_of_file(file_path, sheet=None) return parse_export_data(sheet) diff --git a/tests/test_budget_parser.py b/tests/test_budget_parser.py index 892e07d..1fb1800 100644 --- a/tests/test_budget_parser.py +++ b/tests/test_budget_parser.py @@ -2,7 +2,7 @@ import pytest def test_check_table_header_raises_error(): - from superx_budget.budget import _check_table_header, ExcelRow + from superx_budget.budget import ExcelRow, _check_table_header from superx_budget.exceptions import BudgetParserError row = ExcelRow(None, ["not", "the", "expected", "row", None, 0]) @@ -12,7 +12,7 @@ def test_check_table_header_raises_error(): def test_skip_empty_lines(): - from superx_budget.budget import _skip_empty_lines, ExcelRow + from superx_budget.budget import ExcelRow, _skip_empty_lines rows = [ ExcelRow(0, [""]), @@ -29,7 +29,7 @@ def test_skip_empty_lines(): def test_parse_data_table(): - from superx_budget.budget import _parse_data_table, ExcelRow + from superx_budget.budget import ExcelRow, _parse_data_table rows = [ ExcelRow(2, list("ABCDEFG")), diff --git a/tests/test_overview.py b/tests/test_overview.py index b95be30..dd93374 100644 --- a/tests/test_overview.py +++ b/tests/test_overview.py @@ -79,8 +79,8 @@ def test_project_overview_available_property(example_budget_data): def test_create_overview_map(budget_example_file): - from superx_budget.overview import _create_overview_map from superx_budget.budget import parse_budget_file + from superx_budget.overview import _create_overview_map budget_data = parse_budget_file(budget_example_file) result = _create_overview_map(budget_data) @@ -92,12 +92,12 @@ def test_create_overview_map(budget_example_file): def test_create_entries_from_export(budget_example_file, superx_example_file): + from superx_budget.budget import parse_budget_file + from superx_budget.superx import parse_exported_file from superx_budget.overview import ( - _create_entries_from_superx, _create_overview_map, + _create_entries_from_superx, ) - from superx_budget.budget import parse_budget_file - from superx_budget.superx import parse_exported_file superx_data = parse_exported_file(superx_example_file) budget_data = parse_budget_file(budget_example_file) @@ -109,11 +109,11 @@ def test_create_entries_from_export(budget_example_file, superx_example_file): def test_filter_superx_material_expenses(superx_example_file): + from superx_budget.superx import parse_exported_file from superx_budget.overview import ( - _filter_superx_material_expenses, VALID_MATERIAL_IDS, + _filter_superx_material_expenses, ) - from superx_budget.superx import parse_exported_file superx_data = parse_exported_file(superx_example_file) result = list(_filter_superx_material_expenses(superx_data)) @@ -124,9 +124,9 @@ def test_filter_superx_material_expenses(superx_example_file): def test_create_overview(budget_example_file, superx_example_file): - from superx_budget.overview import create_overview from superx_budget.budget import parse_budget_file from superx_budget.superx import parse_exported_file + from superx_budget.overview import create_overview superx_data = parse_exported_file(superx_example_file) budget_data = parse_budget_file(budget_example_file) diff --git a/tests/test_superx_parser.py b/tests/test_superx_parser.py index 55cafbe..1a757b0 100644 --- a/tests/test_superx_parser.py +++ b/tests/test_superx_parser.py @@ -15,9 +15,10 @@ def test_check_export_headline(): def test_get_export_metadata_ok(): - from superx_budget.superx import _get_export_metadata from datetime import datetime + from superx_budget.superx import _get_export_metadata + value = "Haushaltsjahr: XXX;Stand:31.12.2020;Gruppierung:automatisch" row = [value] metadata = _get_export_metadata(row) @@ -88,9 +89,10 @@ def test_parse_data_table(): def test_parse_export_data(superx_example_workbook): - from superx_budget.superx import parse_export_data from datetime import datetime + from superx_budget.superx import parse_export_data + result = parse_export_data(superx_example_workbook.active) assert result.account_year == "2020"