From 46d6f300b730cb96dcfd5fd0b3ee30029ed65378 Mon Sep 17 00:00:00 2001 From: Shane Jaroch Date: Sat, 8 Apr 2023 10:24:42 -0400 Subject: [PATCH] Dev (#19) --- .banditrc | 1 - .envrc | 2 +- .gitattributes | 4 + .github/workflows/install-linux.yml | 23 +++- .github/workflows/install-win32.yml | 41 ++++++ .github/workflows/lint.yml | 20 ++- .github/workflows/test.yml | 31 +++-- .gitignore | 2 +- .mailmap | 1 - .pylintrc | 2 +- CHANGELOG.rst | 29 +++++ MANIFEST.in | 3 +- Makefile | 131 +++++++++---------- README.rst | 56 ++++---- ntclient/__init__.py | 145 +-------------------- ntclient/__main__.py | 14 +- ntclient/argparser/__init__.py | 16 +-- ntclient/argparser/funcs.py | 6 +- ntclient/models/__init__.py | 2 +- ntclient/ntsqlite | 2 +- ntclient/persistence/sql/__init__.py | 3 +- ntclient/persistence/sql/usda/__init__.py | 2 +- ntclient/persistence/sql/usda/funcs.py | 3 +- ntclient/services/analyze.py | 2 +- ntclient/services/calculate.py | 6 +- ntclient/services/recipe/csv_utils.py | 2 +- ntclient/services/usda.py | 2 +- ntclient/utils/__init__.py | 152 ++++++++++++++++++++++ ntclient/utils/colors.py | 49 +------ ntclient/utils/tree.py | 6 +- requirements-lint.txt | 21 ++- requirements-old.txt | 6 +- requirements-optional.txt | 2 +- requirements-test.txt | 4 +- requirements.txt | 6 +- scripts/n | 12 ++ setup.cfg | 24 +++- setup.py | 19 +-- tests/resources/day/dog-simple.csv | 26 ++-- tests/test_cli.py | 5 +- 40 files changed, 492 insertions(+), 391 deletions(-) create mode 100644 .github/workflows/install-win32.yml create mode 100755 scripts/n diff --git a/.banditrc b/.banditrc index f5fba94..179faa8 100644 --- a/.banditrc +++ b/.banditrc @@ -1,3 +1,2 @@ assert_used: # B101 skips: ["*/test_*.py"] - diff --git a/.envrc b/.envrc index 1c21d04..b694c19 100644 --- a/.envrc +++ b/.envrc @@ -1,2 +1,2 @@ -source .venv/bin/activate +source .venv/bin/activate || source .venv/Scripts/activate unset PS1 diff --git a/.gitattributes b/.gitattributes index 28924fe..6e5b04b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,5 @@ +# Line ending configuration +* text eol=lf + +# GitHub Linguist (language detection overrides) *.wls linguist-language=Mathematica diff --git a/.github/workflows/install-linux.yml b/.github/workflows/install-linux.yml index 6d283a6..ca22d1c 100644 --- a/.github/workflows/install-linux.yml +++ b/.github/workflows/install-linux.yml @@ -2,18 +2,20 @@ name: install-linux "on": - push: {} + push: + branches: + - "**" permissions: contents: read jobs: python: - runs-on: ubuntu-18.04 + runs-on: [ubuntu-20.04] strategy: matrix: - python-version: ["3.4", "3.6", "3.8", "3.10"] + python-version: ["3.5", "3.8", "3.10"] steps: - name: Checkout @@ -21,16 +23,25 @@ jobs: with: submodules: recursive - - name: Set up Python ${{ matrix.python-version }} + - name: Set up Python ${{ matrix.python-version }} & Restore Cache uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + # update-environment: false + # NOTE: fails on Python 3.6 and 3.4 with the pip error, + # ERROR: unknown command "cache" - maybe you meant "check" + # cache: "pip" # caching pip dependencies + # cache-dependency-path: "**/requirements*.txt" + # NOTE: see above NOTE, we are still using deprecated cache restore - name: Reload Cache / pip uses: actions/cache@v3 with: path: ~/.cache/pip - key: ${{ runner.os }}-${{ matrix.python-version }}-pip-${{ hashFiles('**/requirements*.txt') }} + # NOTE: only cares about base requirements.txt + # yamllint disable rule:line-length + key: ${{ runner.os }}-${{ matrix.python-version }}-pip-${{ hashFiles('**/requirements.txt') }} + # yamllint enable rule:line-length restore-keys: | ${{ runner.os }}-${{ matrix.python-version }}-pip- @@ -42,5 +53,5 @@ jobs: - name: Basic Tests / CLI / Integration run: | n -v - nutra -d recipe init + nutra -d recipe init -f nutra --no-pager recipe diff --git a/.github/workflows/install-win32.yml b/.github/workflows/install-win32.yml new file mode 100644 index 0000000..9323928 --- /dev/null +++ b/.github/workflows/install-win32.yml @@ -0,0 +1,41 @@ +--- +name: install-win32 + +"on": + push: + branches: + # TODO: only run on master, it's slow (or just make unrequired check?) + - "**" + +jobs: + windows-latest: + runs-on: [windows-latest] + + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Reload Cache / pip + uses: actions/cache@v3 + with: + path: ~\AppData\Local\pip\Cache + # NOTE: only cares about base requirements.txt + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install + run: make install + + - name: Basic Tests / CLI / Integration + run: | + n -v + nutra -d init -y + nutra --no-pager nt + nutra --no-pager sort -c 789 + nutra --no-pager search ultraviolet mushrooms + nutra --no-pager anl 9050 + nutra --no-pager recipe + nutra day tests/resources/day/human-test.csv diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0776699..3cc2823 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -9,7 +9,10 @@ permissions: jobs: lint: - runs-on: ubuntu-latest + runs-on: [ubuntu-latest] + + env: + SKIP_VENV: 1 steps: - name: Checkout @@ -17,13 +20,16 @@ jobs: with: submodules: recursive + - name: Fetch master (for incremental diff, lint filter mask) + run: git fetch origin master + - name: Reload Cache / pip - uses: actions/cache@v3 + uses: actions/setup-python@v4 with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} - restore-keys: | - ${{ runner.os }}-pip- + python-version: 3 + cache: "pip" # caching pip dependencies + cache-dependency-path: "**/requirements*.txt" + update-environment: false - name: Install requirements run: | @@ -34,4 +40,4 @@ jobs: pip install -r requirements-test.txt - name: Lint - run: make _lint + run: make lint diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1ce797d..9372dd2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,15 +1,14 @@ --- name: test -"on": - push: {} +"on": push permissions: contents: read jobs: test: - runs-on: [self-hosted, dev-east] + runs-on: [self-hosted, dev] steps: - name: Checkout @@ -17,18 +16,30 @@ jobs: with: submodules: recursive - - name: Cloc + - name: Count lines of code run: make extras/cloc - - name: Install requirements - run: | - /usr/bin/python3 -m pip install -r requirements.txt - /usr/bin/python3 -m pip install -r requirements-test.txt + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Install requirements + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + - name: Initialize (venv) + run: make init + - name: Install requirements + run: > + source .venv/bin/activate && + python -m pip install + coveralls + -r requirements.txt + -r requirements-test.txt + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Test + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - name: Test - run: PATH=~/.local/bin:$PATH make _test + run: source .venv/bin/activate && make test - name: Submit coverage report / coveralls env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: PATH=~/.local/bin:$PATH coveralls --service=github + run: /usr/bin/python3 -m coveralls --service=github diff --git a/.gitignore b/.gitignore index 7a30ff7..c362bbe 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,7 @@ __sha__.py *.sqlite .nutra* - +.env* # Temporary docs* diff --git a/.mailmap b/.mailmap index a8d540e..5880a27 100644 --- a/.mailmap +++ b/.mailmap @@ -8,4 +8,3 @@ Shane Shane Shane Shane - diff --git a/.pylintrc b/.pylintrc index bb0f16a..a92217f 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,6 +1,6 @@ [MASTER] -fail-under=9.75 +fail-under=9.93 [MESSAGES CONTROL] diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 41bc5c1..23c6b48 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,35 @@ and this project adheres to `Semantic Versioning character and two pound "\t##" to show up in this list. Keep it brief! IGNORE_ME .PHONY: _help _help: - @grep -h "##" $(MAKEFILE_LIST) | grep -v IGNORE_ME | sed -e 's/##//' | column -t -s $$'\t' + @printf "\nUsage: make , valid commands:\n\n" + @grep "##" $(MAKEFILE_LIST) | grep -v IGNORE_ME | sed -e 's/##//' | column -t -s $$'\t' # --------------------------------------- # init & venv # --------------------------------------- + .PHONY: init init: ## Set up a Python virtual environment git submodule update --init - if [ ! -d .venv ]; then \ - $(PY_SYS_INTERPRETER) -m venv .venv; \ - fi + rm -rf .venv + ${PY_SYS_INTERPRETER} -m venv .venv + - if [ -z "${CI}" ]; then ${PY_SYS_INTERPRETER} -m venv --upgrade-deps .venv; fi - direnv allow - @echo -e "\r\nNOTE: activate venv, and run 'make deps'\r\n" - @echo -e "HINT: run 'source .venv/bin/activate'" - +# include .env +SKIP_VENV ?= PYTHON ?= $(shell which python) PWD ?= $(shell pwd) .PHONY: _venv _venv: # Test to enforce venv usage across important make targets - [ "$(PYTHON)" = "$(PWD)/.venv/bin/python" ] || [ "$(PYTHON)" = "$(PWD)/.venv/Scripts/python" ] + [ "${SKIP_VENV}" ] || [ "${PYTHON}" = "${PWD}/.venv/bin/python" ] @@ -46,7 +47,6 @@ ifeq ($(PY_SYS_INTERPRETER),) endif PY_VIRTUAL_INTERPRETER ?= python - PIP ?= $(PY_VIRTUAL_INTERPRETER) -m pip REQ_OPT := requirements-optional.txt @@ -54,20 +54,15 @@ REQ_LINT := requirements-lint.txt REQ_TEST := requirements-test.txt REQ_TEST_OLD := requirements-test-old.txt - -PIP_OPT_ARGS ?= - -.PHONY: _deps -_deps: - $(PIP) install wheel - $(PIP) install $(PIP_OPT_ARGS) -r requirements.txt - - $(PIP) install $(PIP_OPT_ARGS) -r $(REQ_OPT) - - $(PIP) install $(PIP_OPT_ARGS) -r $(REQ_LINT) - - $(PIP) install $(PIP_OPT_ARGS) -r $(REQ_TEST) || \ - echo "TEST REQs failed. Try with '--user' flag, or old version: $(PIP) install -r $(REQ_TEST_OLD)" +PIP_OPT_ARGS ?= $(shell if [ "$(SKIP_VENV)" ]; then echo "--user"; fi) .PHONY: deps -deps: _venv _deps ## Install requirements +deps: _venv ## Install requirements + ${PIP} install wheel + ${PIP} install ${PIP_OPT_ARGS} -r requirements.txt + - ${PIP} install ${PIP_OPT_ARGS} -r ${REQ_OPT} + - ${PIP} install ${PIP_OPT_ARGS} -r ${REQ_LINT} + ${PIP} install ${PIP_OPT_ARGS} -r ${REQ_TEST} || ${PIP} install ${PIP_OPT_ARGS} -r ${REQ_TEST_OLD} # --------------------------------------- @@ -75,42 +70,44 @@ deps: _venv _deps ## Install requirements # --------------------------------------- .PHONY: format -format: - isort $(LINT_LOCS) - autopep8 --recursive --in-place --max-line-length 88 $(LINT_LOCS) - black $(LINT_LOCS) +format: _venv ## Format with isort & black + if [ "${CHANGED_FILES_PY_FLAG}" ]; then isort ${CHANGED_FILES_PY} ; fi + if [ "${CHANGED_FILES_PY_FLAG}" ]; then black ${CHANGED_FILES_PY} ; fi LINT_LOCS := ntclient/ tests/ setup.py -# NOTE: doc8 ntclient/ntsqlite/README.rst ? (submodule) -.PHONY: _lint -_lint: +CHANGED_FILES_RST ?= $(shell git diff origin/master --name-only --diff-filter=MACRU \*.rst) +CHANGED_FILES_PY ?= $(shell git diff origin/master --name-only --diff-filter=MACRU \*.py) +CHANGED_FILES_PY_FLAG ?= $(shell if [ "$(CHANGED_FILES_PY)" ]; then echo 1; fi) + +.PHONY: lint +lint: _venv ## Lint code and documentation + # lint RST + if [ "${CHANGED_FILES_RST}" ]; then doc8 --quiet ${CHANGED_FILES_RST}; fi # check formatting: Python - pycodestyle --statistics $(LINT_LOCS) - autopep8 --recursive --diff --max-line-length 88 --exit-code $(LINT_LOCS) - isort --diff --check $(LINT_LOCS) - black --check $(LINT_LOCS) - # lint RST (last param is search term, NOT ignore) - doc8 --quiet *.rst ntclient/ntsqlite/*.rst + if [ "${CHANGED_FILES_PY_FLAG}" ]; then isort --diff --check ${CHANGED_FILES_PY} ; fi + if [ "${CHANGED_FILES_PY_FLAG}" ]; then black --check ${CHANGED_FILES_PY} ; fi # lint Python - bandit -q -c .banditrc -r $(LINT_LOCS) - mypy $(LINT_LOCS) - flake8 $(LINT_LOCS) - pylint $(LINT_LOCS) + if [ "${CHANGED_FILES_PY_FLAG}" ]; then pycodestyle --statistics ${CHANGED_FILES_PY}; fi + if [ "${CHANGED_FILES_PY_FLAG}" ]; then bandit -q -c .banditrc -r ${CHANGED_FILES_PY}; fi + if [ "${CHANGED_FILES_PY_FLAG}" ]; then flake8 ${CHANGED_FILES_PY}; fi + if [ "${CHANGED_FILES_PY_FLAG}" ]; then mypy ${CHANGED_FILES_PY}; fi + if [ "${CHANGED_FILES_PY_FLAG}" ]; then pylint ${CHANGED_FILES_PY}; fi -.PHONY: lint -lint: _venv _lint ## Lint code and documentation +.PHONY: pylint +pylint: + if [ "${CHANGED_FILES_PY_FLAG}" ]; then pylint ${CHANGED_FILES_PY}; fi +.PHONY: mypy +mypy: + if [ "${CHANGED_FILES_PY_FLAG}" ]; then mypy ${CHANGED_FILES_PY}; fi -TEST_HOME := tests/ -MIN_COV := 80 -.PHONY: _test -_test: - coverage run -m pytest $(TEST_HOME) - coverage report .PHONY: test -test: _venv _test ## Run CLI unittests +test: _venv ## Run CLI unittests + coverage run + coverage report + - grep fail_under setup.cfg @@ -122,7 +119,7 @@ test: _venv _test ## Run CLI unittests .PHONY: ntsqlite/build ntsqlite/build: - $(PY_SYS_INTERPRETER) ntclient/ntsqlite/sql/__init__.py + ${PY_SYS_INTERPRETER} ntclient/ntsqlite/sql/__init__.py # TODO: nt-sqlite/test @@ -134,7 +131,7 @@ ntsqlite/build: .PHONY: _build _build: - $(PY_SYS_INTERPRETER) setup.py --quiet sdist + ${PY_SYS_INTERPRETER} setup.py --quiet sdist .PHONY: build build: ## Create sdist binary *.tar.gz @@ -142,11 +139,11 @@ build: _build clean .PHONY: install -install: ## pip install nutra - $(PY_SYS_INTERPRETER) -m pip install wheel - $(PY_SYS_INTERPRETER) -m pip install . || $(PY_SYS_INTERPRETER) -m pip install --user . - $(PY_SYS_INTERPRETER) -m pip show nutra - - $(PY_SYS_INTERPRETER) -c 'import shutil; print(shutil.which("nutra"));' +install: ## pip install . + ${PY_SYS_INTERPRETER} -m pip install wheel + ${PY_SYS_INTERPRETER} -m pip install . || ${PY_SYS_INTERPRETER} -m pip install --user . + ${PY_SYS_INTERPRETER} -m pip show nutra + - ${PY_SYS_INTERPRETER} -c 'import shutil; print(shutil.which("nutra"));' nutra -v @@ -155,6 +152,12 @@ install: ## pip install nutra # Clean # --------------------------------------- +RECURSIVE_CLEAN_LOCS ?= $(shell find ntclient/ tests/ \ +-name __pycache__ \ +-o -name .coverage \ +-o -name .mypy_cache \ +-o -name .pytest_cache) + .PHONY: clean clean: ## Clean up __pycache__ and leftover bits rm -f .coverage ntclient/ntsqlite/sql/nt.sqlite3 @@ -162,14 +165,7 @@ clean: ## Clean up __pycache__ and leftover bits rm -rf nutra.egg-info/ rm -rf .pytest_cache/ .mypy_cache/ # Recursively find & remove - find ntclient/ tests/ \ - -name \ - __pycache__ \ - -o -name \ - .coverage \ - -o -name .mypy_cache \ - -o -name .pytest_cache \ - | xargs rm -rf + if [ "${RECURSIVE_CLEAN_LOCS}" ]; then rm -rf ${RECURSIVE_CLEAN_LOCS}; fi @@ -177,15 +173,6 @@ clean: ## Clean up __pycache__ and leftover bits # Extras # --------------------------------------- -CLOC_ARGS ?= .PHONY: extras/cloc extras/cloc: ## Count lines of source code - - cloc \ - --exclude-dir=\ - .venv,venv,\ - .mypy_cache,.pytest_cache,\ - .idea,\ - build,dist \ - --exclude-ext=svg \ - $(CLOC_ARGS) \ - . + - cloc HEAD diff --git a/README.rst b/README.rst index e49ab11..50699c0 100644 --- a/README.rst +++ b/README.rst @@ -1,13 +1,13 @@ -************** - nutratracker -************** +******************** + nutratracker (cli) +******************** Command line tools for interacting with government food database, and analyzing your health trends. The ``SR28`` database includes data for ~8500 foods and ~180 nutrients. Customizable with extensions and mapping rules built on top. -**Requires** +**Requires:** - Python 3.4.0 or later (``lzma``, ``ssl`` & ``sqlite3`` modules) [Win XP / Ubuntu 14.04]. @@ -83,7 +83,7 @@ extension. direnv -You can add the direnv hook, ``direnv hook bash >>~.bashrc``. +You can add the ``direnv`` hook, ``direnv hook bash >>~/.bashrc``. Only run this once. @@ -93,6 +93,8 @@ Plugin Development You can develop plugins (or data modifications sets) that are imported and built on the base (or core) installation. +These currently can take the form of custom recipes, foods, and RDA injection. + Supporting Old Versions of Python ####################################################### @@ -107,18 +109,19 @@ Simply install them with this (inside your ``venv`` environment). This won't guarantee compatibility for every version, but it will help. We provide a wide range. The oldest version of ``tabulate`` is from 2013. -To use an old interpreter (Python 3.4 does not have the ``typing`` module! -Only ``collections.abc``.) you may need to use -a virtual machine or install old SSL libraries or enter a similar messy state. -My preference is for VirtualBox images, where -I manually test Windows XP & Ubuntu 14.04. +There is automated testing on GitHub, but to use an old interpreter +(Python 3.4 does not have the ``typing`` module! Only ``collections.abc``), +you may need to use a virtual machine or install old SSL libraries or enter a +similar messy state. +My preference is for VirtualBox images, where I manually test *Windows XP* +& *Ubuntu 14.04*. Notes ####################################################### On Windows you should check the box during the Python installer -to include ``Scripts`` directory in your ``$PATH``. This can be done +to include ``Scripts`` directory in your ``%PATH%``. This can be done manually after installation too. Windows users may also have differing results if they install for all users @@ -126,19 +129,19 @@ Windows users may also have differing results if they install for all users location of installed scripts, and affect the ``$PATH`` variable being correctly populated for prior installs. +Install the Levenshtein speedup with this. If it fails remove the ``[extras]``. + +.. code-block:: bash + + pip install nutra[extras] + Linux may need to install ``python-dev`` package to build ``python-Levenshtein``. -I am currently debating making this an optional dependency to avoid -confusing install failures for people without ``gcc`` or ``python3-dev``. - -I'm also currently working on doing phased installs of dependencies based on -the host Python version, since some of the old versions of pip have trouble -finding something that works, and again, spit out confusing errors. Windows users may not be able to install ``python-Levenshtein``. Main program works 100%, but ``test`` and ``lint`` may break on older operating -systems (Ubuntu 14.04, Windows XP). +systems (*Ubuntu 14.04*, *Windows XP*). Install PyPi release (from pip) @@ -153,6 +156,7 @@ Install PyPi release (from pip) Using the source code directly ####################################################### + Clone down, initialize ``nt-sqlite`` submodule, and install requirements: .. code-block:: bash @@ -165,6 +169,7 @@ Clone down, initialize ``nt-sqlite`` submodule, and install requirements: ./nutra -h + Initialize the DBs (``nt`` and ``usda``). .. code-block:: bash @@ -176,6 +181,7 @@ Initialize the DBs (``nt`` and ``usda``). make install n init + If installed (or inside ``cli``) folder, the program can also run with ``python -m ntclient``. @@ -235,6 +241,7 @@ I've run the command to seed the autocomplete script. mkdir -p $HOME/.bash_completion.d activate-global-python-argcomplete --user + And my ``~/.bashrc`` file looks like this. .. code-block:: bash @@ -246,8 +253,9 @@ And my ``~/.bashrc`` file looks like this. source ~/.bash_completion.d/python-argcomplete fi -**NOTE:** This is a work in progress, we are adding more autocomplete -functions. + +**NOTE:** Standard autocomplete is fully functional, we are adding customized +completions Currently Supported Data @@ -255,11 +263,11 @@ Currently Supported Data **USDA Stock database** -- Standard reference database (SR28) `[7794 foods]` +- Standard reference database (SR28) **[7794 foods]** -**Relative USDA Extensions** +**USDA Extensions (Relational)** -- Flavonoid, Isoflavonoids, and Proanthocyanidins `[1352 foods]` +- Flavonoid, Isoflavonoids, and Proanthocyanidins **[1352 foods]** Usage @@ -268,4 +276,4 @@ Usage Requires internet connection to download initial datasets. Run ``nutra init`` for this step. -Run the ``n`` script to output usage. +Run ``n`` or ``nutra`` to output usage (``-h`` flag is optional and defaulted). diff --git a/ntclient/__init__.py b/ntclient/__init__.py index df18834..eb03714 100644 --- a/ntclient/__init__.py +++ b/ntclient/__init__.py @@ -7,19 +7,16 @@ Created on Fri Jan 31 16:01:31 2020 @author: shane """ -import argparse import os import platform import shutil import sys -from enum import Enum from ntclient.ntsqlite.sql import NT_DB_NAME -from ntclient.utils import colors # Package info __title__ = "nutra" -__version__ = "0.2.7.dev0" +__version__ = "0.2.7.dev3" __author__ = "Shane Jaroch" __email__ = "chown_tee@proton.me" __license__ = "GPL v3" @@ -47,7 +44,7 @@ PY_MIN_STR = ".".join(str(x) for x in PY_MIN_VER) PY_SYS_STR = ".".join(str(x) for x in PY_SYS_VER) if PY_SYS_VER < PY_MIN_VER: # TODO: make this testable with: `class CliConfig`? - raise RuntimeError( + raise RuntimeError( # pragma: no cover "ERROR: %s requires Python %s or later to run" % (__title__, PY_MIN_STR), "HINT: You're running Python %s" % PY_SYS_STR, ) @@ -70,144 +67,6 @@ DEFAULT_SEARCH_H_BUFFER = ( ) -################################################################################ -# CLI config class (settings & preferences / defaults) -################################################################################ -class RdaColors(Enum): - """ - Stores values for report colors. - Default values: - Acceptable =Cyan - Overage =Magenta (Dim) - Low =Yellow - Critically Low =Red (Dim) - TODO: make configurable in SQLite or prefs.json - """ - - THRESH_WARN = 0.7 - THRESH_CRIT = 0.4 - THRESH_OVER = 1.9 - - COLOR_WARN = colors.COLOR_WARN - COLOR_CRIT = colors.COLOR_CRIT - COLOR_OVER = colors.COLOR_OVER - - COLOR_DEFAULT = colors.COLOR_DEFAULT - - STYLE_RESET_ALL = colors.STYLE_RESET_ALL - - # Used in macro bars - COLOR_YELLOW = colors.COLOR_YELLOW - COLOR_BLUE = colors.COLOR_BLUE - COLOR_RED = colors.COLOR_RED - - -# pylint: disable=too-few-public-methods,too-many-instance-attributes -class _CliConfig: - """Mutable global store for configuration values""" - - def __init__(self, debug: bool = False, paging: bool = True) -> None: - self.debug = debug - self.paging = paging - - # TODO: respect a prefs.json, or similar config file. - self.thresh_warn = RdaColors.THRESH_WARN.value - self.thresh_crit = RdaColors.THRESH_CRIT.value - self.thresh_over = RdaColors.THRESH_OVER.value - - self.color_warn = RdaColors.COLOR_WARN.value - self.color_crit = RdaColors.COLOR_CRIT.value - self.color_over = RdaColors.COLOR_OVER.value - self.color_default = RdaColors.COLOR_DEFAULT.value - - self.style_reset_all = RdaColors.STYLE_RESET_ALL.value - - self.color_yellow = RdaColors.COLOR_YELLOW.value - self.color_red = RdaColors.COLOR_RED.value - self.color_blue = RdaColors.COLOR_BLUE.value - - def set_flags(self, args: argparse.Namespace) -> None: - """ - Sets flags: - {DEBUG, PAGING} - from main (after arg parse). Accessible throughout package. - Must be re-imported globally. - """ - - self.debug = args.debug - self.paging = not args.no_pager - - if self.debug: - print("Console size: %sh x %sw" % (BUFFER_HT, BUFFER_WD)) - - -# Create the shared instance object -CLI_CONFIG = _CliConfig() - - -# TODO: -# Nested nutrient tree, like: -# http://www.whfoods.com/genpage.php?tname=nutrientprofile&dbid=132 -# Attempt to record errors in failed try/catch block (bottom of __main__.py) -# Make use of argcomplete.warn(msg) ? - - -################################################################################ -# Validation Enums -################################################################################ -class Gender(Enum): - """ - A validator and Enum class for gender inputs; used in several calculations. - @note: floating point -1 to 1, or 0 to 1... for non-binary? - """ - - MALE = "m" - FEMALE = "f" - - -class ActivityFactor(Enum): - """ - Used in BMR calculations. - Different activity levels: {0.200, 0.375, 0.550, 0.725, 0.900} - - Activity Factor\n - ------------------------\n - 0.200 = sedentary (little or no exercise) - - 0.375 = lightly active - (light exercise/sports 1-3 days/week, approx. 590 Cal/day) - - 0.550 = moderately active - (moderate exercise/sports 3-5 days/week, approx. 870 Cal/day) - - 0.725 = very active - (hard exercise/sports 6-7 days a week, approx. 1150 Cal/day) - - 0.900 = extremely active - (very hard exercise/sports and physical job, approx. 1580 Cal/day) - - @todo: Verify the accuracy of these "names". Access by index? - """ - - SEDENTARY = {1: 0.2} - MILDLY_ACTIVE = {2: 0.375} - ACTIVE = {3: 0.55} - HIGHLY_ACTIVE = {4: 0.725} - INTENSELY_ACTIVE = {5: 0.9} - - -def activity_factor_from_index(activity_factor: int) -> float: - """ - Gets ActivityFactor Enum by float value if it exists, else raise ValueError. - Basically just verifies the float is among the allowed values, and re-returns it. - """ - for enum_entry in ActivityFactor: - if activity_factor in enum_entry.value: - return float(enum_entry.value[activity_factor]) - # TODO: custom exception. And handle in main file? - raise ValueError("No such ActivityFactor for value: %s" % activity_factor) - - ################################################################################ # Nutrient IDs ################################################################################ diff --git a/ntclient/__main__.py b/ntclient/__main__.py index 73e1116..ccde961 100644 --- a/ntclient/__main__.py +++ b/ntclient/__main__.py @@ -15,7 +15,6 @@ from urllib.error import HTTPError, URLError import argcomplete from ntclient import ( - CLI_CONFIG, __db_target_nt__, __db_target_usda__, __email__, @@ -24,6 +23,7 @@ from ntclient import ( __version__, ) from ntclient.argparser import build_subcommands +from ntclient.utils import CLI_CONFIG from ntclient.utils.exceptions import SqlException @@ -53,7 +53,7 @@ def build_arg_parser() -> argparse.ArgumentParser: return arg_parser -def main(args: list = None) -> int: +def main(args: list = None) -> int: # type: ignore """ Main method for CLI @@ -67,7 +67,7 @@ def main(args: list = None) -> int: def parse_args() -> argparse.Namespace: """Returns parsed args""" if args is None: - return arg_parser.parse_args() + return arg_parser.parse_args() # type: ignore return arg_parser.parse_args(args=args) def func(parser: argparse.Namespace) -> tuple: @@ -100,20 +100,20 @@ def main(args: list = None) -> int: exit_code = 1 try: exit_code, *_results = func(_parser) - except SqlException as sql_exception: + except SqlException as sql_exception: # pragma: no cover print("Issue with an sqlite database: " + repr(sql_exception)) if CLI_CONFIG.debug: raise - except HTTPError as http_error: + except HTTPError as http_error: # pragma: no cover err_msg = "{0}: {1}".format(http_error.code, repr(http_error)) print("Server response error, try again: " + err_msg) if CLI_CONFIG.debug: raise - except URLError as url_error: + except URLError as url_error: # pragma: no cover print("Connection error, check your internet: " + repr(url_error.reason)) if CLI_CONFIG.debug: raise - except Exception as exception: # pylint: disable=broad-except + except Exception as exception: # pylint: disable=broad-except # pragma: no cover print("Unforeseen error, run with -d for more info: " + repr(exception)) print("You can open an issue here: %s" % __url__) print("Or send me an email with the debug output: %s" % __email__) diff --git a/ntclient/argparser/__init__.py b/ntclient/argparser/__init__.py index 698c339..86a5f67 100644 --- a/ntclient/argparser/__init__.py +++ b/ntclient/argparser/__init__.py @@ -190,7 +190,7 @@ def build_calc_subcommand(subparsers: argparse._SubParsersAction) -> None: "calc", help="calculate 1-rep max, body fat, BMR, etc." ) - calc_subparsers = calc_parser.add_subparsers(title="recipe subcommands") + calc_subparsers = calc_parser.add_subparsers(title="calc subcommands") calc_parser.set_defaults(func=calc_parser.print_help) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -257,43 +257,43 @@ def build_calc_subcommand(subparsers: argparse._SubParsersAction) -> None: "chest", type=int, nargs="?", - help="pectoral (mm) -[3-Site skin caliper measurement]", + help="pectoral (mm) -- [3-Site skin caliper measurement]", ) calc_bf_parser.add_argument( "abd", type=int, nargs="?", - help="abdominal (mm) [3-Site skin caliper measurement]", + help="abdominal (mm) - [3-Site skin caliper measurement]", ) calc_bf_parser.add_argument( "thigh", type=int, nargs="?", - help="thigh (mm) --- [3-Site skin caliper measurement]", + help="thigh (mm) ----- [3-Site skin caliper measurement]", ) calc_bf_parser.add_argument( "tricep", type=int, nargs="?", - help="triceps (mm) - [7-Site skin caliper measurement]", + help="triceps (mm) ---- [7-Site skin caliper measurement]", ) calc_bf_parser.add_argument( "sub", type=int, nargs="?", - help="sub (mm) ----- [7-Site skin caliper measurement]", + help="sub (mm) -------- [7-Site skin caliper measurement]", ) calc_bf_parser.add_argument( "sup", type=int, nargs="?", - help="sup (mm) ----- [7-Site skin caliper measurement]", + help="sup (mm) -------- [7-Site skin caliper measurement]", ) calc_bf_parser.add_argument( "mid", type=int, nargs="?", - help="mid (mm) ----- [7-Site skin caliper measurement]", + help="mid (mm) -------- [7-Site skin caliper measurement]", ) calc_bf_parser.set_defaults(func=parser_funcs.calc_body_fat) diff --git a/ntclient/argparser/funcs.py b/ntclient/argparser/funcs.py index a9ac713..f295730 100644 --- a/ntclient/argparser/funcs.py +++ b/ntclient/argparser/funcs.py @@ -16,8 +16,8 @@ from tabulate import tabulate import ntclient.services.analyze import ntclient.services.recipe.utils import ntclient.services.usda -from ntclient import CLI_CONFIG, Gender, activity_factor_from_index from ntclient.services import calculate as calc +from ntclient.utils import CLI_CONFIG, Gender, activity_factor_from_index def init(args: argparse.Namespace) -> tuple: @@ -126,7 +126,7 @@ def calc_1rm(args: argparse.Namespace) -> tuple: # TODO: fourth column: average or `avg` column too. # Prepare table rows, to display all 3 results in one table _all = [] - for _rep in _epley.keys(): + for _rep in sorted(_epley.keys()): # NOTE: dicts not sorted prior to 3.7 row = [_rep] for _calc, _values in result.items(): # Round down for now @@ -235,6 +235,8 @@ def calc_body_fat(args: argparse.Namespace) -> tuple: } """ + print("HINT: re-run with '-h' to show usage.") + print(os.linesep + "INPUTS" + os.linesep + "------") gender = Gender.FEMALE if args.female_gender else Gender.MALE print("Gender: %s" % gender) try: diff --git a/ntclient/models/__init__.py b/ntclient/models/__init__.py index c4ed89e..7a8b378 100644 --- a/ntclient/models/__init__.py +++ b/ntclient/models/__init__.py @@ -7,7 +7,7 @@ Classes, structures for storing, displaying, and editing data. """ import csv -from ntclient import CLI_CONFIG +from ntclient.utils import CLI_CONFIG class Recipe: diff --git a/ntclient/ntsqlite b/ntclient/ntsqlite index 0eb89de..e69368f 160000 --- a/ntclient/ntsqlite +++ b/ntclient/ntsqlite @@ -1 +1 @@ -Subproject commit 0eb89de7db73aa68d71da3b4113296ba595500bd +Subproject commit e69368ff9a64db7134a212686c08922c6537bcee diff --git a/ntclient/persistence/sql/__init__.py b/ntclient/persistence/sql/__init__.py index 9d603b9..944b0f8 100644 --- a/ntclient/persistence/sql/__init__.py +++ b/ntclient/persistence/sql/__init__.py @@ -2,10 +2,11 @@ import sqlite3 from collections.abc import Sequence +from ntclient.utils import CLI_CONFIG + # ------------------------------------------------ # Entry fetching methods # ------------------------------------------------ -from ntclient import CLI_CONFIG def sql_entries(sql_result: sqlite3.Cursor) -> list: diff --git a/ntclient/persistence/sql/usda/__init__.py b/ntclient/persistence/sql/usda/__init__.py index b210326..f63fd2b 100644 --- a/ntclient/persistence/sql/usda/__init__.py +++ b/ntclient/persistence/sql/usda/__init__.py @@ -32,7 +32,7 @@ def usda_init(yes: bool = False) -> None: # Extract the archive with tarfile.open(save_path, mode="r:xz") as usda_sqlite_file: print("\n" + "tar xvf %s.tar.xz" % USDA_DB_NAME) - usda_sqlite_file.extractall(NUTRA_HOME) + usda_sqlite_file.extractall(NUTRA_HOME) # nosec: B202 print("==> done downloading %s" % USDA_DB_NAME) diff --git a/ntclient/persistence/sql/usda/funcs.py b/ntclient/persistence/sql/usda/funcs.py index cc7c5c8..3442232 100644 --- a/ntclient/persistence/sql/usda/funcs.py +++ b/ntclient/persistence/sql/usda/funcs.py @@ -1,4 +1,5 @@ """usda.sqlite functions module""" + from ntclient import NUTR_ID_KCAL from ntclient.persistence.sql.usda import sql, sql_headers @@ -14,7 +15,7 @@ def sql_fdgrp() -> dict: return {x[0]: x for x in result} -def sql_food_details(_food_ids: set = None) -> list: +def sql_food_details(_food_ids: set = None) -> list: # type: ignore """Readable human details for foods""" if not _food_ids: diff --git a/ntclient/services/analyze.py b/ntclient/services/analyze.py index 4b533db..6824f3e 100644 --- a/ntclient/services/analyze.py +++ b/ntclient/services/analyze.py @@ -12,7 +12,6 @@ from tabulate import tabulate from ntclient import ( BUFFER_WD, - CLI_CONFIG, NUTR_ID_CARBS, NUTR_ID_FAT_TOT, NUTR_ID_FIBER, @@ -25,6 +24,7 @@ from ntclient.persistence.sql.usda.funcs import ( sql_nutrients_overview, sql_servings, ) +from ntclient.utils import CLI_CONFIG ################################################################################ diff --git a/ntclient/services/calculate.py b/ntclient/services/calculate.py index 5ff1cae..ff4e428 100644 --- a/ntclient/services/calculate.py +++ b/ntclient/services/calculate.py @@ -9,7 +9,7 @@ Created on Tue Aug 11 20:53:14 2020 import argparse import math -from ntclient import Gender +from ntclient.utils import Gender # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # 1 rep max @@ -431,7 +431,7 @@ def lbl_eric_helms(height: float, args: argparse.Namespace) -> dict: try: desired_bf = float(args.desired_bf) * 100 - except (KeyError, TypeError): + except (KeyError, TypeError): # pragma: no cover return {"errMsg": "Eric Helms failed, requires: height, desired_bf."} _min = round(4851.00 * height * 0.01 * height * 0.01 / (100.0 - desired_bf), 1) @@ -460,7 +460,7 @@ def lbl_casey_butt(height: float, args: argparse.Namespace) -> dict: wrist = float(args.wrist) / 2.54 # convert cm --> inches ankle = float(args.ankle) / 2.54 # convert cm --> inches - except (KeyError, TypeError): + except (KeyError, TypeError): # pragma: no cover return { "errMsg": "Casey Butt failed, requires: height, desired_bf, wrist, & ankle." } diff --git a/ntclient/services/recipe/csv_utils.py b/ntclient/services/recipe/csv_utils.py index 39d48b0..c4faa1c 100644 --- a/ntclient/services/recipe/csv_utils.py +++ b/ntclient/services/recipe/csv_utils.py @@ -15,7 +15,7 @@ from ntclient.utils import tree def csv_files() -> list: - """Returns full filenames for everything under RECIPE_HOME'""" + """Returns full filenames for everything under RECIPE_HOME""" return glob.glob(RECIPE_HOME + "/**/*.csv", recursive=True) diff --git a/ntclient/services/usda.py b/ntclient/services/usda.py index cefeb9b..b6face2 100644 --- a/ntclient/services/usda.py +++ b/ntclient/services/usda.py @@ -10,7 +10,6 @@ import pydoc from tabulate import tabulate from ntclient import ( - CLI_CONFIG, DEFAULT_RESULT_LIMIT, DEFAULT_SEARCH_H_BUFFER, DEFAULT_SORT_H_BUFFER, @@ -25,6 +24,7 @@ from ntclient.persistence.sql.usda.funcs import ( sql_nutrients_overview, sql_sort_helper1, ) +from ntclient.utils import CLI_CONFIG def list_nutrients() -> tuple: diff --git a/ntclient/utils/__init__.py b/ntclient/utils/__init__.py index e69de29..042f66f 100644 --- a/ntclient/utils/__init__.py +++ b/ntclient/utils/__init__.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Sun Mar 26 23:07:30 2023 + +@author: shane +""" +import argparse +from enum import Enum + +from ntclient import BUFFER_HT, BUFFER_WD +from ntclient.utils import colors + + +################################################################################ +# CLI config class (settings & preferences / defaults) +################################################################################ +class RdaColors(Enum): + """ + Stores values for report colors. + Default values: + Acceptable =Cyan + Overage =Magenta (Dim) + Low =Yellow + Critically Low =Red (Dim) + TODO: make configurable in SQLite or prefs.json + """ + + THRESH_WARN = 0.7 + THRESH_CRIT = 0.4 + THRESH_OVER = 1.9 + + COLOR_WARN = colors.COLOR_WARN + COLOR_CRIT = colors.COLOR_CRIT + COLOR_OVER = colors.COLOR_OVER + + COLOR_DEFAULT = colors.COLOR_DEFAULT + + STYLE_RESET_ALL = colors.STYLE_RESET_ALL + + # Used in macro bars + COLOR_YELLOW = colors.COLOR_YELLOW + COLOR_BLUE = colors.COLOR_BLUE + COLOR_RED = colors.COLOR_RED + + +# pylint: disable=too-few-public-methods,too-many-instance-attributes +class _CliConfig: + """Mutable global store for configuration values""" + + def __init__(self, debug: bool = False, paging: bool = True) -> None: + self.debug = debug + self.paging = paging + + # TODO: respect a prefs.json, or similar config file. + self.thresh_warn = RdaColors.THRESH_WARN.value + self.thresh_crit = RdaColors.THRESH_CRIT.value + self.thresh_over = RdaColors.THRESH_OVER.value + + self.color_warn = RdaColors.COLOR_WARN.value + self.color_crit = RdaColors.COLOR_CRIT.value + self.color_over = RdaColors.COLOR_OVER.value + self.color_default = RdaColors.COLOR_DEFAULT.value + + self.style_reset_all = RdaColors.STYLE_RESET_ALL.value + + self.color_yellow = RdaColors.COLOR_YELLOW.value + self.color_red = RdaColors.COLOR_RED.value + self.color_blue = RdaColors.COLOR_BLUE.value + + def set_flags(self, args: argparse.Namespace) -> None: + """ + Sets flags: + {DEBUG, PAGING} + from main (after arg parse). Accessible throughout package. + Must be re-imported globally. + """ + + self.debug = args.debug + self.paging = not args.no_pager + + if self.debug: + print("Console size: %sh x %sw" % (BUFFER_HT, BUFFER_WD)) + + +# Create the shared instance object +CLI_CONFIG = _CliConfig() + + +# TODO: +# Nested nutrient tree, like: +# http://www.whfoods.com/genpage.php?tname=nutrientprofile&dbid=132 +# Attempt to record errors in failed try/catch block (bottom of __main__.py) +# Make use of argcomplete.warn(msg) ? + + +################################################################################ +# Validation Enums +################################################################################ +class Gender(Enum): + """ + A validator and Enum class for gender inputs; used in several calculations. + @note: floating point -1 to 1, or 0 to 1... for non-binary? + """ + + MALE = "m" + FEMALE = "f" + + +class ActivityFactor(Enum): + """ + Used in BMR calculations. + Different activity levels: {0.200, 0.375, 0.550, 0.725, 0.900} + + Activity Factor\n + ------------------------\n + 0.200 = sedentary (little or no exercise) + + 0.375 = lightly active + (light exercise/sports 1-3 days/week, approx. 590 Cal/day) + + 0.550 = moderately active + (moderate exercise/sports 3-5 days/week, approx. 870 Cal/day) + + 0.725 = very active + (hard exercise/sports 6-7 days a week, approx. 1150 Cal/day) + + 0.900 = extremely active + (very hard exercise/sports and physical job, approx. 1580 Cal/day) + + @todo: Verify the accuracy of these "names". Access by index? + """ + + SEDENTARY = {1: 0.2} + MILDLY_ACTIVE = {2: 0.375} + ACTIVE = {3: 0.55} + HIGHLY_ACTIVE = {4: 0.725} + INTENSELY_ACTIVE = {5: 0.9} + + +def activity_factor_from_index(activity_factor: int) -> float: + """ + Gets ActivityFactor Enum by float value if it exists, else raise ValueError. + Basically just verifies the float is among the allowed values, and re-returns it. + """ + for enum_entry in ActivityFactor: + if activity_factor in enum_entry.value: + return float(enum_entry.value[activity_factor]) + # TODO: custom exception. And handle in main file? + raise ValueError( # pragma: no cover + "No such ActivityFactor for value: %s" % activity_factor + ) diff --git a/ntclient/utils/colors.py b/ntclient/utils/colors.py index d319e5e..13b84bb 100644 --- a/ntclient/utils/colors.py +++ b/ntclient/utils/colors.py @@ -4,52 +4,15 @@ Created on Mon Aug 8 14:35:43 2022 @author: shane -Allows the safe avoidance of ImportError on non-colorama capable systems. +Wrapper for colorama. Used to default to white text if it wasn't installed, +but it's available on virtually all systems now so why not. """ +from colorama import Fore, Style +from colorama import init as colorama_init -# pylint: disable=invalid-name +# Made it this far, so run the init function (which is needed on Windows) +colorama_init() - -# pylint: disable=too-few-public-methods -class _STYLE: - def __init__(self) -> None: - self.BRIGHT = str() - self.DIM = str() - self.RESET_ALL = str() - - -# pylint: disable=too-few-public-methods,too-many-instance-attributes -class _FORE: - def __init__(self) -> None: - self.WARN = str() - self.CRIT = str() - self.OVER = str() - self.DEFAULT = str() - - self.YELLOW = str() - self.BLUE = str() - self.RED = str() - self.MAGENTA = str() - - self.GREEN = str() - self.CYAN = str() - - -_Style = _STYLE() -_Fore = _FORE() - -try: - from colorama import Fore, Style - from colorama import init as colorama_init - - # Made it this far, so run the init function (which is needed on Windows) - colorama_init() - -except ImportError: - Fore, Style = _Fore, _Style # type: ignore - - -# NOTE: These will all just be empty strings if colorama isn't installed # Styles STYLE_BRIGHT = Style.BRIGHT STYLE_DIM = Style.DIM diff --git a/ntclient/utils/tree.py b/ntclient/utils/tree.py index b994090..9a50a7e 100644 --- a/ntclient/utils/tree.py +++ b/ntclient/utils/tree.py @@ -93,13 +93,13 @@ def print_dir(_dir: str, pre: str = str()) -> tuple: return (n_dirs, n_files, n_size) -def main_tree(_args: list = None) -> int: +def main_tree(_args: list = None) -> int: # type: ignore """Handle input arguments, print off tree""" n_dirs = 0 n_files = 0 - if not _args: - _args = sys.argv + if _args is None: + _args = sys.argv # type: ignore if len(_args) == 1: # Used for development diff --git a/requirements-lint.txt b/requirements-lint.txt index 4120b78..2cab066 100644 --- a/requirements-lint.txt +++ b/requirements-lint.txt @@ -1,11 +1,10 @@ -autopep8>=1.6 -bandit>=1.7 -black>=22.3 -doc8>=0.11 -flake8>=4.0 -mypy>=0.960 -pylint>=2.13 -types-colorama>=0.4.15 -types-psycopg2>=2.9.18 -types-setuptools>=57.4.0 -types-tabulate>=0.8.11 +bandit==1.7.5 +black==23.3.0 +doc8==1.1.1 +flake8==6.0.0 +mypy==1.1.1 +pylint==2.17.1 +types-colorama==0.4.15.11 +types-psycopg2==2.9.21.9 +types-setuptools==67.6.0.6 +types-tabulate==0.9.0.2 diff --git a/requirements-old.txt b/requirements-old.txt index 73afc09..8885001 100644 --- a/requirements-old.txt +++ b/requirements-old.txt @@ -1,4 +1,4 @@ -argcomplete==1.8.0 -colorama==0.3.6 +argcomplete<=1.8.2 +colorama<=0.3.6 fuzzywuzzy==0.3.0 -tabulate==0.4.3 +tabulate<=0.4.3 diff --git a/requirements-optional.txt b/requirements-optional.txt index 3e3e163..453c47b 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -1 +1 @@ -python-Levenshtein==0.12.2 +python-Levenshtein<=0.12.2 diff --git a/requirements-test.txt b/requirements-test.txt index c4985c9..86c8192 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,2 +1,2 @@ -coverage>=6.0 -pytest>=7.0 +coverage>=6.2 +pytest>=7.0.1 diff --git a/requirements.txt b/requirements.txt index 1548cc4..6a6c215 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -argcomplete>=1.8.0 -colorama>=0.3.6,<=0.4.1 +argcomplete>=1.8.2,<=1.12.3 +colorama>=0.1.17,<=0.4.1 fuzzywuzzy>=0.3.0 -tabulate>=0.4.3 +tabulate>=0.4.3,<=0.8.9 diff --git a/scripts/n b/scripts/n new file mode 100755 index 0000000..a1b3619 --- /dev/null +++ b/scripts/n @@ -0,0 +1,12 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# PYTHON_ARGCOMPLETE_OK +"""Executable script, copied over by pip""" +import re +import sys + +from ntclient.__main__ import main + +if __name__ == "__main__": + sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) + sys.exit(main()) diff --git a/setup.cfg b/setup.cfg index 33f211e..377db12 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,9 +1,16 @@ +[tool:pytest] +# See: https://docs.pytest.org/en/7.1.x/reference/customize.html +testpaths = + tests + [coverage:run] +# See: https://coverage.readthedocs.io/en/7.2.2/config.html#run +command_line = -m pytest source = ntclient [coverage:report] -fail_under = 80 -; precision = 2 +fail_under = 90.00 +precision = 2 show_missing = True skip_empty = True @@ -14,6 +21,9 @@ omit = # It directly imports the `build_ntsqlite()` function. ntclient/ntsqlite/sql/__main__.py, +exclude_lines = + pragma: no cover + [pycodestyle] @@ -29,7 +39,8 @@ per-file-ignores = max-line-length = 88 ignore = - W503, # line break before binary operator + # line break before binary operator + W503, @@ -53,6 +64,9 @@ disallow_untyped_defs = True disallow_untyped_calls = True disallow_untyped_decorators = True +;strict_optional = True +no_implicit_optional = True + warn_return_any = True warn_redundant_casts = True warn_unreachable = True @@ -61,7 +75,7 @@ warn_unused_ignores = True warn_unused_configs = True warn_incomplete_stub = True -# Our test, they don't return a value typically +# Our tests, they don't return a value typically [mypy-tests.*] disallow_untyped_defs = False @@ -70,5 +84,5 @@ disallow_untyped_defs = False ignore_missing_imports = True # 3rd party packages missing types -[mypy-argcomplete,coverage,fuzzywuzzy] +[mypy-argcomplete,fuzzywuzzy] ignore_missing_imports = True diff --git a/setup.py b/setup.py index 4a9f1ec..02b230d 100644 --- a/setup.py +++ b/setup.py @@ -4,6 +4,7 @@ Created on Sat Oct 13 16:30:30 2018 @author: shane """ +import glob import os import platform @@ -49,11 +50,9 @@ with open("README.rst", encoding="utf-8") as file: with open("requirements.txt", encoding="utf-8") as file: REQUIREMENTS = file.read().split() -if PLATFORM_SYSTEM != "Windows" or int(os.getenv("NUTRA_OS_FORCE_OPT_REQS", str(0))): - # python-Levenshtein builds natively on Unix; Windows needs vcvarsall.bat or vc++10 - with open("requirements-optional.txt", encoding="utf-8") as file: - optional_reqs = file.read().split() - REQUIREMENTS.extend(optional_reqs) + +with open("requirements-optional.txt", encoding="utf-8") as file: + REQUIREMENTS_EXTRA = file.read().split() # Setup method setup( @@ -62,13 +61,15 @@ setup( author_email=__email__, classifiers=CLASSIFIERS, install_requires=REQUIREMENTS, + extras_require={"extras": REQUIREMENTS_EXTRA}, python_requires=">=%s" % PY_MIN_STR, zip_safe=False, - packages=find_packages(exclude=["tests", "ntclient.docs"]), + packages=find_packages(exclude=["tests*"]), include_package_data=True, - entry_points={ - "console_scripts": ["nutra=ntclient.__main__:main", "n=ntclient.__main__:main"] - }, + # Linux / macOS argcomplete compatible script "n" + scripts=glob.glob("scripts/*"), + # Windows compatible nutra.exe + entry_points={"console_scripts": ["nutra=ntclient.__main__:main"]}, platforms=["linux", "darwin", "win32"], description="Home and office nutrient tracking software", long_description=README, diff --git a/tests/resources/day/dog-simple.csv b/tests/resources/day/dog-simple.csv index e91283b..b099ada 100644 --- a/tests/resources/day/dog-simple.csv +++ b/tests/resources/day/dog-simple.csv @@ -1,14 +1,14 @@ -meal,subcat,id,grams,notes,desired_kcal -breakfast,base,20044,90,"rice, uncooked [55g cooked]",120 -breakfast,base,16042,50,"pinto beans, raw [cooked: ¼ cup or 56g]",60 -breakfast,base,5062,50,chicken breast,50 -breakfast,cooked (veg),11507,25,"sweet potato, raw",20 -breakfast,cooked (veg),11233,10,"kale, raw OR broccoli, boiled, w/o salt",5 -breakfast,soaked,12220,10,flax [½ tbsp],30 -breakfast,chopped,11297,3,"parsley, fresh",1 -breakfast,supplements,,0.075,milk thistle extract, -breakfast,supplements,,0.15,"calcium citrate, [1/8 tsp]", -,,,,, -,,,,, -,,,,, +meal,subcat,id,grams,notes,desired_kcal +breakfast,base,20044,90,"rice, uncooked [55g cooked]",120 +breakfast,base,16042,50,"pinto beans, raw [cooked: ¼ cup or 56g]",60 +breakfast,base,5062,50,chicken breast,50 +breakfast,cooked (veg),11507,25,"sweet potato, raw",20 +breakfast,cooked (veg),11233,10,"kale, raw OR broccoli, boiled, w/o salt",5 +breakfast,soaked,12220,10,flax [½ tbsp],30 +breakfast,chopped,11297,3,"parsley, fresh",1 +breakfast,supplements,,0.075,milk thistle extract, +breakfast,supplements,,0.15,"calcium citrate, [1/8 tsp]", +,,,,, +,,,,, +,,,,, TOTAL,,,,,286 \ No newline at end of file diff --git a/tests/test_cli.py b/tests/test_cli.py index c46ee84..6623e0e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -14,7 +14,6 @@ import unittest import pytest from ntclient import ( - CLI_CONFIG, NTSQLITE_BUILDPATH, NUTRA_HOME, USDA_DB_NAME, @@ -32,6 +31,7 @@ from ntclient.persistence.sql.usda import sql as _usda_sql from ntclient.persistence.sql.usda import usda_ver from ntclient.services import init, usda from ntclient.services.recipe import RECIPE_HOME +from ntclient.utils import CLI_CONFIG from ntclient.utils.exceptions import SqlInvalidVersionError TEST_HOME = os.path.dirname(os.path.abspath(__file__)) @@ -161,6 +161,7 @@ class TestCli(unittest.TestCase): assert len(nutrients_rows[0]) == 30 assert len(servings_rows[0]) == 1 + # pylint: disable=too-many-statements def test_410_nt_argparser_funcs(self): """ Tests nt functions in argparser.funcs (to varying degrees each) @@ -426,7 +427,7 @@ class TestCli(unittest.TestCase): pytest.xfail("PermissionError, are you using Microsoft Windows?") # mocks input, could also pass `-y` flag or set yes=True - usda.input = lambda x: "y" + usda.input = lambda x: "y" # pylint: disable=redefined-builtin code, successful = init() assert code == 0 -- 2.52.0