assert_used: # B101
skips: ["*/test_*.py"]
-
-source .venv/bin/activate
+source .venv/bin/activate || source .venv/Scripts/activate
unset PS1
+# Line ending configuration
+* text eol=lf
+
+# GitHub Linguist (language detection overrides)
*.wls linguist-language=Mathematica
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
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-
- name: Basic Tests / CLI / Integration
run: |
n -v
- nutra -d recipe init
+ nutra -d recipe init -f
nutra --no-pager recipe
--- /dev/null
+---
+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
jobs:
lint:
- runs-on: ubuntu-latest
+ runs-on: [ubuntu-latest]
+
+ env:
+ SKIP_VENV: 1
steps:
- name: Checkout
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: |
pip install -r requirements-test.txt
- name: Lint
- run: make _lint
+ run: make lint
---
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
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
__sha__.py
*.sqlite
.nutra*
-
+.env*
# Temporary
docs*
Shane <chown_tee@proton.me> <nutratracker@protonmail.com>
Shane <chown_tee@proton.me> <shane.jaroch@centurylink.com>
Shane <chown_tee@proton.me> <sjj.lumen@gmail.com>
-
[MASTER]
-fail-under=9.75
+fail-under=9.93
[MESSAGES CONTROL]
+[0.2.8] - Unreleased
+########################################################################
+
+
+
+[0.2.7] - 2023-03-27
+########################################################################
+
+Changed
+~~~~~~~
+
+- Tweaked dependency versions to work with most older stuff (wip still testing)
+- ``n`` vs. ``nutra`` different strategies to work on both Windows/Unix
+- Install Levenshtein speedup with ``pip install --pre nutra[extras]``
+
+Fixed
+~~~~~
+
+- Added backs scripts to fix ``argcomplete`` in different edge cases
+- A couple spots where unsorted dictionaries were causing bugs on older python
+- Enhanced some developer experience things (Makefile, GitHub CI/workflows)
+
+Removed
+~~~~~~~
+
+- PyPI builds exclude ``tests/`` and unneeded ``requirements-*.txt files``
+
+
+
[0.2.6] - 2022-08-08
########################################################################
include requirements.txt
-include requirements-*.txt
+include requirements-old.txt
+include requirements-optional.txt
include ntclient/ntsqlite/sql/*.sql
include ntclient/ntsqlite/sql/data/*.csv
# NOTE: must put a <TAB> 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 <command>, 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" ]
endif
PY_VIRTUAL_INTERPRETER ?= python
-
PIP ?= $(PY_VIRTUAL_INTERPRETER) -m pip
REQ_OPT := requirements-optional.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}
# ---------------------------------------
# ---------------------------------------
.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
.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
.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
.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
# 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
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
# 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
-**************
- 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].
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.
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
#######################################################
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
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)
Using the source code directly
#######################################################
+
Clone down, initialize ``nt-sqlite`` submodule, and install requirements:
.. code-block:: bash
./nutra -h
+
Initialize the DBs (``nt`` and ``usda``).
.. code-block:: bash
make install
n init
+
If installed (or inside ``cli``) folder, the program can also run
with ``python -m ntclient``.
mkdir -p $HOME/.bash_completion.d
activate-global-python-argcomplete --user
+
And my ``~/.bashrc`` file looks like this.
.. code-block:: bash
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
**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
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).
@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"
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,
)
)
-################################################################################
-# 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
################################################################################
import argcomplete
from ntclient import (
- CLI_CONFIG,
__db_target_nt__,
__db_target_usda__,
__email__,
__version__,
)
from ntclient.argparser import build_subcommands
+from ntclient.utils import CLI_CONFIG
from ntclient.utils.exceptions import SqlException
return arg_parser
-def main(args: list = None) -> int:
+def main(args: list = None) -> int: # type: ignore
"""
Main method for CLI
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:
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__)
"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)
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"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)
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:
# 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
}
"""
+ 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:
"""
import csv
-from ntclient import CLI_CONFIG
+from ntclient.utils import CLI_CONFIG
class Recipe:
-Subproject commit 0eb89de7db73aa68d71da3b4113296ba595500bd
+Subproject commit e69368ff9a64db7134a212686c08922c6537bcee
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:
# 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)
"""usda.sqlite functions module"""
+
from ntclient import NUTR_ID_KCAL
from ntclient.persistence.sql.usda import sql, sql_headers
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:
from ntclient import (
BUFFER_WD,
- CLI_CONFIG,
NUTR_ID_CARBS,
NUTR_ID_FAT_TOT,
NUTR_ID_FIBER,
sql_nutrients_overview,
sql_servings,
)
+from ntclient.utils import CLI_CONFIG
################################################################################
import argparse
import math
-from ntclient import Gender
+from ntclient.utils import Gender
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 1 rep max
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)
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."
}
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)
from tabulate import tabulate
from ntclient import (
- CLI_CONFIG,
DEFAULT_RESULT_LIMIT,
DEFAULT_SEARCH_H_BUFFER,
DEFAULT_SORT_H_BUFFER,
sql_nutrients_overview,
sql_sort_helper1,
)
+from ntclient.utils import CLI_CONFIG
def list_nutrients() -> tuple:
+#!/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
+ )
@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
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
-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
-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
-python-Levenshtein==0.12.2
+python-Levenshtein<=0.12.2
-coverage>=6.0
-pytest>=7.0
+coverage>=6.2
+pytest>=7.0.1
-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
--- /dev/null
+#!/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())
+[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
# It directly imports the `build_ntsqlite()` function.
ntclient/ntsqlite/sql/__main__.py,
+exclude_lines =
+ pragma: no cover
+
[pycodestyle]
max-line-length = 88
ignore =
- W503, # line break before binary operator
+ # line break before binary operator
+ W503,
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
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
ignore_missing_imports = True
# 3rd party packages missing types
-[mypy-argcomplete,coverage,fuzzywuzzy]
+[mypy-argcomplete,fuzzywuzzy]
ignore_missing_imports = True
@author: shane
"""
+import glob
import os
import platform
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(
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,
-meal,subcat,id,grams,notes,desired_kcal\r
-breakfast,base,20044,90,"rice, uncooked [55g cooked]",120\r
-breakfast,base,16042,50,"pinto beans, raw [cooked: ¼ cup or 56g]",60\r
-breakfast,base,5062,50,chicken breast,50\r
-breakfast,cooked (veg),11507,25,"sweet potato, raw",20\r
-breakfast,cooked (veg),11233,10,"kale, raw OR broccoli, boiled, w/o salt",5\r
-breakfast,soaked,12220,10,flax [½ tbsp],30\r
-breakfast,chopped,11297,3,"parsley, fresh",1\r
-breakfast,supplements,,0.075,milk thistle extract,\r
-breakfast,supplements,,0.15,"calcium citrate, [1/8 tsp]",\r
-,,,,,\r
-,,,,,\r
-,,,,,\r
+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
import pytest
from ntclient import (
- CLI_CONFIG,
NTSQLITE_BUILDPATH,
NUTRA_HOME,
USDA_DB_NAME,
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__))
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)
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