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/Dockerfile b/Dockerfile new file mode 100644 index 0000000..36de295 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,37 @@ +FROM ubuntu:latest + +# update to the latest packages +ENV DEBIAN_FRONTEND="noninteractive" +RUN apt-get update && apt-get upgrade -y + +# add user that will be used to install and run the application +RUN groupadd -g 1000 deploy +RUN useradd -m -u 1000 -g deploy deploy + +# install required packages for debugging +# RUN apt-get install -y vim curl lsof + + +# +# CUSTOM PART IS BELOW HERE +# + + +# install required packages +RUN apt-get install -y python3 python3-pip + +# switch to the created user and install the application +USER deploy +ENV PATH "$PATH:/home/deploy/.local/bin" + +COPY . /app +WORKDIR /app + +RUN pip install --upgrade pip +RUN pip install gunicorn +RUN pip install wheel +RUN pip install flit + +RUN flit install --pth-file + +CMD ["gunicorn", "--paster", "/app/production.ini", "-b", "0.0.0.0:8000"] diff --git a/Makefile b/Makefile index 6bfe8db..5b350ed 100644 --- a/Makefile +++ b/Makefile @@ -51,16 +51,19 @@ clean-test: ## remove test and coverage artifacts rm -fr htmlcov/ lint: ## reformat with black and check style with flake8 - isort -rc superx_budget - isort -rc tests + isort superx_budget + isort tests black superx_budget tests 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 56149ce..80695c0 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,47 +26,51 @@ classifiers = [ "License :: Freely Distributable", ] -requires = [ +dependencies = [ "openpyxl >= 3.0.0", - "pyramid >= 1.10", - "pyramid_jinja2 >= 2.7", - "pyramid_mailer >= 0.15.1", - "waitress >= 1.4.3", + "pyramid >= 2.0", + "pyramid_jinja2", + "pyramid_mailer", + "waitress", + "passlib[argon2]", ] -requires-python = ">=3.7" -[tool.flit.metadata.requires-extra] +[project.entry-points."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 = [ - "passlib[argon2] >= 1.7.2", "pytest >=4.0.0", "pytest-cov", "pytest-mock", + "pytest-randomly", "tox", ] dev = [ "black", "flake8", "flake8-comprehensions", - "isort", + "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/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 0200e21..eceaca1 100644 --- a/superx_budget/helpers.py +++ b/superx_budget/helpers.py @@ -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 e3ee9d1..3986a7c 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] @@ -60,7 +60,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: @@ -70,7 +70,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 @@ -79,7 +79,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)) @@ -89,6 +89,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)