[*.md]
-max_line_length = 90
+max_line_length = 85
trim_trailing_whitespace = false
+++ /dev/null
----
-name: coveralls
-
-"on":
- push: {}
-
-jobs:
- cov-submit:
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout
- uses: actions/checkout@v3
- with:
- submodules: recursive
-
- - name: Reload Cache / pip
- uses: actions/cache@v3
- with:
- path: ~/.cache/pip
- key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
- restore-keys: |
- ${{ runner.os }}-pip-
-
- - name: Install requirements
- run: |
- pip install coveralls==3.3.1
- pip install -r requirements.txt
- pip install -r requirements-test.txt
-
- # TODO: Tests for: python-argcomplete (tab-completion)
- - name: Test
- run: |
- export NUTRA_HOME=$(pwd)/tests/.nutra.test
- make _test
-
- - name: Submit coverage report / coveralls
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: coveralls --service=github
---
-name: test-linux
+name: install-linux
"on":
push: {}
strategy:
matrix:
- python-version: ["3.4.10", "3.6", "3.8", "3.10"]
+ python-version: ["3.4", "3.6", "3.8", "3.10"]
steps:
- name: Checkout
- 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 -d recipe init
nutra --no-pager recipe
- nutra day tests/resources/day/human-test.csv
+++ /dev/null
----
-name: test-win32
-
-"on":
- push: {}
-
-jobs:
- windows-latest:
- runs-on: windows-latest
-
- steps:
- - name: Configure Line Endings / git / LF
- run: |
- git config --global core.autocrlf input
- git config --global core.eol lf
-
- - name: Checkout
- uses: actions/checkout@v3
- with:
- submodules: recursive
-
- - name: Reload Cache / pip
- uses: actions/cache@v3
- with:
- path: ~\AppData\Local\pip\Cache
- 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
--- /dev/null
+---
+name: test
+
+"on":
+ push: {}
+
+jobs:
+ test:
+ runs-on: [self-hosted, dev-east]
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ submodules: recursive
+
+ - name: Cloc
+ 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
+
+ - name: Test
+ run: PATH=~/.local/bin:$PATH make _test
+
+ - name: Submit coverage report / coveralls
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: PATH=~/.local/bin:$PATH coveralls --service=github
[MASTER]
-fail-under=9.5
+fail-under=9.75
[MESSAGES CONTROL]
-***********
- ChangeLog
-***********
+************
+ Change Log
+************
All notable changes to this project will be documented in this file.
Added
~~~~~
-- Basic functionality of ``import`` and ``export`` sub-commands.
+- Recipes are stored in a ``csv`` format now with ``uuid`` instead of ``int``,
+ and they can be viewed in a convenient ``tree`` output.
+- Calculate functions for body fat, BMR, 1-rep max, & lean body limits.
+- Example recipe ``csv`` files.
+- ``unittest`` compatibility, not sure this will stay.
+ May revert to ``pytest``.
+
+Changed
+~~~~~~~
+
+- Use a ``CliConfig`` class for storing ``DEBUG`` and ``PAGING`` flags.
+- Requirements versions for ``colorama`` and ``python3.4``.
+- Removed ``F401`` warnings, e.g. importing services into ``__init__``.
+- General refactor of ``argparse`` stuff, but still not complete.
+- Start to split apart some of the original ``test_cli`` functions.
+- Enable more verbose ``mypy`` flags.
include ntclient/ntsqlite/sql/*.sql
include ntclient/ntsqlite/sql/data/*.csv
-global-exclude nt.sqlite3
+recursive-include ntclient/resources/ *.csv
+
+global-exclude *.sqlite3
REQ_OPT := requirements-optional.txt
REQ_LINT := requirements-lint.txt
REQ_TEST := requirements-test.txt
-REQ_OLD := requirements-test-win_xp-ubu1604.txt
+REQ_TEST_OLD := requirements-test-old.txt
PIP_OPT_ARGS ?=
- $(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_OLD)"
+ echo "TEST REQs failed. Try with '--user' flag, or old version: $(PIP) install -r $(REQ_TEST_OLD)"
.PHONY: deps
deps: _venv _deps ## Install requirements
.PHONY: _lint
_lint:
# check formatting: Python
- pycodestyle --max-line-length=99 --statistics $(LINT_LOCS)
+ pycodestyle --statistics $(LINT_LOCS)
autopep8 --recursive --diff --max-line-length 88 --exit-code $(LINT_LOCS)
isort --diff --check $(LINT_LOCS)
black --check $(LINT_LOCS)
MIN_COV := 80
.PHONY: _test
_test:
- coverage run -m pytest -v -s -p no:cacheprovider -o log_cli=true $(TEST_HOME)
+ coverage run -m pytest $(TEST_HOME)
coverage report
.PHONY: test
rm -rf build/
rm -rf nutra.egg-info/
rm -rf .pytest_cache/ .mypy_cache/
- find ntclient/ tests/ -name __pycache__ -o -name .coverage -o -name .pytest_cache | xargs rm -rf
+ find ntclient/ tests/ \
+ -name \
+ __pycache__ \
+ -o -name \
+ .coverage \
+ -o -name .mypy_cache \
+ -o -name .pytest_cache \
+ | xargs rm -rf
# ---------------------------------------
nutratracker
**************
+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**
+
+- Python 3.4.0 or later (``lzma``, ``ssl`` & ``sqlite3`` modules)
+ [Win XP / Ubuntu 14.04].
+- Packages: see ``setup.py``, and ``requirements.txt`` files.
+- Internet connection, to download food database & package dependencies.
+
+See ``nt`` database: https://github.com/nutratech/nt-sqlite
+
+See ``usda`` database: https://github.com/nutratech/usda-sqlite
+
+
+Details
+#######################################################
+
.. list-table::
:widths: 15 25 20
:header-rows: 1
- * -
+ * - Category
-
-
* - Test / Linux
:alt: License GPL-3
-
-Command line tools for interacting with government food databases.
-*Requires:*
+Linux / macOS requirements (for development)
+#######################################################
-- Python 3.4.0 or later (lzma, ssl & sqlite3 modules) [Win XP / Ubuntu 14.04].
-- Packages: see ``setup.py``, and ``requirements.txt`` files.
-- Internet connection, to download food database & package dependencies.
+You will need to install ``make`` and ``gcc`` to build the ``Levenshtein``
+extension.
+
+::
+
+ sudo apt install \
+ make gcc \
+ python3-dev python3-venv \
+ direnv
-See nt database: https://github.com/nutratech/nt-sqlite
-See usda database: https://github.com/nutratech/usda-sqlite
+You can add the direnv hook, ``direnv hook bash >>~.bashrc``.
+Only run this once.
+
Plugin Development
-==================
+#######################################################
+
+You can develop plugins (or data modifications sets) that
+are imported and built on the base (or core) installation.
+
+
+Supporting Old Versions of Python
+#######################################################
+
+The old requirements can still be tested on modern interpreters.
+Simply install them with this (inside your ``venv`` environment).
+
+::
+
+ pip install -r requirements-old.txt
+
+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.
-We're looking to start developing plugins or data modifications sets that
-can be imported and built on the base installation, which remains pure.
Notes
-=====
+#######################################################
On Windows you should check the box during the Python installer
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
+(as an administrator) vs. installing just for themselves. It may change the
+location of installed scripts, and affect the ``$PATH`` variable being
+correctly populated for prior installs.
+
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``.
-Windows users may not be able to install ``python-Levenshtein``.
+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.
-Mac and Linux developers will do well to install ``direnv``.
+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).
+
Install PyPi release (from pip)
-===============================
+#######################################################
.. code-block:: bash
- pip install nutra
+ pip install -U nutra
(**Specify:** flag ``-U`` to upgrade, or ``--pre`` for development releases)
+
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).
+Initialize the DBs (``nt`` and ``usda``).
.. code-block:: bash
# Or install and run as package script
make install
- nutra init
+ n init
If installed (or inside ``cli``) folder, the program can also run
-with ``python -m ntclient``
+with ``python -m ntclient``.
+
+You may need to set the ``PY_SYS_INTERPRETER`` value for the ``Makefile``
+if trying to install other than with ``/usr/bin/python3``.
-Building the PyPi release
-#########################
+Building the PyPi release (sdist)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: bash
make build # python3 setup.py --quiet sdist
twine upload dist/nutra-X.X.X.tar.gz
+
Linting & Tests
-===============
+#######################################################
-Install the dependencies (``make deps``) and then:
+Install the dependencies (``make deps``). Now you can lint & test.
.. code-block:: bash
# source .venv/bin/activate # uncomment if NOT using direnv
make format lint test
+
ArgComplete (tab completion / autocomplete)
-===========================================
+#######################################################
+
+The ``argcomplete`` package will be installed alongside.
-After installing nutra, argcomplete package should also be installed.
Linux, macOS, and Linux Subsystem for Windows
-#############################################
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Simply run the following out of a ``bash`` shell. Check their page for more
specifics on using other shells, e.g. ``zsh``, ``fish``, or ``tsh``.
.. code-block:: bash
- activate-global-python-argcomplete
+ activate-global-python-argcomplete --user
-Then you can press tab to fill in or complete subcommands
+Then you can press tab to fill in or complete sub-commands
and to list argument flags.
+
Windows (Git Bash)
-##################
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This can work with git bash too. I followed the instructions on their README.
**NOTE:** This is a work in progress, we are adding more autocomplete
functions.
+
Currently Supported Data
-========================
+#######################################################
**USDA Stock database**
- Standard reference database (SR28) `[7794 foods]`
-
**Relative USDA Extensions**
- Flavonoid, Isoflavonoids, and Proanthocyanidins `[1352 foods]`
+
Usage
-=====
+#######################################################
Requires internet connection to download initial datasets.
Run ``nutra init`` for this step.
-Run the ``nutra`` script to output usage.
-
-Usage: ``nutra [options] <command>``
-
-
-Commands
-########
-
-::
-
- usage: nutra [-h] [-v] [-d] [--no-pager]
- {init,nt,search,sort,anl,day,recipe} ...
-
- optional arguments:
- -h, --help show this help message and exit
- -v, --version show program's version number and exit
- -d, --debug enable detailed error messages
- --no-pager disable paging (print full output)
-
- nutra subcommands:
- {init,nt,search,sort,anl,day,recipe}
- init setup profiles, USDA and NT database
- nt list out nutrients and their info
- search search foods by name, list overview info
- sort sort foods by nutrient ID
- anl analyze food(s)
- day analyze a DAY.csv file, RDAs optional
- recipe list and analyze recipes
+Run the ``n`` script to output usage.
+++ /dev/null
- GNU GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
-
- The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works. By contrast,
-the GNU General Public License is intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users. We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors. You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
- To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights. Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received. You must make sure that they, too, receive
-or can get the source code. And you must show them these terms so they
-know their rights.
-
- Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
- For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software. For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
- Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so. This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software. The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable. Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products. If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
- Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary. To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- TERMS AND CONDITIONS
-
- 0. Definitions.
-
- "This License" refers to version 3 of the GNU General Public License.
-
- "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
- "The Program" refers to any copyrightable work licensed under this
-License. Each licensee is addressed as "you". "Licensees" and
-"recipients" may be individuals or organizations.
-
- To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy. The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
- A "covered work" means either the unmodified Program or a work based
-on the Program.
-
- To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy. Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
- To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies. Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
- An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License. If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
- 1. Source Code.
-
- The "source code" for a work means the preferred form of the work
-for making modifications to it. "Object code" means any non-source
-form of a work.
-
- A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
- The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form. A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
- The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities. However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work. For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
- The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
- The Corresponding Source for a work in source code form is that
-same work.
-
- 2. Basic Permissions.
-
- All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met. This License explicitly affirms your unlimited
-permission to run the unmodified Program. The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work. This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
- You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force. You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright. Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
- Conveying under any other circumstances is permitted solely under
-the conditions stated below. Sublicensing is not allowed; section 10
-makes it unnecessary.
-
- 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
- No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
- When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
- 4. Conveying Verbatim Copies.
-
- You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
- You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
- 5. Conveying Modified Source Versions.
-
- You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
- a) The work must carry prominent notices stating that you modified
- it, and giving a relevant date.
-
- b) The work must carry prominent notices stating that it is
- released under this License and any conditions added under section
- 7. This requirement modifies the requirement in section 4 to
- "keep intact all notices".
-
- c) You must license the entire work, as a whole, under this
- License to anyone who comes into possession of a copy. This
- License will therefore apply, along with any applicable section 7
- additional terms, to the whole of the work, and all its parts,
- regardless of how they are packaged. This License gives no
- permission to license the work in any other way, but it does not
- invalidate such permission if you have separately received it.
-
- d) If the work has interactive user interfaces, each must display
- Appropriate Legal Notices; however, if the Program has interactive
- interfaces that do not display Appropriate Legal Notices, your
- work need not make them do so.
-
- A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit. Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
- 6. Conveying Non-Source Forms.
-
- You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
- a) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by the
- Corresponding Source fixed on a durable physical medium
- customarily used for software interchange.
-
- b) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by a
- written offer, valid for at least three years and valid for as
- long as you offer spare parts or customer support for that product
- model, to give anyone who possesses the object code either (1) a
- copy of the Corresponding Source for all the software in the
- product that is covered by this License, on a durable physical
- medium customarily used for software interchange, for a price no
- more than your reasonable cost of physically performing this
- conveying of source, or (2) access to copy the
- Corresponding Source from a network server at no charge.
-
- c) Convey individual copies of the object code with a copy of the
- written offer to provide the Corresponding Source. This
- alternative is allowed only occasionally and noncommercially, and
- only if you received the object code with such an offer, in accord
- with subsection 6b.
-
- d) Convey the object code by offering access from a designated
- place (gratis or for a charge), and offer equivalent access to the
- Corresponding Source in the same way through the same place at no
- further charge. You need not require recipients to copy the
- Corresponding Source along with the object code. If the place to
- copy the object code is a network server, the Corresponding Source
- may be on a different server (operated by you or a third party)
- that supports equivalent copying facilities, provided you maintain
- clear directions next to the object code saying where to find the
- Corresponding Source. Regardless of what server hosts the
- Corresponding Source, you remain obligated to ensure that it is
- available for as long as needed to satisfy these requirements.
-
- e) Convey the object code using peer-to-peer transmission, provided
- you inform other peers where the object code and Corresponding
- Source of the work are being offered to the general public at no
- charge under subsection 6d.
-
- A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
- A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling. In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage. For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product. A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
- "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source. The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
- If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information. But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
- The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed. Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
- Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
- 7. Additional Terms.
-
- "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law. If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
- When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it. (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.) You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
- Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
- a) Disclaiming warranty or limiting liability differently from the
- terms of sections 15 and 16 of this License; or
-
- b) Requiring preservation of specified reasonable legal notices or
- author attributions in that material or in the Appropriate Legal
- Notices displayed by works containing it; or
-
- c) Prohibiting misrepresentation of the origin of that material, or
- requiring that modified versions of such material be marked in
- reasonable ways as different from the original version; or
-
- d) Limiting the use for publicity purposes of names of licensors or
- authors of the material; or
-
- e) Declining to grant rights under trademark law for use of some
- trade names, trademarks, or service marks; or
-
- f) Requiring indemnification of licensors and authors of that
- material by anyone who conveys the material (or modified versions of
- it) with contractual assumptions of liability to the recipient, for
- any liability that these contractual assumptions directly impose on
- those licensors and authors.
-
- All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10. If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term. If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
- If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
- Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
- 8. Termination.
-
- You may not propagate or modify a covered work except as expressly
-provided under this License. Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
- However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
- Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
- Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License. If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
- 9. Acceptance Not Required for Having Copies.
-
- You are not required to accept this License in order to receive or
-run a copy of the Program. Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance. However,
-nothing other than this License grants you permission to propagate or
-modify any covered work. These actions infringe copyright if you do
-not accept this License. Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
- 10. Automatic Licensing of Downstream Recipients.
-
- Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License. You are not responsible
-for enforcing compliance by third parties with this License.
-
- An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations. If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
- You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License. For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
- 11. Patents.
-
- A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based. The
-work thus licensed is called the contributor's "contributor version".
-
- A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version. For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
- Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
- In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement). To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
- If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients. "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
- If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
- A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License. You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
- Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
- 12. No Surrender of Others' Freedom.
-
- If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all. For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
- 13. Use with the GNU Affero General Public License.
-
- Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
-combined work, and to convey the resulting work. The terms of this
-License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
-
- 14. Revised Versions of this License.
-
- The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation. If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
- If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
- Later license versions may give you additional or different
-permissions. However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
- 15. Disclaimer of Warranty.
-
- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 16. Limitation of Liability.
-
- IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
- 17. Interpretation of Sections 15 and 16.
-
- If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
- <one line to give the program's name and a brief idea of what it does.>
- Copyright (C) <year> <name of author>
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-Also add information on how to contact you by electronic and paper mail.
-
- If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
- <program> Copyright (C) <year> <name of author>
- This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
-
- You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
-<https://www.gnu.org/licenses/>.
-
- The GNU General Public License does not permit incorporating your program
-into proprietary programs. If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License. But first, please read
-<https://www.gnu.org/licenses/why-not-lgpl.html>.
# -*- coding: utf-8 -*-
"""
+Package info, database targets, paging/debug flags, PROJECT_ROOT,
+ and other configurations.
+
Created on Fri Jan 31 16:01:31 2020
@author: shane
-
-This file is part of nutra, a nutrient analysis program.
- https://github.com/nutratech/cli
- https://pypi.org/project/nutra/
-
-nutra is an extensible nutrient analysis and composition application.
-Copyright (C) 2018-2022 Shane Jaroch <chown_tee@proton.me>
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
-
import argparse
import os
import platform
import shutil
import sys
+from enum import Enum
+
+try:
+ from colorama import Fore, Style
+ from colorama import init as colorama_init
+
+ COLORAMA_CAPABLE = True
+ colorama_init()
+except ImportError:
+ COLORAMA_CAPABLE = False
from ntclient.ntsqlite.sql import NT_DB_NAME
# Package info
__title__ = "nutra"
-__version__ = "0.2.5"
+__version__ = "0.2.6.dev5"
__author__ = "Shane Jaroch"
__email__ = "chown_tee@proton.me"
__license__ = "GPL v3"
USDA_XZ_SHA256 = "25dba8428ced42d646bec704981d3a95dc7943240254e884aad37d59eee9616a"
# Global variables
-ROOT_DIR = os.path.abspath(os.path.dirname(__file__))
+PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__))
NUTRA_HOME = os.getenv("NUTRA_HOME", os.path.join(os.path.expanduser("~"), ".nutra"))
USDA_DB_NAME = "usda.sqlite"
# NOTE: NT_DB_NAME = "nt.sqlite3" is defined in ntclient.ntsqlite.sql
-DEBUG = False
-PAGING = True
-NTSQLITE_BUILDPATH = os.path.join(ROOT_DIR, "ntsqlite", "sql", NT_DB_NAME)
+NTSQLITE_BUILDPATH = os.path.join(PROJECT_ROOT, "ntsqlite", "sql", NT_DB_NAME)
NTSQLITE_DESTINATION = os.path.join(NUTRA_HOME, NT_DB_NAME)
# Check Python version
-PY_MIN_VER = (3, 4, 0)
+PY_MIN_VER = (3, 4, 3)
PY_SYS_VER = sys.version_info[0:3]
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:
- print("ERROR: %s requires Python %s or later to run" % (__title__, PY_MIN_STR))
- print("HINT: You're running Python %s" % PY_SYS_STR)
- sys.exit(1)
+ # TODO: make this testable with: `class CliConfig`?
+ raise RuntimeError(
+ "ERROR: %s requires Python %s or later to run" % (__title__, PY_MIN_STR),
+ "HINT: You're running Python %s" % PY_SYS_STR,
+ )
-# Buffer truncation
+# Console size, don't print more than it
BUFFER_WD = shutil.get_terminal_size()[0]
BUFFER_HT = shutil.get_terminal_size()[1]
)
-# NOTE: wip
-# class CLIConfig:
-# def __init__(self):
-# prop1 = True
-# usda_sqlite
-# nt_sqlitedriver
+################################################################################
+# 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
+
+ if COLORAMA_CAPABLE:
+ COLOR_WARN = Fore.YELLOW
+ COLOR_CRIT = Style.DIM + Fore.RED
+ COLOR_OVER = Style.DIM + Fore.MAGENTA
+
+ COLOR_DEFAULT = Fore.CYAN
+
+ COLOR_RESET_ALL = Style.RESET_ALL
+
+ # Used in macro bars
+ COLOR_YELLOW = Fore.YELLOW
+ COLOR_BLUE = Fore.BLUE
+ COLOR_RED = Fore.RED
+ else:
+ COLOR_WARN = str() # type: ignore
+ COLOR_CRIT = str() # type: ignore
+ COLOR_OVER = str() # type: ignore
+
+ COLOR_DEFAULT = str() # type: ignore
+
+ COLOR_RESET_ALL = str() # type: ignore
+
+ COLOR_YELLOW = str() # type: ignore
+ COLOR_BLUE = str() # type: ignore
+ COLOR_RED = str() # type: ignore
+
+
+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.color_reset_all = RdaColors.COLOR_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
+# 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) ?
-def set_flags(args: argparse.Namespace) -> None:
+################################################################################
+# 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:
"""
- Sets
- DEBUG flag
- PAGING flag
- from main (after arg parse). Accessible throughout package
+ 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.
"""
- global DEBUG, PAGING # pylint: disable=global-statement
- DEBUG = args.debug
- PAGING = not args.no_pager
+ 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
+################################################################################
+NUTR_ID_KCAL = 208
+
+NUTR_ID_PROTEIN = 203
+
+NUTR_ID_CARBS = 205
+NUTR_ID_SUGAR = 269
+NUTR_ID_FIBER = 291
+
+NUTR_ID_FAT_TOT = 204
+NUTR_ID_FAT_SAT = 606
+NUTR_ID_FAT_MONO = 645
+NUTR_ID_FAT_POLY = 646
+
+NUTR_IDS_FLAVONES = [
+ 710,
+ 711,
+ 712,
+ 713,
+ 714,
+ 715,
+ 716,
+ 734,
+ 735,
+ 736,
+ 737,
+ 738,
+ 731,
+ 740,
+ 741,
+ 742,
+ 743,
+ 745,
+ 749,
+ 750,
+ 751,
+ 752,
+ 753,
+ 755,
+ 756,
+ 758,
+ 759,
+ 762,
+ 770,
+ 773,
+ 785,
+ 786,
+ 788,
+ 789,
+ 791,
+ 792,
+ 793,
+ 794,
+]
- if DEBUG:
- print("Console size: %sh x %sw" % (BUFFER_HT, BUFFER_WD))
+NUTR_IDS_AMINOS = [
+ 501,
+ 502,
+ 503,
+ 504,
+ 505,
+ 506,
+ 507,
+ 508,
+ 509,
+ 510,
+ 511,
+ 512,
+ 513,
+ 514,
+ 515,
+ 516,
+ 517,
+ 518,
+ 521,
+]
# -*- coding: utf-8 -*-
"""
+Main module which is called by scripts.
+Top-level argument parsing logic; error handling.
+
Created on Fri Jan 31 16:02:19 2020
@author: shane
-
-This file is part of nutra, a nutrient analysis program.
- https://github.com/nutratech/cli
- https://pypi.org/project/nutra/
-
-nutra is an extensible nutrient analysis and composition application.
-Copyright (C) 2018-2022 Shane Jaroch <chown_tee@proton.me>
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import argparse
-import sys
import time
from urllib.error import HTTPError, URLError
import argcomplete
-from colorama import init as colorama_init
from ntclient import (
+ CLI_CONFIG,
__db_target_nt__,
__db_target_usda__,
+ __email__,
__title__,
+ __url__,
__version__,
- set_flags,
)
from ntclient.argparser import build_subcommands
-from ntclient.persistence import persistence_init
from ntclient.utils.exceptions import SqlException
-colorama_init()
-
-def build_argparser() -> argparse.ArgumentParser:
+def build_arg_parser() -> argparse.ArgumentParser:
"""Adds all subparsers and parsing logic"""
arg_parser = argparse.ArgumentParser(prog=__title__)
"""
start_time = time.time()
- arg_parser = build_argparser()
+ arg_parser = build_arg_parser()
argcomplete.autocomplete(arg_parser)
def parse_args() -> argparse.Namespace:
def func(parser: argparse.Namespace) -> tuple:
"""Executes a function for a given argument call to the parser"""
if hasattr(parser, "func"):
- # More than an empty command, so initialize the storage folder
- persistence_init()
+ # Print help for nested commands
+ if parser.func.__name__ == "print_help":
+ return 0, parser.func()
+ # Collect non-default args
args_dict = dict(vars(parser))
for expected_arg in ["func", "debug", "no_pager"]:
args_dict.pop(expected_arg)
# Build the parser, set flags
_parser = parse_args()
- set_flags(_parser)
- from ntclient import DEBUG # pylint: disable=import-outside-toplevel
+ CLI_CONFIG.set_flags(_parser)
- # TODO: bug reporting?
# Try to run the function
exit_code = 1
try:
exit_code, *_results = func(_parser)
- return exit_code
except SqlException as sql_exception:
print("Issue with an sqlite database: " + repr(sql_exception))
- if DEBUG:
+ if CLI_CONFIG.debug:
raise
except HTTPError as http_error:
err_msg = "{0}: {1}".format(http_error.code, repr(http_error))
print("Server response error, try again: " + err_msg)
- if DEBUG:
+ if CLI_CONFIG.debug:
raise
except URLError as url_error:
print("Connection error, check your internet: " + repr(url_error.reason))
- if DEBUG:
+ if CLI_CONFIG.debug:
raise
except Exception as exception: # pylint: disable=broad-except
- print("There was an unforeseen error: " + repr(exception))
- if DEBUG:
+ 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__)
+ if CLI_CONFIG.debug:
raise
finally:
- if DEBUG:
- exc_time = time.time() - start_time # type: ignore
+ if CLI_CONFIG.debug:
+ exc_time = time.time() - start_time
print("\nExecuted in: %s ms" % round(exc_time * 1000, 1))
print("Exit code: %s" % exit_code)
return exit_code
-
-
-if __name__ == "__main__":
- sys.exit(main())
-"""Main module for things related to argparse"""
+# -*- coding: utf-8 -*-
+"""
+Created on Tue May 25 13:08:55 2021 -0400
+
+@author: shane
+Main module for things related to argparse
+"""
import argparse
from ntclient.argparser import funcs as parser_funcs
from ntclient.argparser import types
+# noinspection PyUnresolvedReferences,PyProtectedMember
def build_subcommands(subparsers: argparse._SubParsersAction) -> None:
"""Attaches subcommands to main parser"""
+
build_init_subcommand(subparsers)
build_nt_subcommand(subparsers)
build_search_subcommand(subparsers)
build_analyze_subcommand(subparsers)
build_day_subcommand(subparsers)
build_recipe_subcommand(subparsers)
+ build_calc_subcommand(subparsers)
################################################################################
# Methods to build subparsers, and attach back to main arg_parser
################################################################################
+# noinspection PyUnresolvedReferences,PyProtectedMember
def build_init_subcommand(subparsers: argparse._SubParsersAction) -> None:
"""Self running init command"""
+
init_parser = subparsers.add_parser(
"init", help="setup profiles, USDA and NT database"
)
init_parser.set_defaults(func=parser_funcs.init)
+# noinspection PyUnresolvedReferences,PyProtectedMember
def build_nt_subcommand(subparsers: argparse._SubParsersAction) -> None:
"""Lists out nutrients details with computed totals and averages"""
+
nutrient_parser = subparsers.add_parser(
"nt", help="list out nutrients and their info"
)
nutrient_parser.set_defaults(func=parser_funcs.nutrients)
+# noinspection PyUnresolvedReferences,PyProtectedMember
def build_search_subcommand(subparsers: argparse._SubParsersAction) -> None:
"""Search: terms [terms ... ]"""
+
search_parser = subparsers.add_parser(
"search", help="search foods by name, list overview info"
)
search_parser.set_defaults(func=parser_funcs.search)
+# noinspection PyUnresolvedReferences,PyProtectedMember
def build_sort_subcommand(subparsers: argparse._SubParsersAction) -> None:
"""Sort foods ranked by nutr_id, per 100g or 200kcal"""
+
sort_parser = subparsers.add_parser("sort", help="sort foods by nutrient ID")
+
sort_parser.add_argument(
"-c",
dest="kcal",
sort_parser.set_defaults(func=parser_funcs.sort)
+# noinspection PyUnresolvedReferences,PyProtectedMember
def build_analyze_subcommand(subparsers: argparse._SubParsersAction) -> None:
"""Analyzes (foods only for now)"""
+
analyze_parser = subparsers.add_parser("anl", help="analyze food(s)")
+
analyze_parser.add_argument(
"-g",
dest="grams",
analyze_parser.set_defaults(func=parser_funcs.analyze)
+# noinspection PyUnresolvedReferences,PyProtectedMember
def build_day_subcommand(subparsers: argparse._SubParsersAction) -> None:
"""Analyzes a DAY.csv, uses new colored progress bar spec"""
+
day_parser = subparsers.add_parser(
"day", help="analyze a DAY.csv file, RDAs optional"
)
day_parser.set_defaults(func=parser_funcs.day)
+# noinspection PyUnresolvedReferences,PyProtectedMember
def build_recipe_subcommand(subparsers: argparse._SubParsersAction) -> None:
"""View, add, edit, delete recipes"""
+
recipe_parser = subparsers.add_parser("recipe", help="list and analyze recipes")
+ recipe_parser.set_defaults(func=parser_funcs.recipes)
+
recipe_subparsers = recipe_parser.add_subparsers(title="recipe subcommands")
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ # Init
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ recipe_init_parser = recipe_subparsers.add_parser(
+ "init", help="create recipe folder, copy stock data in"
+ )
+ recipe_init_parser.add_argument(
+ "--force",
+ "-f",
+ action="store_true",
+ help="forcibly remove and re-copy stock/core data",
+ )
+ recipe_init_parser.set_defaults(func=parser_funcs.recipes_init)
+
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ # Analyze
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ # TODO: tab-completion for not just cwd, but also inject for RECIPE_HOME
+ # TODO: support analysis for multiple file path(s) in one call
recipe_anl_parser = recipe_subparsers.add_parser(
"anl", help="view and analyze for recipe"
)
recipe_anl_parser.add_argument(
- "recipe_id", type=int, help="view (and analyze) recipe by ID"
+ "path", type=str, help="view (and analyze) recipe by file path"
)
recipe_anl_parser.set_defaults(func=parser_funcs.recipe)
- recipe_import_parser = recipe_subparsers.add_parser("import", help="add a recipe")
- recipe_import_parser.add_argument(
- "path",
- type=types.file_or_dir_path,
- help="path to recipe.csv (or folder with multiple CSV files)",
+
+# noinspection PyUnresolvedReferences,PyProtectedMember
+def build_calc_subcommand(subparsers: argparse._SubParsersAction) -> None:
+ """BMR, 1 rep-max, and other calculators"""
+
+ calc_parser = subparsers.add_parser(
+ "calc", help="find you 1 rep max, body fat, BMR"
)
- recipe_import_parser.set_defaults(func=parser_funcs.recipe_import)
- # TODO: edit.. support renaming, and overwriting/re-importing food_amts (from CSV)
+ calc_subparsers = calc_parser.add_subparsers(title="recipe subcommands")
+ calc_parser.set_defaults(func=calc_parser.print_help)
- recipe_delete_parser = recipe_subparsers.add_parser(
- "delete", help="delete a recipe(s) by ID or range"
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ # 1-rep max
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ calc_1rm_parser = calc_subparsers.add_parser(
+ "1rm", help="calculate 1 rep maxes, by different equations"
)
- recipe_delete_parser.add_argument("recipe_id", type=int, help="delete recipe by ID")
- recipe_delete_parser.set_defaults(func=parser_funcs.recipe_delete)
+ calc_1rm_parser.add_argument("weight", type=float, help="weight (lbs or kg)")
+ calc_1rm_parser.add_argument("reps", type=int, help="number of reps performed")
+ calc_1rm_parser.set_defaults(func=parser_funcs.calc_1rm)
- recipe_parser.set_defaults(func=parser_funcs.recipes)
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ # BMR
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ calc_bmr_parser = calc_subparsers.add_parser(
+ "bmr", help="calculate BMR and TDEE values"
+ )
+ calc_bmr_parser.add_argument(
+ "-F", dest="female_gender", action="store_true", help="Female gender"
+ )
+ # TODO: optional (union) with age / dob
+ calc_bmr_parser.add_argument("-a", type=str, dest="age", help="e.g. 95")
+ calc_bmr_parser.add_argument("-ht", type=float, dest="height", help="height (cm)")
+ calc_bmr_parser.add_argument("-bf", dest="body_fat", type=float, help="e.g. 0.16")
+ calc_bmr_parser.add_argument(
+ "-wt", dest="weight", type=float, required=True, help="weight (kg)"
+ )
+ calc_bmr_parser.add_argument(
+ "-x",
+ type=int,
+ dest="activity_factor",
+ required=True,
+ help="1 thru 5, sedentary thru intense",
+ )
+ calc_bmr_parser.set_defaults(func=parser_funcs.calc_bmr)
+
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ # Body fat
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ calc_bf_parser = calc_subparsers.add_parser(
+ "bf",
+ help="calculate body fat %% with Navy, 3-Site, 7-Site",
+ )
+ calc_bf_parser.add_argument(
+ "-F", dest="female_gender", action="store_true", help="Female gender"
+ )
+ calc_bf_parser.add_argument(
+ "-a", type=int, dest="age", help="e.g. 95 [3-Site & 7-Site]"
+ )
+ calc_bf_parser.add_argument(
+ "-ht", type=float, dest="height", help="height (cm) [Navy]"
+ )
+
+ calc_bf_parser.add_argument(
+ "-w", type=float, dest="waist", help="waist (cm) [Navy]"
+ )
+ calc_bf_parser.add_argument("-n", type=float, dest="neck", help="neck (cm) [Navy]")
+ calc_bf_parser.add_argument(
+ "-hip", type=float, dest="hip", help="hip (cm) [Navy / FEMALE only]"
+ )
+
+ calc_bf_parser.add_argument(
+ "chest",
+ type=int,
+ nargs="?",
+ 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]",
+ )
+ calc_bf_parser.add_argument(
+ "thigh",
+ type=int,
+ nargs="?",
+ 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]",
+ )
+ calc_bf_parser.add_argument(
+ "sub",
+ type=int,
+ nargs="?",
+ 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]",
+ )
+ calc_bf_parser.add_argument(
+ "mid",
+ type=int,
+ nargs="?",
+ help="mid (mm) ----- [7-Site skin caliper measurement]",
+ )
+ calc_bf_parser.set_defaults(func=parser_funcs.calc_body_fat)
+
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ # Lean body limits (young male)
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ calc_lbl_parser = calc_subparsers.add_parser(
+ "lbl", help="lean body limits (young, male)"
+ )
+ calc_lbl_parser.add_argument("height", type=float, help="height (cm)")
+ calc_lbl_parser.add_argument(
+ "desired_bf",
+ type=float,
+ nargs="?",
+ help="e.g. 0.12 -[eric_helms & casey_butt]",
+ )
+ calc_lbl_parser.add_argument(
+ "wrist", type=float, nargs="?", help="wrist (cm) [casey_butt]"
+ )
+ calc_lbl_parser.add_argument(
+ "ankle", type=float, nargs="?", help="ankle (cm) [casey_butt]"
+ )
+ calc_lbl_parser.set_defaults(func=parser_funcs.calc_lbm_limits)
-"""Current home to subparsers and service-level logic"""
+# -*- coding: utf-8 -*-
+"""
+Current home to subparsers and service-level logic.
+These functions all return a tuple of (exit_code: int, results: list|dict).
+
+Created on Sat Jul 18 16:30:28 2020 -0400
+
+@author: shane
+"""
import argparse
import os
+import traceback
+
+from tabulate import tabulate
-from ntclient import services
+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
def init(args: argparse.Namespace) -> tuple:
"""Wrapper init method for persistence stuff"""
- return services.init(yes=args.yes)
+ return ntclient.services.init(yes=args.yes)
-################################################################################
+##############################################################################
# Nutrients, search and sort
-################################################################################
-def nutrients(): # type: ignore
+##############################################################################
+def nutrients() -> tuple:
"""List nutrients"""
- return services.usda.list_nutrients()
+ return ntclient.services.usda.list_nutrients()
def search(args: argparse.Namespace) -> tuple:
"""Searches all dbs, foods, recipes, recent items and favorites."""
if args.top:
- return services.usda.search(
+ return ntclient.services.usda.search(
words=args.terms, fdgrp_id=args.fdgrp_id, limit=args.top
)
- return services.usda.search(words=args.terms, fdgrp_id=args.fdgrp_id)
+ return ntclient.services.usda.search(words=args.terms, fdgrp_id=args.fdgrp_id)
def sort(args: argparse.Namespace) -> tuple:
"""Sorts based on nutrient id"""
if args.top:
- return services.usda.sort_foods(args.nutr_id, by_kcal=args.kcal, limit=args.top)
- return services.usda.sort_foods(args.nutr_id, by_kcal=args.kcal)
+ return ntclient.services.usda.sort_foods(
+ args.nutr_id, by_kcal=args.kcal, limit=args.top
+ )
+ return ntclient.services.usda.sort_foods(args.nutr_id, by_kcal=args.kcal)
-################################################################################
+##############################################################################
# Analysis and Day scoring
-################################################################################
+##############################################################################
def analyze(args: argparse.Namespace) -> tuple:
"""Analyze a food"""
- food_ids = args.food_id
- grams = args.grams
+ # exc: ValueError,
+ food_ids = set(args.food_id)
+ grams = float(args.grams) if args.grams else 0.0
- return services.analyze.foods_analyze(food_ids, grams)
+ return ntclient.services.analyze.foods_analyze(food_ids, grams)
def day(args: argparse.Namespace) -> tuple:
"""Analyze a day's worth of meals"""
- day_csv_paths = args.food_log
- day_csv_paths = [os.path.expanduser(x) for x in day_csv_paths]
- rda_csv_path = os.path.expanduser(args.rda) if args.rda else None
+ day_csv_paths = [str(os.path.expanduser(x)) for x in args.food_log]
+ rda_csv_path = str(os.path.expanduser(args.rda)) if args.rda else str()
- return services.analyze.day_analyze(day_csv_paths, rda_csv_path=rda_csv_path)
+ return ntclient.services.analyze.day_analyze(
+ day_csv_paths, rda_csv_path=rda_csv_path
+ )
-################################################################################
+##############################################################################
# Recipes
-################################################################################
+##############################################################################
+def recipes_init(args: argparse.Namespace) -> tuple:
+ """Copy example/stock data into RECIPE_HOME"""
+ _force = args.force
+
+ return ntclient.services.recipe.utils.recipes_init(_force=_force)
+
+
def recipes() -> tuple:
- """Return recipes"""
- return services.recipe.recipes_overview()
+ """Show all, in tree or detail view"""
+ return ntclient.services.recipe.utils.recipes_overview()
def recipe(args: argparse.Namespace) -> tuple:
- """Return recipe view (analysis)"""
- return services.recipe.recipe_overview(args.recipe_id)
+ """
+ View and analyze a single (or a range)
+ @todo: argcomplete based on RECIPE_HOME folder
+ @todo: use as default command? Currently this is reached by `nutra recipe anl`
+ """
+ recipe_path = args.path
+
+ return ntclient.services.recipe.utils.recipe_overview(recipe_path=recipe_path)
+
+
+##############################################################################
+# Calculators
+##############################################################################
+def calc_1rm(args: argparse.Namespace) -> tuple:
+ """Perform 1-rep max calculations"""
+
+ weight = float(args.weight)
+ print("Weight: %s" % weight)
+ reps = int(args.reps)
+ print("Reps: %s" % reps)
+
+ _epley = calc.orm_epley(weight, reps)
+ _brzycki = calc.orm_brzycki(weight, reps)
+ _dos_remedios = calc.orm_dos_remedios(weight, reps)
+
+ result = {"epley": _epley, "brzycki": _brzycki, "dos_remedios": _dos_remedios}
+
+ # 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():
+ row = [_rep]
+ for _calc, _values in result.items():
+ try:
+ # Round down for now
+ row.append(int(_values[_rep]))
+ except KeyError:
+ row.append(None)
+ _all.append(row)
+
+ # Print results
+ print()
+ print("Results for: epley, brzycki, and dos_remedios")
+ if "errMsg" in _dos_remedios:
+ print("WARN: Dos Remedios failed: %s" % _dos_remedios["errMsg"])
+ print()
+ _table = tabulate(_all, headers=["n", "epl", "brz", "rmds"])
+ print(_table)
+
+ return 0, result
+
+
+def calc_bmr(args: argparse.Namespace) -> tuple:
+ """
+ Perform BMR & TDEE calculations
+
+ Example POST:
+ {
+ "weight": 71,
+ "height": 177,
+ "gender": "MALE",
+ "dob": 725864400,
+ "bodyFat": 0.14,
+ "activityFactor": 0.55
+ }
+ """
+
+ activity_factor = activity_factor_from_index(args.activity_factor)
+ print("Activity factor: %s" % activity_factor)
+ weight = float(args.weight) # kg
+ print("Weight: %s kg" % weight)
+
+ # TODO: require these all for any? Or do exception handling & optional args like bf?
+ try:
+ _katch_mcardle = calc.bmr_katch_mcardle(activity_factor, weight, args=args)
+ except (KeyError, TypeError, ValueError):
+ _katch_mcardle = {
+ "errMsg": "Katch McArdle failed, requires: "
+ "activity_factor, weight, body_fat."
+ }
+ try:
+ _cunningham = calc.bmr_cunningham(activity_factor, weight, args=args)
+ except (KeyError, TypeError, ValueError):
+ _cunningham = {
+ "errMsg": "Cunningham failed, requires: activity_factor, weight, body_fat."
+ }
+ try:
+ _mifflin_st_jeor = calc.bmr_mifflin_st_jeor(activity_factor, weight, args=args)
+ except (KeyError, TypeError, ValueError):
+ _mifflin_st_jeor = {
+ "errMsg": "Mifflin St Jeor failed, requires: "
+ "activity_factor, weight, gender, height, & age."
+ }
+ try:
+ _harris_benedict = calc.bmr_harris_benedict(activity_factor, weight, args=args)
+ except (KeyError, TypeError, ValueError):
+ _harris_benedict = {
+ "errMsg": "Harris Benedict failed, requires: "
+ "activity_factor, weight, gender, height, & age."
+ }
+
+ result = {
+ "katch_mcardle": _katch_mcardle,
+ "cunningham": _cunningham,
+ "mifflin_st_jeor": _mifflin_st_jeor,
+ "harris_benedict": _harris_benedict,
+ }
+
+ # Prepare the table for printing
+ headers = ("Equation", "BMR", "TDEE")
+ rows = []
+ for _equation, _calculation in result.items():
+ row = [_equation]
+ row.extend(_calculation.values())
+ rows.append(row)
+
+ _katch_mcardle_table = tabulate(rows, headers=headers, tablefmt="simple")
+ print(_katch_mcardle_table)
+
+ return 0, result
+
+
+def calc_body_fat(args: argparse.Namespace) -> tuple:
+ """
+ Perform body fat calculations for Navy, 3-Site, and 7-Site.
+
+ Example POST. @note FEMALE, also includes "hip" (cm)
+ {
+ "gender": "MALE",
+ "age": 29,
+ "height": 178,
+ "waist": 80,
+ "neck": 36.8,
+ // also: hip, if FEMALE
+ "chest": 5,
+ "abd": 6,
+ "thigh": 9,
+ "tricep": 6,
+ "sub": 8,
+ "sup": 7,
+ "mid": 4
+ }
+ """
+
+ gender = Gender.FEMALE if args.female_gender else Gender.MALE
+ print("Gender: %s" % gender)
+ try:
+ _navy = calc.bf_navy(gender, args)
+ except (TypeError, ValueError):
+ print()
+ if CLI_CONFIG.debug:
+ traceback.print_exc()
+ print(
+ "WARN: Navy failed, requires: gender, height, waist, neck, "
+ "and (if female) hip."
+ )
+ _navy = 0.0
+ try:
+ _3site = calc.bf_3site(gender, args)
+ except (TypeError, ValueError):
+ print()
+ if CLI_CONFIG.debug:
+ traceback.print_exc()
+ print(
+ "WARN: 3-Site failed, requires: gender, age, chest (mm), "
+ "abdominal (mm), and thigh (mm)."
+ )
+ _3site = 0.0
+ try:
+ _7site = calc.bf_7site(gender, args)
+ except (TypeError, ValueError):
+ print()
+ if CLI_CONFIG.debug:
+ traceback.print_exc()
+ print(
+ "WARN: 7-Site failed, requires: gender, age, chest (mm), "
+ "abdominal (mm), thigh (mm), tricep (mm), sub (mm), sup (mm), and mid (mm)."
+ )
+ _7site = 0.0
+
+ _table = tabulate([(_navy, _3site, _7site)], headers=["Navy", "3-Site", "7-Site"])
+ print()
+ print()
+ print(_table)
+
+ return 0, {"navy": _navy, "threeSite": _3site, "sevenSite": _7site}
+
+
+def calc_lbm_limits(args: argparse.Namespace) -> tuple:
+ """
+ Perform body fat calculations for Navy, 3-Site, and 7-Site.
+
+ Example POST.
+ {
+ "height": 179,
+ "desired-bf": 0.12,
+ "wrist": 17.2,
+ "ankle": 21.5
+ }
+ """
+
+ height = float(args.height)
+
+ # Perform calculations & handle errors
+ _berkhan = calc.lbl_berkhan(height)
+ _eric_helms = calc.lbl_eric_helms(height, args)
+ _casey_butt = calc.lbl_casey_butt(height, args)
+ result = {"berkhan": _berkhan, "helms": _eric_helms, "casey": _casey_butt}
-def recipe_import(args: argparse.Namespace) -> tuple:
- """Add a recipe"""
- # TODO: custom serving sizes, not always in grams?
- return services.recipe.recipe_import(args.path)
+ headers = [
+ "eq",
+ "condition",
+ "weight",
+ "lbm",
+ "chest",
+ "arm",
+ "forearm",
+ "neck",
+ "thigh",
+ "calf",
+ ]
+ rows = []
+ for _calc, _result in result.items():
+ _values = list(_result.values())
+ row = [_calc]
+ row.extend(_values)
+ while len(row) < len(headers):
+ row.append(str())
+ rows.append(row)
+ _table = tabulate(rows, headers=headers, tablefmt="pretty")
+ print(_table)
-def recipe_delete(args: argparse.Namespace) -> tuple:
- """Delete a recipe"""
- return services.recipe.recipe_delete(args.recipe_id)
+ return 0, result
-"""Custom types for argparse validation"""
+# -*- coding: utf-8 -*-
+"""
+Created on Mon May 31 09:19:00 2021 -0400
+
+@author: shane
+Custom types for argparse validation
+"""
import argparse
import os
-#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Sat Aug 29 19:43:55 2020
-#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Fri Jul 31 21:23:51 2020
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+Created on Fri Jul 22 15:34:07 2022
+
+@author: shane
+Classes, structures for storing, displaying, and editing data.
+"""
+import csv
+
+from ntclient import CLI_CONFIG
+
+
+class Recipe:
+ """Allows reading up CSV, filtering by UUID, and displaying detail view"""
+
+ def __init__(self, file_path: str) -> None:
+ """Initialize entity"""
+
+ self.file_path = file_path
+ self.csv_reader = csv.DictReader(str())
+
+ # Defined now, populated later
+ self.headers = tuple() # type: ignore
+ self.rows = tuple() # type: ignore
+
+ self.uuid = str()
+
+ self.food_data = {} # type: ignore
+
+ def process_data(self) -> None:
+ """
+ Parses out the raw CSV input read in during self.__init__()
+ TODO: test this with an empty CSV file
+ @todo: CliConfig class, to avoid these non top-level import shenanigans
+ """
+
+ # Read into memory
+ print("Processing recipe file: %s" % self.file_path)
+ with open(self.file_path, "r", encoding="utf-8") as _file:
+ self.csv_reader = csv.DictReader(_file)
+ self.rows = tuple(self.csv_reader)
+
+ # Validate data
+ uuids = {x["recipe_id"] for x in self.rows}
+ if len(uuids) != 1:
+ print("Found %s keys: %s" % (len(uuids), uuids))
+ raise KeyError("FATAL: must have exactly 1 uuid per recipe CSV file!")
+ self.uuid = list(uuids)[0]
+
+ # exc: ValueError (could not cast int / float)
+ self.food_data = {int(x["food_id"]): float(x["grams"]) for x in self.rows}
+
+ if CLI_CONFIG.debug:
+ print("Finished with recipe.")
+
+ def print_analysis(self) -> None:
+ """Run analysis on a single recipe"""
-Subproject commit f76955d55293d166f833befbab2c52447d0cd07e
+Subproject commit 0eb89de7db73aa68d71da3b4113296ba595500bd
# -*- coding: utf-8 -*-
"""
+Home to persistence and storage utilities.
+Used to have a prefs.json, but was deleted and is planned to be maintained
+ in sqlite.
+
Created on Sat Mar 23 13:09:07 2019
@author: shane
"""
-
-import json
-import os
-
-from ntclient import NUTRA_HOME
-
-# TODO: init, handle when it doesn't exist yet
-# TODO: prompt to create profile if copying default `prefs.json` with PROFILE_ID: -1
-# (non-existent)
-PREFS_FILE = os.path.join(NUTRA_HOME, "prefs.json")
-PREFS = {}
-PROFILE_ID = None
-
-
-def persistence_init() -> None:
- """Loads the preferences file and relevant bits"""
- global PREFS, PROFILE_ID # pylint: disable=global-statement
- from ntclient import DEBUG # pylint: disable=import-outside-toplevel
-
- if os.path.isfile(PREFS_FILE):
- with open(PREFS_FILE, encoding="utf-8") as file_path:
- PREFS = json.load(file_path)
- else:
- if DEBUG:
- print("WARN: ~/.nutra/prefs.json doesn't exist, using defaults")
- PREFS = {}
-
- PROFILE_ID = PREFS.get("current_user")
- if DEBUG and not PROFILE_ID:
- print(
- "WARN: ~/.nutra/prefs.json doesn't contain valid PROFILE_ID,"
- "proceeding in bare mode"
- )
import sqlite3
from collections.abc import Sequence
-
# ------------------------------------------------
# Entry fetching methods
# ------------------------------------------------
+from ntclient import CLI_CONFIG
+
+
def sql_entries(sql_result: sqlite3.Cursor) -> list:
"""Formats and returns a `sql_result()` for console digestion and output"""
# TODO: return object: metadata, command, status, errors, etc?
@return: A sqlite3.Cursor object with populated return values.
"""
- from ntclient import DEBUG # pylint: disable=import-outside-toplevel
-
cur = con.cursor()
- if DEBUG:
+ if CLI_CONFIG.debug:
print("%s.sqlite3: %s" % (db_name, query))
if values:
# TODO: better debug logging, more "control-findable",
"""nt.sqlite3 functions module"""
-from ntclient.persistence.sql.nt import sql, sql_headers
+from ntclient.persistence.sql.nt import sql
def sql_nt_next_index(table: str) -> int:
# noinspection SqlResolve
query = "SELECT MAX(id) as max_id FROM %s;" % table # nosec: B608
return int(sql(query)[0]["max_id"])
-
-
-################################################################################
-# Recipe functions
-################################################################################
-def sql_recipe(recipe_id: int) -> list:
- """Selects columns for recipe_id"""
- query = "SELECT * FROM recipe WHERE id=?;"
- return sql(query, values=(recipe_id,))
-
-
-def sql_recipes() -> tuple:
- """Show recipes with selected details"""
- query = """
-SELECT
- id,
- tagname,
- name,
- COUNT(recipe_id) AS n_foods,
- SUM(grams) AS grams,
- recipe.created as created
-FROM
- recipe
- LEFT JOIN recipe_dat ON recipe_id = id
-GROUP BY
- id;
-"""
- return sql_headers(query)
-
-
-def sql_analyze_recipe(recipe_id: int) -> list:
- """Output (nutrient) analysis columns for a given recipe_id"""
- query = """
-SELECT
- id,
- name,
- food_id,
- grams
-FROM
- recipe
- INNER JOIN recipe_dat ON recipe_id = id
- AND id = ?;
-"""
- return sql(query, values=(recipe_id,))
-
-
-def sql_recipe_add() -> list:
- """TODO: method for adding recipe"""
- query = """
-"""
- return sql(query)
def download_extract_usda() -> None:
"""Download USDA tarball from BitBucket and extract to storage folder"""
+ # TODO: move this into separate module, ignore coverage. Avoid SLOW tests
if yes or input_agree().lower() == "y":
# TODO: save with version in filename?
# Don't re-download tarball, just extract?
# Extract the archive
with tarfile.open(save_path, mode="r:xz") as usda_sqlite_file:
- print("\ntar xvf %s.tar.xz" % USDA_DB_NAME)
+ print("\n" + "tar xvf %s.tar.xz" % USDA_DB_NAME)
usda_sqlite_file.extractall(NUTRA_HOME)
print("==> done downloading %s" % USDA_DB_NAME)
- # TODO: handle resource moved on Bitbucket
- # or version mismatch due to manual overwrite?
+ # TODO: handle resource moved on Bitbucket,
+ # or version mismatch due to developer mistake / overwrite?
+ # And seed mirrors; don't hard code one host here!
url = (
"https://bitbucket.org/dasheenster/nutra-utils/downloads/{0}-{1}.tar.xz".format(
USDA_DB_NAME, __db_target_usda__
"""usda.sqlite functions module"""
+from ntclient import NUTR_ID_KCAL
from ntclient.persistence.sql.usda import sql, sql_headers
-from ntclient.utils import NUTR_ID_KCAL
################################################################################
--- /dev/null
+recipe_id,food_id,grams,name
+e403ede0-76a1-4992-81f9-2f72e9e4bc0e,20045,180,white rice
+e403ede0-76a1-4992-81f9-2f72e9e4bc0e,16042,25,pinto beans
+e403ede0-76a1-4992-81f9-2f72e9e4bc0e,11282,45,onions
+e403ede0-76a1-4992-81f9-2f72e9e4bc0e,11260,45,mushrooms
+e403ede0-76a1-4992-81f9-2f72e9e4bc0e,11821,35,bell peppers
+e403ede0-76a1-4992-81f9-2f72e9e4bc0e,11233,25,kale
+e403ede0-76a1-4992-81f9-2f72e9e4bc0e,23293,85,"beef (grass-fed, 85/15)"
+e403ede0-76a1-4992-81f9-2f72e9e4bc0e,11529,40,tomatoes
+e403ede0-76a1-4992-81f9-2f72e9e4bc0e,9037,40,avocados
+e403ede0-76a1-4992-81f9-2f72e9e4bc0e,1009,20,cheese (cheddar)
--- /dev/null
+recipe_id,food_id,grams,name (optional)
+77d54a8c-0b69-4f10-b9e7-b5f15807a8f0,18351,55,roll (mixed-grain)
+77d54a8c-0b69-4f10-b9e7-b5f15807a8f1,23293,85,"beef (grass-fed, 85/15)"
+77d54a8c-0b69-4f10-b9e7-b5f15807a8f2,1009,20,cheddar cheese
+77d54a8c-0b69-4f10-b9e7-b5f15807a8f3,11251,25,lettuce (romaine)
+77d54a8c-0b69-4f10-b9e7-b5f15807a8f4,11529,40,tomatoes
+77d54a8c-0b69-4f10-b9e7-b5f15807a8f5,11282,20,onions
+77d54a8c-0b69-4f10-b9e7-b5f15807a8f6,9037,40,avocados
--- /dev/null
+recipe_id,food_id,grams,name (optional)
+bbac2626-83d4-41ca-a1cb-dda5c4bf4707,11355,300,potatoes (red)
+bbac2626-83d4-41ca-a1cb-dda5c4bf4707,4053,30,olive oil
+bbac2626-83d4-41ca-a1cb-dda5c4bf4707,11297,30,parsley (fresh)
--- /dev/null
+recipe_id,food_id,grams,name (optional)
+2b2d0375-fd5a-4544-9da7-189f4da18ed8,20011,60,flour (buckwheat)
+2b2d0375-fd5a-4544-9da7-189f4da18ed9,20140,30,flour (spelt)
+2b2d0375-fd5a-4544-9da7-189f4da18ed10,20080,30,flour (whole wheat)
+2b2d0375-fd5a-4544-9da7-189f4da18ed11,1123,56,egg
+2b2d0375-fd5a-4544-9da7-189f4da18ed12,1079,244,milk (2%)
+2b2d0375-fd5a-4544-9da7-189f4da18ed13,19911,25,syrup (maple)
+2b2d0375-fd5a-4544-9da7-189f4da18ed14,16122,20,protein (soy or whey)
+2b2d0375-fd5a-4544-9da7-189f4da18ed15,2047,1.5,salt
+2b2d0375-fd5a-4544-9da7-189f4da18ed16,18372,1.5,baking soda
+2b2d0375-fd5a-4544-9da7-189f4da18ed8,18370,0.75,baking powder
--- /dev/null
+recipe_id,food_id,grams,name (optional)
+30e04b8b-3c2b-4b28-a41e-6445b2dbb9f8,12061,50,almonds
+30e04b8b-3c2b-4b28-a41e-6445b2dbb9f9,12220,30,flaxseed
+30e04b8b-3c2b-4b28-a41e-6445b2dbb9f10,12012,20,hemp (seed/protein)
+30e04b8b-3c2b-4b28-a41e-6445b2dbb9f11,16122,28,protein (soy or whey)
+30e04b8b-3c2b-4b28-a41e-6445b2dbb9f12,9050,50,blueberries
+30e04b8b-3c2b-4b28-a41e-6445b2dbb9f13,9040,80,bananas
+30e04b8b-3c2b-4b28-a41e-6445b2dbb9f14,9176,40,mangos
+30e04b8b-3c2b-4b28-a41e-6445b2dbb9f15,1289,140,kefir
--- /dev/null
+recipe_id,food_id,grams,name
+1,20045,180,white rice
+1,16042,25,pinto beans
+1,11282,45,onions
+1,11260,45,mushrooms
+1,11821,35,bell peppers
+1,11233,25,kale
+1,23293,85,"beef (grass-fed, 85/15)"
+1,11529,40,tomatoes
+1,9037,40,avocados
+1,1009,20,cheese (cheddar)
+,,,
+2,18351,55,roll (mixed-grain)
+2,23293,85,"beef (grass-fed, 85/15)"
+2,1009,20,cheddar cheese
+2,11251,25,lettuce (romaine)
+2,11529,40,tomatoes
+2,11282,20,onions
+2,9037,40,avocados
+,,,
+3,11355,300,potatoes (red)
+3,4053,30,olive oil
+3,11297,30,parsley (fresh)
+,,,
+4,20011,60,flour (buckwheat)
+4,20140,30,flour (spelt)
+4,20080,30,flour (whole wheat)
+4,1123,56,egg
+4,1079,244,milk (2%)
+4,19911,25,syrup (maple)
+4,16122,20,protein (soy or whey)
+4,2047,1.5,salt
+4,18372,1.5,baking soda
+4,18370,0.75,baking powder
+,,,
+5,12061,50,almonds
+5,12220,30,flaxseed
+5,12012,20,hemp (seed/protein)
+5,16122,28,protein (soy or whey)
+5,9050,50,blueberries
+5,9040,80,bananas
+5,9176,40,mangos
+5,1289,140,kefir
--- /dev/null
+id,name
+1,Burrito bowl (Everyday)
+2,"Burger (Grass fed, Beef)"
+3,Baked potato wedges
+4,Buckwheat pancake (w/ syrup)
+5,Blueberry-hemp Smoothie
--- /dev/null
+recipe_id,serving,grams
+1,cup,160
from ntclient.ntsqlite.sql import build_ntsqlite
from ntclient.persistence.sql.nt import nt_init
from ntclient.persistence.sql.usda import usda_init
-from ntclient.services import analyze, recipe, usda
+
+# TODO: rethink the above imports, if this belongs in __init__ or not
def init(yes: bool = False) -> tuple:
nt_init()
print("\nAll checks have passed!")
+ print(
+ """
+Nutrient tracker is free software. It comes with NO warranty or guarantee.
+You may use it as you please.
+You may make changes, as long as you disclose and publish them.
+ """
+ )
return 0, True
import csv
from collections import OrderedDict
-from colorama import Fore, Style
from tabulate import tabulate
-from ntclient import BUFFER_WD
-from ntclient.persistence.sql.usda.funcs import (
- sql_analyze_foods,
- sql_food_details,
- sql_nutrients_overview,
- sql_servings,
-)
-from ntclient.utils import (
- COLOR_CRIT,
- COLOR_DEFAULT,
- COLOR_OVER,
- COLOR_WARN,
+from ntclient import (
+ BUFFER_WD,
+ CLI_CONFIG,
NUTR_ID_CARBS,
NUTR_ID_FAT_TOT,
NUTR_ID_FIBER,
NUTR_ID_KCAL,
NUTR_ID_PROTEIN,
- THRESH_CRIT,
- THRESH_OVER,
- THRESH_WARN,
+)
+from ntclient.persistence.sql.usda.funcs import (
+ sql_analyze_foods,
+ sql_food_details,
+ sql_nutrients_overview,
+ sql_servings,
)
################################################################################
# Foods
################################################################################
-def foods_analyze(food_ids: set, grams: int = 0) -> tuple:
+def foods_analyze(food_ids: set, grams: float = 0) -> tuple:
"""
Analyze a list of food_ids against stock RDA values
TODO: from ntclient.utils.nutprogbar import nutprogbar
################################################################################
# Day
################################################################################
-def day_analyze(day_csv_paths: str, rda_csv_path: str = str()) -> tuple:
+def day_analyze(day_csv_paths: list, rda_csv_path: str = str()) -> tuple:
"""Analyze a day optionally with custom RDAs,
e.g. nutra day ~/.nutra/rocky.csv -r ~/.nutra/dog-rdas-18lbs.csv
TODO: Should be a subset of foods_analyze
"""
- from ntclient import DEBUG # pylint: disable=import-outside-toplevel
if rda_csv_path:
with open(rda_csv_path, encoding="utf-8") as file_path:
for _nutrient in nutrients_lists:
if _nutrient[0] == nutrient_id:
_nutrient[1] = _rda
- if DEBUG:
+ if CLI_CONFIG.debug:
substr = "{0} {1}".format(_rda, _nutrient[2]).ljust(12)
print("INJECT RDA: {0} --> {1}".format(substr, _nutrient[4]))
nutrients = {x[0]: x for x in nutrients_lists}
"""Formats day analysis for printing to console"""
def print_header(header: str) -> None:
- print(Fore.CYAN, end="")
+ print(CLI_CONFIG.color_default, end="")
print("~~~~~~~~~~~~~~~~~~~~~~~~~~~")
print("--> %s" % header)
print("~~~~~~~~~~~~~~~~~~~~~~~~~~~")
- print(Style.RESET_ALL)
+ print(CLI_CONFIG.color_reset_all)
def print_macro_bar(
_fat: float, _net_carb: float, _pro: float, _kcals_max: float, _buffer: int = 0
p_buf = " " * (n_pro // 2) + "Pro" + " " * (n_pro - n_pro // 2 - 3)
print(
" "
- + Fore.YELLOW
+ + CLI_CONFIG.color_yellow
+ f_buf
- + Fore.BLUE
+ + CLI_CONFIG.color_blue
+ c_buf
- + Fore.RED
+ + CLI_CONFIG.color_red
+ p_buf
- + Style.RESET_ALL
+ + CLI_CONFIG.color_reset_all
)
# Bars
print(" <", end="")
- print(Fore.YELLOW + "=" * n_fat, end="")
- print(Fore.BLUE + "=" * n_car, end="")
- print(Fore.RED + "=" * n_pro, end="")
- print(Style.RESET_ALL + ">")
+ print(CLI_CONFIG.color_yellow + "=" * n_fat, end="")
+ print(CLI_CONFIG.color_blue + "=" * n_car, end="")
+ print(CLI_CONFIG.color_red + "=" * n_pro, end="")
+ print(CLI_CONFIG.color_reset_all + ">")
# Calorie footers
k_fat = str(round(fat * 9))
p_buf = " " * (n_pro // 2) + k_pro + " " * (n_pro - n_pro // 2 - len(k_pro))
print(
" "
- + Fore.YELLOW
+ + CLI_CONFIG.color_yellow
+ f_buf
- + Fore.BLUE
+ + CLI_CONFIG.color_blue
+ c_buf
- + Fore.RED
+ + CLI_CONFIG.color_red
+ p_buf
- + Style.RESET_ALL
+ + CLI_CONFIG.color_reset_all
)
def print_nute_bar(_n_id: int, amount: float, _nutrients: dict) -> tuple:
attain = amount / rda
perc = round(100 * attain, 1)
- if attain >= THRESH_OVER:
- color = COLOR_OVER
- elif attain <= THRESH_CRIT:
- color = COLOR_CRIT
- elif attain <= THRESH_WARN:
- color = COLOR_WARN
+ if attain >= CLI_CONFIG.thresh_over:
+ color = CLI_CONFIG.color_over
+ elif attain <= CLI_CONFIG.thresh_crit:
+ color = CLI_CONFIG.color_crit
+ elif attain <= CLI_CONFIG.thresh_warn:
+ color = CLI_CONFIG.color_warn
else:
- color = COLOR_DEFAULT
+ color = CLI_CONFIG.color_default
# Print
detail_amount = "{0}/{1} {2}".format(round(amount, 1), rda, unit).ljust(18)
print(" {0}<".format(color), end="")
print("=" * left_pos + " " * (left_index - left_pos) + ">", end="")
print(" {0}%\t[{1}]".format(perc, detail_amount), end="")
- print(Style.RESET_ALL)
+ print(CLI_CONFIG.color_reset_all)
return True, perc
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+Calculate service for one rep max, BMR, body fat.
+
+Created on Tue Aug 11 20:53:14 2020
+
+@author: shane
+"""
+import argparse
+import math
+from datetime import datetime
+
+from ntclient import Gender
+
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+# 1 rep max
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+common_n_reps = (1, 2, 3, 5, 6, 8, 10, 12, 15, 20)
+
+
+def orm_epley(weight: float, reps: float) -> dict:
+ """
+ Returns a dict {n_reps: max_weight, ...}
+ for n_reps: (1, 2, 3, 5, 6, 8, 10, 12, 15, 20)
+
+ 1 RM = weight * (1 + (reps - 1) / 30)
+
+ Source: https://workoutable.com/one-rep-max-calculator/
+ """
+
+ def one_rm() -> float:
+ _un_rounded_result = weight * (1 + (reps - 1) / 30)
+ return round(_un_rounded_result, 1)
+
+ def weight_max_reps(target_reps: float) -> float:
+ _un_rounded_result = one_rm() / (1 + (target_reps - 1) / 30)
+ return round(_un_rounded_result, 1)
+
+ maxes = {n_reps: weight_max_reps(n_reps) for n_reps in common_n_reps}
+ return maxes
+
+
+def orm_brzycki(weight: float, reps: float) -> dict:
+ """
+ Returns a dict {n_reps: max_weight, ...}
+ for n_reps: (1, 2, 3, 5, 6, 8, 10, 12, 15)
+
+ 1 RM = weight * 36 / (37 - reps)
+
+ Source: https://workoutable.com/one-rep-max-calculator/
+ """
+
+ def _one_rm() -> float:
+ _un_rounded_result = weight * 36 / (37 - reps)
+ return round(_un_rounded_result, 1)
+
+ one_rm = _one_rm()
+
+ def weight_max_reps(target_reps: float) -> float:
+ _un_rounded_result = one_rm / (1 + (target_reps - 1) / 30)
+ return round(_un_rounded_result, 1)
+
+ maxes = {n_reps: weight_max_reps(n_reps) for n_reps in common_n_reps}
+ return maxes
+
+
+def orm_dos_remedios(weight: float, reps: int) -> dict:
+ """
+ Returns dict {n_reps: max_weight, ...}
+ for n_reps: (1, 2, 3, 5, 6, 8, 10, 12, 15)
+
+ Or an {"errMsg": "INVALID_RANGE", ...}
+
+ Source:
+ https://www.peterrobertscoaching.com/blog/the-best-way-to-calculate-1-rep-max
+ """
+
+ _common_n_reps = {
+ 1: 1,
+ 2: 0.92,
+ 3: 0.9,
+ 5: 0.87,
+ 6: 0.82,
+ 8: 0.75,
+ 10: 0.7,
+ 12: 0.65,
+ 15: 0.6,
+ }
+
+ def _one_rm() -> float:
+ _multiplier = _common_n_reps[reps]
+ _un_rounded_result = weight / _multiplier
+ return round(_un_rounded_result, 1)
+
+ try:
+ one_rm = _one_rm()
+ except KeyError:
+ # _logger.debug(traceback.format_exc())
+ valid_reps = list(_common_n_reps.keys())
+ return {
+ "errMsg": "INVALID_RANGE — "
+ + "requires: reps in %s, got %s" % (valid_reps, reps),
+ }
+
+ def max_weight(target_reps: int) -> float:
+ _multiplier = _common_n_reps[target_reps]
+ _un_rounded_result = one_rm * _multiplier
+ return round(_un_rounded_result, 1)
+
+ return {n_reps: max_weight(n_reps) for n_reps in _common_n_reps}
+
+
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+# BMR
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+# TODO: write true service level calls, which accepts: lbm | (weight & body_fat)
+def bmr_katch_mcardle(
+ activity_factor: float, weight: float, args: argparse.Namespace
+) -> dict:
+ """
+ BMR = 370 + (21.6 x Lean Body Mass(kg) )
+
+ Source: https://www.calculatorpro.com/calculator/katch-mcardle-bmr-calculator/
+ Source: https://tdeecalculator.net/about.php
+
+ @param activity_factor: {0.200, 0.375, 0.550, 0.725, 0.900}
+ @param weight: kg
+ @param args: Namespace containing: body_fat (to calculate lean mass in kg)
+ """
+
+ body_fat = float(args.body_fat)
+ print("Body fat: %s %%" % (body_fat * 100))
+
+ lbm = weight * (1 - body_fat)
+ bmr = 370 + (21.6 * lbm)
+ tdee = bmr * (1 + activity_factor)
+
+ return {
+ "bmr": round(bmr),
+ "tdee": round(tdee),
+ }
+
+
+def bmr_cunningham(
+ activity_factor: float, weight: float, args: argparse.Namespace
+) -> dict:
+ """
+ Source: https://www.slideshare.net/lsandon/weight-management-in-athletes-lecture
+
+ @param activity_factor: {0.200, 0.375, 0.550, 0.725, 0.900}
+ @param weight: kg
+ @param args: Namespace containing: body_fat (to calculate lean mass in kg)
+ """
+
+ body_fat = float(args.body_fat)
+
+ lbm = weight * (1 - body_fat)
+ bmr = 500 + 22 * lbm
+ tdee = bmr * (1 + activity_factor)
+
+ return {
+ "bmr": round(bmr),
+ "tdee": round(tdee),
+ }
+
+
+def bmr_mifflin_st_jeor(
+ activity_factor: float, weight: float, args: argparse.Namespace
+) -> dict:
+ """
+ Source: https://www.myfeetinmotion.com/mifflin-st-jeor-equation/
+
+ @param activity_factor: {0.200, 0.375, 0.550, 0.725, 0.900}
+ @param weight: kg
+ @param args: Namespace containing:
+ gender: {'MALE', 'FEMALE'}
+ height: cm
+ age: float (years)
+ """
+
+ def gender_specific_bmr(_gender: Gender, _bmr: float) -> float:
+ _second_term = {
+ Gender.MALE: 5,
+ Gender.FEMALE: -161,
+ }
+ return _bmr + _second_term[_gender]
+
+ gender = Gender.FEMALE if args.female_gender else Gender.MALE
+ print()
+ print("Gender: %s" % gender)
+
+ height = float(args.height)
+ print("Height: %s cm" % height)
+ age = float(args.age)
+ print("Age: %s years" % age)
+ print()
+
+ bmr = 10 * weight + 6.25 + 6.25 * height - 5 * age
+
+ bmr = gender_specific_bmr(gender, bmr)
+ tdee = bmr * (1 + activity_factor)
+
+ return {
+ "bmr": round(bmr),
+ "tdee": round(tdee),
+ }
+
+
+def bmr_harris_benedict(
+ activity_factor: float, weight: float, args: argparse.Namespace
+) -> dict:
+ """
+ Harris-Benedict = (13.397m + 4.799h - 5.677a) + 88.362 (MEN)
+
+ Harris-Benedict = (9.247m + 3.098h - 4.330a) + 447.593 (WOMEN)
+
+ m: mass (kg), h: height (cm), a: age (years)
+
+ Source: https://tdeecalculator.net/about.php
+
+ @param activity_factor: {0.200, 0.375, 0.550, 0.725, 0.900}
+ @param weight: kg
+ @param args: Namespace containing:
+ gender: {'MALE', 'FEMALE'}
+ height: cm
+ age: float (years)
+ """
+
+ gender = Gender.FEMALE if args.female_gender else Gender.MALE
+
+ height = float(args.height)
+ age = float(args.age)
+
+ def gender_specific_bmr(_gender: Gender) -> float:
+ _gender_specific_bmr = {
+ Gender.MALE: (13.397 * weight + 4.799 * height - 5.677 * age) + 88.362,
+ Gender.FEMALE: (9.247 * weight + 3.098 * height - 4.330 * age) + 447.593,
+ }
+ return _gender_specific_bmr[_gender]
+
+ bmr = gender_specific_bmr(gender)
+ tdee = bmr * (1 + activity_factor)
+
+ return {
+ "bmr": round(bmr),
+ "tdee": round(tdee),
+ }
+
+
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+# Body fat
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+def bf_navy(gender: Gender, args: argparse.Namespace) -> float:
+ """
+ @param gender: MALE or FEMALE
+ @param args: argparse namespace dict containing:
+ height, waist, neck, and (if female) hip.
+ All values are in cm.
+
+ @return: float (e.g. 0.17)
+
+ Source:
+ https://www.thecalculator.co/health/Navy-Method-Body-Fat-Measurement-Calculator-1112.html
+ """
+
+ # Navy values
+ height = float(args.height)
+ print()
+ print("Height: %s cm" % height)
+
+ waist = float(args.waist)
+ print("Waist: %s cm" % waist)
+ if gender == Gender.FEMALE:
+ hip = float(args.hip)
+ print("Hip: %s cm" % hip)
+ else:
+ hip = 0.0 # placeholder value, not used for men anyway
+ neck = float(args.neck)
+ print("Neck: %s cm" % neck)
+
+ # Compute values
+ _gender_specific_denominator = {
+ Gender.MALE: (
+ 1.0324 - 0.19077 * math.log10(waist - neck) + 0.15456 * math.log10(height)
+ ),
+ Gender.FEMALE: (
+ 1.29579
+ - 0.35004 * math.log10(waist + hip - neck)
+ + 0.22100 * math.log10(height)
+ ),
+ }
+
+ return round(495 / _gender_specific_denominator[gender] - 450, 2)
+
+
+def bf_3site(gender: Gender, args: argparse.Namespace) -> float:
+ """
+ @param gender: MALE or FEMALE
+ @param args: dict containing age, and skin manifolds (mm) for
+ chest, abdominal, and thigh.
+
+ @return: float (e.g. 0.17)
+
+ Source:
+ https://www.thecalculator.co/health/Body-Fat-Percentage-3-Site-Skinfold-Test-1113.html
+ """
+
+ # Shared parameters for skin manifold 3 & 7 site tests
+ age = float(args.age)
+ print()
+ print("Age: %s years" % age)
+
+ # 3-Site values
+ chest = int(args.chest)
+ print("Chest: %s mm" % chest)
+ abd = int(args.abd)
+ print("Abdominal: %s mm" % abd)
+ thigh = int(args.thigh)
+ print("Thigh: %s mm" % thigh)
+
+ # Compute values
+ st3 = chest + abd + thigh
+ _gender_specific_denominator = {
+ Gender.MALE: 1.10938
+ - 0.0008267 * st3
+ + 0.0000016 * st3 * st3
+ - 0.0002574 * age,
+ Gender.FEMALE: 1.089733
+ - 0.0009245 * st3
+ + 0.0000025 * st3 * st3
+ - 0.0000979 * age,
+ }
+
+ return round(495 / _gender_specific_denominator[gender] - 450, 2)
+
+
+def bf_7site(gender: Gender, args: argparse.Namespace) -> float:
+ """
+ @param gender: MALE or FEMALE
+ @param args: dict containing age, and skin manifolds (mm) for
+ chest, abdominal, thigh, triceps, sub, sup, and mid.
+
+ @return: float (e.g. 0.17)
+
+ Source:
+ https://www.thecalculator.co/health/Body-Fat-Percentage-7-Site-Skinfold-Calculator-1115.html
+ """
+
+ # Shared parameters for skin manifold 3 & 7 site tests
+ age = float(args.age)
+
+ # 3-Site values
+ chest = int(args.chest)
+ abd = int(args.abd)
+ thigh = int(args.thigh)
+
+ # 7-Site values
+ tricep = int(args.tricep)
+ print()
+ print("Tricep: %s mm" % tricep)
+ sub = int(args.sub)
+ print("Sub: %s mm" % sub)
+ sup = int(args.sup)
+ print("Sup: %s mm" % sup)
+ mid = int(args.mid)
+ print("Mid: %s mm" % mid)
+
+ # Compute values
+ st7 = chest + abd + thigh + tricep + sub + sup + mid
+
+ _gender_specific_denominator = {
+ Gender.MALE: 1.112
+ - 0.00043499 * st7
+ + 0.00000055 * st7 * st7
+ - 0.00028826 * age,
+ Gender.FEMALE: 1.097
+ - 0.00046971 * st7
+ + 0.00000056 * st7 * st7
+ - 0.00012828 * age,
+ }
+
+ return round(495 / _gender_specific_denominator[gender] - 450, 2)
+
+
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+# Lean body limits (young men)
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+def lbl_berkhan(height: float) -> dict:
+ """
+ Calculate Martin Berkhan's lean body limit for young men.
+
+ Source: https://rippedbody.com/maximum-muscular-potential/
+
+ @param height: cm
+ @return: {"condition": "...", weight_range: "abc ~ xyz"}
+ """
+
+ _min = round((height - 102) * 2.205, 1)
+ _max = round((height - 98) * 2.205, 1)
+ return {"condition": "Contest shape (5-6%)", "weight": "%s ~ %s lbs" % (_min, _max)}
+
+
+def lbl_eric_helms(height: float, args: argparse.Namespace) -> dict:
+ """
+ Calculate Eric Helm's lean body limit for young men.
+
+ Source:
+
+ @param height: cm
+ @param args: Namespace containing desired_bf, e.g. 0.12
+ @return: {"condition": "...", weight_range: "abc ~ xyz"}
+ """
+
+ try:
+ desired_bf = float(args.desired_bf) * 100
+ except (KeyError, TypeError):
+ return {"errMsg": "Eric Helms failed, requires: height, desired_bf."}
+
+ _min = round(4851.00 * height * 0.01 * height * 0.01 / (100.0 - desired_bf), 1)
+ _max = round(5402.25 * height * 0.01 * height * 0.01 / (100.0 - desired_bf), 1)
+ return {
+ "condition": "%s%% body fat" % desired_bf,
+ "weight": "%s ~ %s lbs" % (_min, _max),
+ }
+
+
+def lbl_casey_butt(height: float, args: argparse.Namespace) -> dict:
+ """
+ Calculate Casey Butt's lean body limit for young men. Includes muscle measurements.
+ Some may find these controversial.
+
+ Source: https://fastfoodmacros.com/maximum-muscular-potential-calculator.asp
+
+ @param height: cm
+ @param args: Namespace containing desired_bf, and wrist & ankle circumference.
+ @return: dict with lbm, weight, and maximum measurements for muscle groups.
+ """
+
+ try:
+ height /= 2.54
+ desired_bf = float(args.desired_bf)
+
+ wrist = float(args.wrist) / 2.54 # convert cm --> inches
+ ankle = float(args.ankle) / 2.54 # convert cm --> inches
+ except (KeyError, TypeError):
+ return {
+ "errMsg": "Casey Butt failed, requires: height, desired_bf, wrist, & ankle."
+ }
+
+ lbm = round(
+ height ** (3 / 2)
+ * (math.sqrt(wrist) / 22.6670 + math.sqrt(ankle) / 17.0104)
+ * (1 + desired_bf / 2.24),
+ 1,
+ )
+ weight = round(lbm / (1 - desired_bf), 1)
+
+ return {
+ "condition": "%s%% body fat" % (desired_bf * 100),
+ "weight": "%s lbs" % weight,
+ "lbm": "%s lbs" % lbm,
+ "chest": round(1.6817 * wrist + 1.3759 * ankle + 0.3314 * height, 2),
+ "arm": round(1.2033 * wrist + 0.1236 * height, 2),
+ "forearm": round(0.9626 * wrist + 0.0989 * height, 2),
+ "neck": round(1.1424 * wrist + 0.1236 * height, 2),
+ "thigh": round(1.3868 * ankle + 0.1805 * height, 2),
+ "calf": round(0.9298 * ankle + 0.1210 * height, 2),
+ }
+
+
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+# Misc functions
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+def _age(dob: int) -> float:
+ """
+ Calculate age based on birthday.
+
+ @param dob: birth time in UNIX seconds
+ @return: age in years
+ """
+ now = datetime.now().timestamp()
+ years = (now - dob) / (365 * 24 * 3600)
+ return years
+++ /dev/null
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-"""
-Created on Wed Aug 12 15:14:00 2020
-
-@author: shane
-"""
-import csv
-import os
-
-from tabulate import tabulate
-
-from ntclient.core.nutprogbar import nutprogbar
-from ntclient.persistence.sql.nt.funcs import (
- sql_analyze_recipe,
- sql_nt_next_index,
- sql_recipe,
- sql_recipes,
-)
-from ntclient.persistence.sql.usda.funcs import (
- sql_analyze_foods,
- sql_food_details,
- sql_nutrients_overview,
-)
-
-
-def recipes_overview() -> tuple:
- """Shows overview for all recipes"""
- recipes = sql_recipes()[1]
-
- results = []
- for recipe in recipes:
- result = {
- "id": recipe[0],
- "name": recipe[2],
- "tagname": recipe[1],
- "n_foods": recipe[3],
- "weight": recipe[4],
- }
- results.append(result)
-
- table = tabulate(results, headers="keys", tablefmt="presto")
- print(table)
- return 0, results
-
-
-def recipe_overview(recipe_id: int) -> tuple:
- """Shows single recipe overview"""
- recipe = sql_analyze_recipe(recipe_id)
- name = recipe[0][1]
- print(name)
-
- food_ids_dict = {x[2]: x[3] for x in recipe}
- food_ids = set(food_ids_dict.keys())
- food_names = {x[0]: x[3] for x in sql_food_details(food_ids)}
- food_analyses = sql_analyze_foods(food_ids)
-
- table = tabulate(
- [[food_names[food_id], grams] for food_id, grams in food_ids_dict.items()],
- headers=["food", "g"],
- )
- print(table)
- # tabulate nutrient RDA %s
- nutrients = sql_nutrients_overview()
- # rdas = {x[0]: x[1] for x in nutrients.values()}
- progbars = nutprogbar(food_ids_dict, food_analyses, nutrients)
- print(progbars)
-
- return 0, recipe
-
-
-def recipe_import(file_path: str) -> tuple:
- """Import a recipe to SQL database"""
-
- def extract_id_from_filename(path: str) -> int:
- filename = str(os.path.basename(path))
- if (
- "[" in filename
- and "]" in filename
- and filename.index("[") < filename.index("]")
- ):
- # TODO: try, raise: print/warn
- return int(filename.split("[")[1].split("]")[0])
- return 0 # zero is falsy
-
- if os.path.isfile(file_path):
- # TODO: better logic than this
- recipe_id = extract_id_from_filename(file_path) or sql_nt_next_index("recipe")
- print(recipe_id)
- with open(file_path, encoding="utf-8") as file:
- reader = csv.DictReader(file)
- # headers = next(reader)
- rows = list(reader)
- print(rows)
- else: # os.path.isdir()
- print("not implemented ;]")
- return 1, False
-
-
-def recipe_add(name: str, food_amts: dict) -> tuple:
- """Add a recipe to SQL database"""
- print()
- print("New recipe: " + name + "\n")
-
- food_ids = set(food_amts.keys())
- food_names = {x[0]: x[2] for x in sql_food_details(food_ids)}
-
- results = []
- for food_id, grams in food_amts.items():
- results.append([food_id, food_names[food_id], grams])
-
- table = tabulate(results, headers=["id", "food_name", "grams"], tablefmt="presto")
- print(table)
-
- confirm = input("\nCreate recipe? [Y/n] ")
-
- if confirm.lower() == "y":
- print("not implemented ;]")
- return 1, False
-
-
-def recipe_delete(recipe_id: int) -> tuple:
- """Deletes recipe by ID, along with any FK constraints"""
- recipe = sql_recipe(recipe_id)[0]
-
- print(recipe[4])
- confirm = input("Do you wish to delete? [Y/n] ")
-
- if confirm.lower() == "y":
- print("not implemented ;]")
- return 1, False
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+Created on Wed Aug 12 15:14:00 2020
+
+@author: shane
+"""
+import os
+
+from ntclient import NUTRA_HOME, PROJECT_ROOT
+
+RECIPE_STOCK = os.path.join(PROJECT_ROOT, "resources", "recipe")
+
+_RECIPE_SUB_PATH = "recipe"
+RECIPE_HOME = os.path.join(NUTRA_HOME, _RECIPE_SUB_PATH)
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+Created on Fri Jul 22 15:33:57 2022
+
+@author: shane
+CSV utilities for reading and processing recipes.
+
+TODO: copy to & cache in sqlite3, only look to CSV if it doesn't exist?
+ Well then what if they edit CSV... gah.
+"""
+import glob
+
+from ntclient.services.recipe import RECIPE_HOME
+from ntclient.utils import tree
+
+
+def csv_files() -> list:
+ """Returns full filenames for everything under RECIPE_HOME'"""
+ return glob.glob(RECIPE_HOME + "/**/*.csv", recursive=True)
+
+
+def csv_recipe_print_tree() -> None:
+ """Print off the recipe tree"""
+ tree.print_dir(RECIPE_HOME)
+
+
+def csv_print_details() -> None:
+ """Print off details (as table)"""
+ print("Not implemented!")
+
+
+def csv_recipes() -> tuple:
+ """
+ Return overview & analysis of a selected recipe
+ TODO: separate methods to search by uuid OR file_name
+ """
+ _csv_files = csv_files()
+ print(_csv_files)
+ return tuple(_csv_files)
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+Created on Fri Jul 22 17:12:28 2022
+
+@author: shane
+
+Supporting methods for service
+"""
+import os
+import shutil
+
+from ntclient.models import Recipe
+from ntclient.services.recipe import RECIPE_HOME, RECIPE_STOCK, csv_utils
+
+
+def recipes_init(_force: bool = True) -> tuple:
+ """
+ A filesystem function which copies the stock data into
+ os.path.join(NUTRA_HOME, "recipes")
+ TODO: put filesystem functions into separate module and ignore in coverage report.
+
+ TODO: check other places, if the tuple is used or just return code.
+ And potentially create a function or class to return the tuple object
+ as a named tuple, and easily constructed & recognized.
+ @return: (exit_code: int, None)
+ """
+ recipes_destination = os.path.join(RECIPE_HOME, "core")
+
+ if _force and os.path.exists(recipes_destination):
+ print("WARN: force removing core recipes: %s" % recipes_destination)
+ # NOTE: is this best?
+ shutil.rmtree(recipes_destination, ignore_errors=True)
+
+ try:
+ shutil.copytree(RECIPE_STOCK, recipes_destination)
+ return 0, None
+ except FileExistsError:
+ print("ERROR: file/directory exists: %s" % recipes_destination)
+ print(" remove it, or use the '-f' flag")
+ return 1, None
+
+
+def recipes_overview() -> tuple:
+ """
+ Shows overview for all recipes.
+ TODO: Accept recipes input Tuple[tuple], else read from disk.
+ TODO: option to print tree vs. detail view
+
+ @return: (exit_code: int, None)
+ """
+
+ try:
+ csv_utils.csv_recipe_print_tree()
+ return 0, None
+ except FileNotFoundError:
+ print("WARN: no recipes found, create some or run: nutra recipe init")
+ return 1, None
+
+
+def recipe_overview(recipe_path: str) -> tuple:
+ """
+ Shows single recipe overview
+
+ @param recipe_path: full path on disk
+ @return: (exit_code: int, None)
+ """
+
+ try:
+ _recipe = Recipe(recipe_path)
+ _recipe.process_data()
+ # TODO: extract relevant bits off, process, use nutprogbar (e.g. day analysis)
+ return 0, _recipe
+ except (FileNotFoundError, IndexError) as err:
+ print("ERROR: %s" % repr(err))
+ return 1, None
from tabulate import tabulate
from ntclient import (
+ CLI_CONFIG,
DEFAULT_RESULT_LIMIT,
DEFAULT_SEARCH_H_BUFFER,
DEFAULT_SORT_H_BUFFER,
+ NUTR_ID_KCAL,
+ NUTR_IDS_AMINOS,
+ NUTR_IDS_FLAVONES,
)
from ntclient.persistence.sql.usda.funcs import (
sql_analyze_foods,
sql_nutrients_overview,
sql_sort_helper1,
)
-from ntclient.utils import NUTR_ID_KCAL, NUTR_IDS_AMINOS, NUTR_IDS_FLAVONES
def list_nutrients() -> tuple:
"""Lists out nutrients with basic details"""
- from ntclient import PAGING # pylint: disable=import-outside-toplevel
-
headers, nutrients = sql_nutrients_details()
# TODO: include in SQL table cache?
headers.append("avg_rda")
nutrient.append(None)
table = tabulate(nutrients, headers=headers, tablefmt="simple")
- if PAGING:
+ if CLI_CONFIG.paging:
pydoc.pager(table)
else:
print(table)
-"""Constants and default settings"""
-from colorama import Fore, Style
-
-################################################################################
-# Colors and buffer settings
-################################################################################
-
-# TODO: make configurable in SQLite or prefs.json
-
-THRESH_WARN = 0.7
-COLOR_WARN = Fore.YELLOW
-
-THRESH_CRIT = 0.4
-COLOR_CRIT = Style.DIM + Fore.RED
-
-THRESH_OVER = 1.9
-COLOR_OVER = Style.DIM + Fore.MAGENTA
-
-COLOR_DEFAULT = Fore.CYAN
-
-################################################################################
-# Nutrient IDs
-################################################################################
-NUTR_ID_KCAL = 208
-
-NUTR_ID_PROTEIN = 203
-
-NUTR_ID_CARBS = 205
-NUTR_ID_SUGAR = 269
-NUTR_ID_FIBER = 291
-
-NUTR_ID_FAT_TOT = 204
-NUTR_ID_FAT_SAT = 606
-NUTR_ID_FAT_MONO = 645
-NUTR_ID_FAT_POLY = 646
-
-NUTR_IDS_FLAVONES = [
- 710,
- 711,
- 712,
- 713,
- 714,
- 715,
- 716,
- 734,
- 735,
- 736,
- 737,
- 738,
- 731,
- 740,
- 741,
- 742,
- 743,
- 745,
- 749,
- 750,
- 751,
- 752,
- 753,
- 755,
- 756,
- 758,
- 759,
- 762,
- 770,
- 773,
- 785,
- 786,
- 788,
- 789,
- 791,
- 792,
- 793,
- 794,
-]
-
-NUTR_IDS_AMINOS = [
- 501,
- 502,
- 503,
- 504,
- 505,
- 506,
- 507,
- 508,
- 509,
- 510,
- 511,
- 512,
- 513,
- 514,
- 515,
- 516,
- 517,
- 518,
- 521,
-]
--- /dev/null
+#!/usr/bin/env python3
+"""Python 3 reimplementation of the linux 'tree' utility"""
+
+import os
+import sys
+
+try:
+ from colorama import Fore, Style
+ from colorama import init as colorama_init
+
+ COLORAMA_CAPABLE = True
+ colorama_init()
+except ImportError:
+ COLORAMA_CAPABLE = False
+
+chars = {"nw": "\u2514", "nws": "\u251c", "ew": "\u2500", "ns": "\u2502"}
+
+strs = [
+ chars["ns"] + " ",
+ chars["nws"] + chars["ew"] * 2 + " ",
+ chars["nw"] + chars["ew"] * 2 + " ",
+ " ",
+]
+
+if COLORAMA_CAPABLE:
+ # Colors and termination strings
+ COLOR_DIR = Style.BRIGHT + Fore.BLUE
+ COLOR_EXEC = Style.BRIGHT + Fore.GREEN
+ COLOR_LINK = Style.BRIGHT + Fore.CYAN
+ COLOR_DEAD_LINK = Style.BRIGHT + Fore.RED
+else:
+ COLOR_DIR = str()
+ COLOR_EXEC = str()
+ COLOR_LINK = str()
+ COLOR_DEAD_LINK = str()
+
+
+def colorize(path: str, full: bool = False) -> str:
+ """Returns string with color / bold"""
+ file = path if full else os.path.basename(path)
+
+ if os.path.islink(path):
+ return "".join(
+ [
+ COLOR_LINK,
+ file,
+ Style.RESET_ALL,
+ " -> ",
+ colorize(os.readlink(path), full=True),
+ ]
+ )
+
+ if os.path.isdir(path):
+ return "".join([COLOR_DIR, file, Style.RESET_ALL])
+
+ if os.access(path, os.X_OK):
+ return "".join([COLOR_EXEC, file, Style.RESET_ALL])
+
+ return file
+
+
+# Tree properties - display / print
+SHOW_HIDDEN = False
+SHOW_SIZE = False
+FOLLOW_SYMLINKS = False
+
+
+def print_dir(_dir: str, pre: str = str()) -> tuple:
+ """
+ Prints the whole tree
+
+ TODO: integrate with data sources to display more than just filenames
+ TODO: filter hidden files, non-CSV files, and hide *.csv extension from files
+ """
+ n_dirs = 0
+ n_files = 0
+ n_size = 0
+
+ if not pre:
+ print(COLOR_DIR + _dir + Style.RESET_ALL)
+
+ dir_len = len(os.listdir(_dir)) - 1
+ for i, file in enumerate(sorted(os.listdir(_dir), key=str.lower)):
+ path = os.path.join(_dir, file)
+ if file[0] == "." and not SHOW_HIDDEN:
+ continue
+ if os.path.isdir(path):
+ print(pre + strs[2 if i == dir_len else 1] + colorize(path))
+ if os.path.islink(path):
+ n_dirs += 1
+ else:
+ n_d, n_f, n_s = print_dir(path, pre + strs[3 if i == dir_len else 0])
+ n_dirs += n_d + 1
+ n_files += n_f
+ n_size += n_s
+ else:
+ n_files += 1
+ n_size += os.path.getsize(path)
+ print(
+ pre
+ + strs[2 if i == dir_len else 1]
+ + ("[{:>11}] ".format(n_size) if SHOW_SIZE else "")
+ + colorize(path)
+ )
+
+ # noinspection PyRedundantParentheses
+ return (n_dirs, n_files, n_size)
+
+
+def main_tree(_args: list = None) -> int:
+ """Handle input arguments, print off tree"""
+ n_dirs = 0
+ n_files = 0
+
+ if not _args:
+ _args = sys.argv
+
+ if len(_args) == 1:
+ # Used for development
+ n_dirs, n_files, _size = print_dir("../resources")
+ else:
+ for _dir in _args[1:]:
+ n_d, n_f, _size = print_dir(_dir)
+ n_dirs += n_d
+ n_files += n_f
+
+ print()
+ print(
+ "{} director{}, {} file{}".format(
+ n_dirs, "ies" if n_dirs > 1 else "y", n_files, "s" if n_files > 1 else ""
+ )
+ )
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main_tree())
"""
Created on Fri Sep 28 22:25:38 2018
-@author: gamesguru
+@author: shane
"""
import sys
--- /dev/null
+argcomplete==1.8.0
+colorama==0.3.6
+fuzzywuzzy==0.3.0
+tabulate==0.4.3
# Don't update these, they are the last supported versions on winXP, Ubuntu 16.04 & Python 3.4
-coverage==5.5
+coverage<=5.5,>=4.5.4
pytest==3.2.5
-argcomplete<=1.12.3,>=1.8.0
-colorama<=0.4.1,>=0.1.16
-fuzzywuzzy<=0.18.0,>=0.3.0
-tabulate<=0.8.9,>=0.4.3
+argcomplete>=1.8.0
+colorama>=0.3.6,<=0.4.1
+fuzzywuzzy>=0.3.0
+tabulate>=0.4.3
[coverage:report]
fail_under = 80
+; precision = 2
show_missing = True
skip_empty = True
skip_covered = True
+omit =
+ # Unlike the server & db, the CLI doesn't call the sql module.
+ # It directly imports the `build_ntsqlite()` function.
+ ntclient/ntsqlite/sql/__main__.py,
+
[pycodestyle]
[flake8]
per-file-ignores =
# Allow unused imports in __init__.py files
- __init__.py:F401
+ ; __init__.py:F401,
max-line-length = 88
[isort]
-line_length=88
-known_first_party=ntclient
+line_length = 88
+known_first_party = ntclient
# See: https://copdips.com/2020/04/making-isort-compatible-with-black.html
-multi_line_output=3
-include_trailing_comma=true
+multi_line_output = 3
+include_trailing_comma = True
# 3rd party packages missing types
[mypy-argcomplete,colorama,coverage,fuzzywuzzy,psycopg2.*,setuptools]
ignore_missing_imports = True
-
"Programming Language :: Unix Shell",
]
-# Read me
+# ReadMe
with open("README.rst", encoding="utf-8") as file:
README = file.read()
# Requirements
+# TODO: check PY_SYS_VER, and decide which requirements for e.g. 3.4, 3.6, 3.10, etc...
with open("requirements.txt", encoding="utf-8") as file:
REQUIREMENTS = file.read().split()
"install_requires": REQUIREMENTS,
"python_requires": ">=%s" % PY_MIN_STR,
"zip_safe": False,
- "packages": find_packages(exclude=["tests"]),
+ "packages": find_packages(exclude=["tests", "ntclient.docs"]),
"include_package_data": True,
"platforms": ["linux", "darwin", "win32"],
"description": "Home and office nutrient tracking software",
+++ /dev/null
-"""
-Allows contributors to run tests on their machine (and in GitHub actions / Travis CI)
-"""
-import os
-import subprocess # nosec: B404
-import sys
-
-import coverage
-
-
-def main() -> int:
- """
- Main test method, callable with `python -m tests`
-
- 1. Calls a subprocess for `coverage run`
- 2. Programmatically invokes coverage.report()
- """
-
- cmd = "coverage run -m pytest -v -s -p no:cacheprovider tests/"
- print(cmd)
- subprocess.call(cmd.split(), shell=False) # nosec: B603
-
- print("\ncoverage report -m --skip-empty")
- cov = coverage.Coverage()
- cov.load()
- cov.report(show_missing=True, skip_empty=True)
-
- # Try to clean up
- try:
- os.remove(".coverage")
- except (FileNotFoundError, PermissionError) as error:
- print("WARN: failed to remove `.coverage`, %s" % repr(error))
-
- return 0
-
-
-if __name__ == "__main__":
- sys.exit(main())
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+Created on Wed Jul 20 21:36:43 2022
+
+@author: shane
+"""
+import os
+import unittest
+
+import pytest
+
+import ntclient.services.recipe.utils as r
+from ntclient.services.recipe import RECIPE_STOCK
+
+
+class TestRecipe(unittest.TestCase):
+ """Tests the recipe service"""
+
+ def test_recipes_init(self):
+ """Checks the init function, which copies over default data (if not already)"""
+
+ exit_code, _result = r.recipes_init(_force=False)
+ assert exit_code in {0, 1}
+
+ exit_code, _result = r.recipes_init(_force=True)
+ assert exit_code in {0, 1}
+
+ def test_recipes_overview(self):
+ """Test type coercion and one-to-one input/output relationship"""
+
+ exit_code, _ = r.recipes_overview()
+ assert exit_code == 0
+
+ @unittest.expectedFailure
+ @pytest.mark.xfail(reason="Due to a wip refactor")
+ def test_recipe_overview_throws_exc_for_nonexistent_path(self):
+ """Raises index error if recipe int id is invalid"""
+
+ # TODO: should we be using guid / uuid instead of integer id?
+ with pytest.raises(IndexError):
+ r.recipe_overview("-12345-FAKE-PATH-")
+
+ def test_recipe_overview_might_succeed_for_maybe_existing_id(self):
+ """Tries check for existing ID, but only can if the user initialized"""
+ exit_code, _ = r.recipe_overview(
+ os.path.join(RECIPE_STOCK, "dinner", "burrito-bowl.csv")
+ )
+ assert exit_code in {0, 1}
# -*- coding: utf-8 -*-
"""
+Most of the original tests for the CLI package were developed here.
+Need to offload them into special modules. The refactor has started.
+
Created on Fri Jan 31 15:19:53 2020
@author: shane
"""
-# pylint: disable=wrong-import-position
import os
import sys
+import unittest
import pytest
from ntclient import (
+ CLI_CONFIG,
NTSQLITE_BUILDPATH,
NUTRA_HOME,
USDA_DB_NAME,
__db_target_nt__,
__db_target_usda__,
- set_flags,
)
-from ntclient.__main__ import build_argparser
+from ntclient.__main__ import build_arg_parser
from ntclient.__main__ import main as nt_main
from ntclient.core import nutprogbar
from ntclient.ntsqlite.sql import build_ntsqlite
from ntclient.persistence.sql.usda import funcs as usda_funcs
from ntclient.persistence.sql.usda import sql as _usda_sql
from ntclient.persistence.sql.usda import usda_ver
-from ntclient.services import init
+from ntclient.services import init, usda
+from ntclient.services.recipe import RECIPE_HOME
from ntclient.utils.exceptions import SqlInvalidVersionError
TEST_HOME = os.path.dirname(os.path.abspath(__file__))
-# NOTE: this doesn't work currently, b/c it's already read up (in imports above)
-# We're just setting it on the shell, as an env var
-# os.environ["NUTRA_HOME"] = os.path.join(TEST_HOME, ".nutra.test")
-
-# TODO: integration tests.. create user, recipe, log.. analyze & compare
-arg_parser = build_argparser()
-
-
-def test_000_init():
- """Tests the SQL/persistence init in real time"""
- code, result = init(yes=True)
- assert code == 0
- assert result
-
-
-def test_100_usda_sql_funcs():
- """Performs cursory inspection (sanity checks) of usda.sqlite3 image"""
- version = usda_ver()
- assert version == __db_target_usda__
- result = usda_funcs.sql_nutrients_details()
- assert len(result[1]) == 186
-
- result = usda_funcs.sql_servings({9050, 9052})
- assert len(result) == 3
-
- result = usda_funcs.sql_analyze_foods({23567, 23293})
- assert len(result) == 188
-
- result = usda_funcs.sql_sort_foods(789)
- assert len(result) == 415
- # result = usda_funcs.sql_sort_foods(789, fdgrp_ids=[100])
- # assert len(result) == 1
-
- result = usda_funcs.sql_sort_foods_by_kcal(789)
- assert len(result) == 246
- # result = usda_funcs.sql_sort_foods_by_kcal(789, fdgrp_ids=[1100])
- # assert len(result) == 127
-
-
-def test_200_nt_sql_funcs():
- """Performs cursory inspection (sanity check) of nt.sqlite3 image"""
- version = nt_ver()
- assert version == __db_target_nt__
-
- next_index = sql_nt_next_index("bf_eq")
- assert next_index > 0
-
- # TODO: add more tests; we used to comb over biometrics here
-
-
-def test_300_argparser_debug_no_paging():
- """Verifies the debug and no_paging flags are set"""
- args = arg_parser.parse_args(args=["-d", "--no-pager"])
- set_flags(args)
-
- assert args.debug is True
- assert args.no_pager is True
-
- from ntclient import DEBUG, PAGING # pylint: disable=import-outside-toplevel
-
- assert DEBUG is True
- assert PAGING is False
-
-
-def test_400_usda_argparser_funcs():
- """Tests udsa functions in argparser.funcs (to varying degrees each)"""
- # Init
- args = arg_parser.parse_args(args=["init", "-y"])
- assert args.yes is True
- code, result = args.func(args=args)
- assert code == 0
- assert result
-
- # Nutrients ( and `--no-pager` flag)
- args = arg_parser.parse_args(args=["--no-pager", "nt"])
- set_flags(args) # unnecessary due to already happening, but hey
- code, result = args.func()
- assert code == 0
- assert len(result) == 186
-
- # Search
- args = arg_parser.parse_args(args=["search", "grass", "beef"])
- code, result = args.func(args)
- assert code == 0
- assert result
- # Top 20 (beats injecting BUFFER_HT/DEFAULT_RESULT_LIMIT)
- args = arg_parser.parse_args(args=["search", "grass", "beef", "-t", "20"])
- code, result = args.func(args)
- assert code == 0
- assert len(result) == 20
- assert result[0]["long_desc"] is not None
-
- # Sort
- args = arg_parser.parse_args(args=["sort", "789"])
- code, result = args.func(args)
- assert code == 0
- assert result
- # Top 20
- args = arg_parser.parse_args(args=["sort", "789", "-t", "20"])
- code, result = args.func(args)
- assert code == 0
- assert len(result) == 20
- assert result[0][4] == "Capers, raw"
-
- # Anl
- args = arg_parser.parse_args(args=["anl", "9053"])
- code, nutrients_rows, servings_rows = args.func(args)
- assert code == 0
- assert len(nutrients_rows[0]) == 30
- assert len(servings_rows[0]) == 1
-
- # Day
- rda_csv_path = os.path.join(TEST_HOME, "resources", "rda", "dog-18lbs.csv")
- day_csv_path = os.path.join(TEST_HOME, "resources", "day", "dog.csv")
- args = arg_parser.parse_args(args=["day", "-r", rda_csv_path, day_csv_path])
- code, result = args.func(args)
- assert code == 0
- assert result[0][213] == 1.295
- assert len(result[0]) == 177
-
-
-def test_401_invalid_path_day_throws_error():
- """Ensures invalid path throws exception in `day` subcommand"""
- invalid_day_csv_path = os.path.join(
- TEST_HOME, "resources", "day", "__NONEXISTENT_CSV_FILE__.csv"
- )
- with pytest.raises(SystemExit) as sys_exit:
- arg_parser.parse_args(args=["day", invalid_day_csv_path])
- assert sys_exit.value.code == 2
-
- invalid_rda_csv_path = os.path.join(
- TEST_HOME, "resources", "rda", "__NONEXISTENT_CSV_FILE__.csv"
- )
- with pytest.raises(SystemExit) as sys_exit:
- arg_parser.parse_args(
- args=["day", "-r", invalid_rda_csv_path, invalid_day_csv_path]
+arg_parser = build_arg_parser()
+
+
+# TODO: attach some env props to it, and re-instantiate a CliConfig() class.
+# We're just setting it on the shell, as an env var, before running tests in CI.
+# e.g. the equivalent of putting this early in the __init__ file;
+# os.environ["NUTRA_HOME"] = os.path.join(TEST_HOME, ".nutra.test")
+
+
+class TestCli(unittest.TestCase):
+ """
+ Original one-stop-shop for testing.
+ @todo: integration tests.. create user, recipe, log.. analyze & compare
+ """
+
+ def test_000_init(self):
+ """Tests the SQL/persistence init in real time"""
+ code, result = init(yes=True)
+ assert code == 0
+ assert result
+
+ def test_100_usda_sql_funcs(self):
+ """Performs cursory inspection (sanity checks) of usda.sqlite3 image"""
+ version = usda_ver()
+ assert version == __db_target_usda__
+ result = usda_funcs.sql_nutrients_details()
+ assert len(result[1]) == 186
+
+ result = usda_funcs.sql_servings({9050, 9052})
+ assert len(result) == 3
+
+ result = usda_funcs.sql_analyze_foods({23567, 23293})
+ assert len(result) == 188
+
+ result = usda_funcs.sql_sort_foods(789)
+ assert len(result) == 415
+ # result = usda_funcs.sql_sort_foods(789, fdgrp_ids=[100])
+ # assert len(result) == 1
+
+ result = usda_funcs.sql_sort_foods_by_kcal(789)
+ assert len(result) == 246
+ # result = usda_funcs.sql_sort_foods_by_kcal(789, fdgrp_ids=[1100])
+ # assert len(result) == 127
+
+ def test_200_nt_sql_funcs(self):
+ """Performs cursory inspection (sanity check) of nt.sqlite3 image"""
+ version = nt_ver()
+ assert version == __db_target_nt__
+
+ next_index = sql_nt_next_index("bf_eq")
+ assert next_index > 0
+
+ # TODO: add more tests; we used to comb over biometrics here
+
+ def test_300_argparser_debug_no_paging(self):
+ """Verifies the debug and no_paging flags are set"""
+ args = arg_parser.parse_args(args=["-d", "--no-pager"])
+ CLI_CONFIG.set_flags(args)
+
+ assert args.debug is True
+ assert args.no_pager is True
+
+ assert CLI_CONFIG.debug is True
+ assert CLI_CONFIG.paging is False
+
+ def test_400_usda_argparser_funcs(self):
+ """Tests udsa functions in argparser.funcs (to varying degrees each)"""
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ # Init
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ args = arg_parser.parse_args(args=["init", "-y"])
+ assert args.yes is True
+ code, result = args.func(args=args)
+ assert code == 0
+ assert result
+
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ # Nutrients ( and `--no-pager` flag)
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ args = arg_parser.parse_args(args=["--no-pager", "nt"])
+ CLI_CONFIG.set_flags(args) # unnecessary due to already happening, but hey
+ code, result = args.func()
+ assert code == 0
+ assert len(result) == 186
+
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ # Search
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ args = arg_parser.parse_args(args=["search", "grass", "beef"])
+ code, result = args.func(args)
+ assert code == 0
+ assert result
+
+ # Top 20 (beats injecting BUFFER_HT/DEFAULT_RESULT_LIMIT)
+ # --------------------
+ args = arg_parser.parse_args(args=["search", "grass", "beef", "-t", "20"])
+ code, result = args.func(args)
+ assert code == 0
+ assert len(result) == 20
+ assert result[0]["long_desc"] is not None
+
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ # Sort
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ args = arg_parser.parse_args(args=["sort", "789"])
+ code, result = args.func(args)
+ assert code == 0
+ assert result
+
+ # Top 20
+ # --------------------
+ args = arg_parser.parse_args(args=["sort", "789", "-t", "20"])
+ code, result = args.func(args)
+ assert code == 0
+ assert len(result) == 20
+ assert result[0][4] == "Capers, raw"
+
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ # Anl
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ args = arg_parser.parse_args(args=["anl", "9053"])
+ code, nutrients_rows, servings_rows = args.func(args)
+ assert code == 0
+ assert len(nutrients_rows[0]) == 30
+ assert len(servings_rows[0]) == 1
+
+ def test_410_nt_argparser_funcs(self):
+ """Tests nt functions in argparser.funcs (to varying degrees each)"""
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ # Day
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ rda_csv_path = os.path.join(TEST_HOME, "resources", "rda", "dog-18lbs.csv")
+ day_csv_path = os.path.join(TEST_HOME, "resources", "day", "dog.csv")
+ args = arg_parser.parse_args(args=["day", "-r", rda_csv_path, day_csv_path])
+ code, result = args.func(args)
+ assert code == 0
+ assert result[0][213] == 1.295
+ assert len(result[0]) == 177
+
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ # Recipe
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ args = arg_parser.parse_args(args=["recipe", "init", "-f"])
+ code, _ = args.func(args)
+ assert code == 0
+
+ # Recipes overview
+ # --------------------
+ args = arg_parser.parse_args(args=["recipe"])
+ code, _ = args.func()
+ assert code == 0
+
+ # Detail view (one recipe)
+ # --------------------
+ args = arg_parser.parse_args(
+ args=[
+ "recipe",
+ "anl",
+ os.path.join(RECIPE_HOME, "core", "dinner", "burrito-bowl.csv"),
+ ]
)
- assert sys_exit.value.code == 2
-
-
-def test_402_nt_argparser_funcs():
- """Tests nt functions in argparser.funcs (to varying degrees each)"""
-
-
-def test_500_main_module():
- """Tests execution of main() and __main__, in __main__.py"""
- code = nt_main(args=["--no-pager", "nt"])
- assert code == 0
-
- sys.argv = ["./nutra"]
- code = nt_main()
- assert code == 0
-
- with pytest.raises(SystemExit) as system_exit:
- nt_main(args=["-h"])
- assert system_exit.value.code == 0
-
- # __main__: if args_dict
- code = nt_main(args=["anl", "9053", "-g", "80"])
- assert code == 0
-
+ code, _ = args.func(args)
+ assert code == 0
+
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ # Calc
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ # 1rm
+ # -----------------------------------
+ args = arg_parser.parse_args(args=["calc", "1rm", "225", "12"])
+ code, _ = args.func(args)
+ assert code == 0
+
+ # Invalid range for dos_remedios (11 instead of 12)
+ args = arg_parser.parse_args(args=["calc", "1rm", "225", "11"])
+ code, result = args.func(args)
+ assert code == 0
+ assert set(result.keys()) == {"epley", "brzycki", "dos_remedios"}
+ assert "errMsg" in result["dos_remedios"]
+
+ # BMR
+ # -----------------------------------
+ args = arg_parser.parse_args(
+ args="calc bmr -a 29 -wt 75 -ht 179 -bf 0.11 -x 3".split()
+ )
+ code, _ = args.func(args)
+ assert code == 0
+
+ # Failed / missing optional: height & body_fat (provoke exceptions)
+ args = arg_parser.parse_args(args="calc bmr -a 29 -wt 75 -x 3".split())
+ code, _ = args.func(args)
+ assert code in {0, 1}
+
+ # Body fat
+ # -----------------------------------
+
+ # Navy only
+ args = arg_parser.parse_args(args="calc bf -ht 178 -w 80 -n 40".split())
+ code, result = args.func(args)
+ assert code == 0
+ assert result["navy"] == 10.64
+
+ # Invalid (failed Navy)
+ args = arg_parser.parse_args(args="-d calc bf -w 80 -n 40".split())
+ CLI_CONFIG.set_flags(args)
+ code, result = args.func(args)
+ assert code in {0, 1} # Might be a failed code one day, but returns 0 for now
+
+ # All
+ args = arg_parser.parse_args(
+ args="calc bf -ht 179 -w 80 -n 40 -a 29 7 13 10 9 9 11 10".split()
+ )
+ code, result = args.func(args)
+ assert code == 0
+ assert result["navy"] == 10.48
+ assert result["threeSite"] == 8.95
+ assert result["sevenSite"] == 9.93
+
+ # Female test
+ # TODO: better values, and don't require hip above (it's 0)
+ args = arg_parser.parse_args(
+ args="calc bf -F -a 29 -ht 178 -w 70 -hip 100 -n 35 "
+ "15 23 19 14 11 10 9".split()
+ )
+ code, result = args.func(args)
+ assert code == 0
+ assert result["navy"] == 22.58
+
+ # Lean body limits (young men)
+ # -----------------------------------
+ args = arg_parser.parse_args(args="calc lbl 179 0.1 17.2 21.5".split())
+ code, result = args.func(args)
+ assert code == 0
+ # NOTE: wip
+ print(result)
+
+ def test_415_invalid_path_day_throws_error(self):
+ """Ensures invalid path throws exception in `day` subcommand"""
+ invalid_day_csv_path = os.path.join(
+ TEST_HOME, "resources", "day", "__NONEXISTENT_CSV_FILE__.csv"
+ )
+ with pytest.raises(SystemExit) as sys_exit:
+ arg_parser.parse_args(args=["day", invalid_day_csv_path])
+ assert sys_exit.value.code == 2
-def test_600_sql_integrity_error__service_wip():
- """Provokes IntegrityError in nt.sqlite3"""
+ invalid_rda_csv_path = os.path.join(
+ TEST_HOME, "resources", "rda", "__NONEXISTENT_CSV_FILE__.csv"
+ )
+ with pytest.raises(SystemExit) as sys_exit:
+ arg_parser.parse_args(
+ args=["day", "-r", invalid_rda_csv_path, invalid_day_csv_path]
+ )
+ assert sys_exit.value.code == 2
+
+ def test_500_main_module(self):
+ """Tests execution of main() and __main__, in __main__.py"""
+ code = nt_main(args=["--no-pager", "nt"])
+ assert code == 0
+
+ # Injection test
+ sys.argv = ["./nutra"]
+ code = nt_main()
+ assert code == 0
+
+ # -h
+ with pytest.raises(SystemExit) as system_exit:
+ nt_main(args=["-h"])
+ assert system_exit.value.code == 0
+
+ # -d
+ code = nt_main(args=["-d"])
+ assert code == 0
+
+ # __main__: if args_dict
+ code = nt_main(args=["anl", "9053", "-g", "80"])
+ assert code == 0
+
+ # nested sub-command with no args
+ code = nt_main(args=["calc"])
+ assert code == 0
+
+ @unittest.skip(reason="Vestigial stub, needs replacement / updating.")
+ def test_600_sql_integrity_error__service_wip(self):
+ """Provokes IntegrityError in nt.sqlite3"""
+
+ # TODO: replace with non-biometric test
+ # from ntclient.services import biometrics
+ #
+ # args = arg_parser.parse_args(args=["-d", "bio", "log", "add", "12,12"])
+ # biometrics.input = (
+ # lambda x: "y"
+ # ) # mocks input, could also pass `-y` flag or set yes=True
+ #
+ # with pytest.raises(sqlite3.IntegrityError) as integrity_error:
+ # args.func(args)
+ # assert (
+ # integrity_error.value.args[0]
+ # == "NOT NULL constraint failed: biometric_log.profile_id"
+ # )
+
+ def test_700_build_ntsqlite_succeeds(self):
+ """Verifies the service level call for git submodule"""
+ try:
+ os.remove(NTSQLITE_BUILDPATH)
+ except FileNotFoundError:
+ pass
+ assert not os.path.exists(NTSQLITE_BUILDPATH)
+
+ result = build_ntsqlite(verbose=True)
+ assert result is True
+ assert os.path.isfile(NTSQLITE_BUILDPATH)
+ os.remove(NTSQLITE_BUILDPATH)
- # TODO: replace with non-biometric test
- # from ntclient.services import biometrics # pylint: disable=import-outside-toplevel
- #
- # args = arg_parser.parse_args(args=["-d", "bio", "log", "add", "12,12"])
- # biometrics.input = (
- # lambda x: "y"
- # ) # mocks input, could also pass `-y` flag or set yes=True
- #
- # with pytest.raises(sqlite3.IntegrityError) as integrity_error:
- # args.func(args)
- # assert (
- # integrity_error.value.args[0]
- # == "NOT NULL constraint failed: biometric_log.profile_id"
- # )
+ @unittest.skip(reason="Long-running test, want to replace with more 'unit' style")
+ def test_800_usda_upgrades_or_downgrades(self):
+ """Ensures the static usda.sqlite3 file can be upgraded/downgraded as needed"""
+ version = usda_ver()
+ major, minor, release = version.split(".")
+ new_release = str(int(release) + 1)
+ new_version = ".".join([major, minor, new_release])
+ _usda_sql(
+ "INSERT INTO version (version) VALUES (?)",
+ values=(new_version,),
+ version_check=False,
+ )
+ code, successful = init(yes=True)
+ assert code == 0
+ assert successful is True
-def test_700_build_ntsqlite_succeeds():
- """Verifies the service level call for git submodule"""
- try:
- os.remove(NTSQLITE_BUILDPATH)
- except FileNotFoundError:
- pass
- assert not os.path.exists(NTSQLITE_BUILDPATH)
-
- result = build_ntsqlite(verbose=True)
- assert result is True
- assert os.path.isfile(NTSQLITE_BUILDPATH)
- os.remove(NTSQLITE_BUILDPATH)
-
-
-def test_800_usda_upgrades_or_downgrades():
- """Ensures the static usda.sqlite3 file can be upgraded/downgraded as needed"""
- version = usda_ver()
- major, minor, release = version.split(".")
- new_release = str(int(release) + 1)
- new_version = ".".join([major, minor, new_release])
- _usda_sql(
- "INSERT INTO version (version) VALUES (?)",
- values=(new_version,),
- version_check=False,
- )
-
- code, successful = init(yes=True)
- assert code == 0
- assert successful is True
-
-
-def test_801_sql_invalid_version_error_if_version_old():
- """Throws base custom SqlException...
- TODO: why lines still missing in `coverage` for __main__ ?"""
- _usda_sql(
- "DELETE FROM version WHERE version=?",
- values=(__db_target_usda__,),
- version_check=False,
- )
-
- with pytest.raises(SqlInvalidVersionError) as sql_invalid_version_error:
- nt_main(["-d", "nt"])
- assert sql_invalid_version_error is not None
-
-
-def test_802_usda_downloads_fresh_if_missing_or_deleted():
- """Ensure download of usda.sqlite3.tar.xz, if usda.sqlite3 is missing"""
- from ntclient.persistence.sql import usda # pylint: disable=import-outside-toplevel
-
- # TODO: similar for nt.sqlite3?
- # Define development standards.. rebuilding, deleting, preserving
- # remove whole `.nutra` in a special test?
- try:
- # TODO: export USDA_DB_PATH at package level,
- # don't pepper os.path.join() throughout code?
- usda_path = os.path.join(NUTRA_HOME, USDA_DB_NAME)
- os.remove(usda_path)
- except (FileNotFoundError, PermissionError) as err:
- # TODO: resolve PermissionError on Windows
- print(repr(err))
+ @unittest.skip(reason="Long-running test, want to replace with more 'unit' style")
+ def test_801_sql_invalid_version_error_if_version_old(self):
+ """Throws base custom SqlException...
+ TODO: why lines still missing in `coverage` for __main__ ?"""
_usda_sql(
- "INSERT INTO version (version) VALUES (?)",
+ "DELETE FROM version WHERE version=?",
values=(__db_target_usda__,),
version_check=False,
)
- pytest.xfail("PermissionError, are you using Microsoft Windows?")
-
- usda.input = lambda x: "y" # mocks input, could also pass `-y` flag or set yes=True
- code, successful = init()
- assert code == 0
- assert successful is True
-
-
-def test_900_nut_rda_bar():
- """Verifies colored/visual output is correctly generated"""
- analysis = usda_funcs.sql_analyze_foods(food_ids={1001})
- nutrients = usda_funcs.sql_nutrients_overview()
- output = nutprogbar.nutprogbar(
- food_amts={1001: 100}, food_analyses=analysis, nutrients=nutrients
- )
- assert output
+
+ with pytest.raises(SqlInvalidVersionError) as sql_invalid_version_error:
+ nt_main(["-d", "nt"])
+ assert sql_invalid_version_error is not None
+
+ @unittest.skip(reason="Long-running test, want to replace with more 'unit' style")
+ def test_802_usda_downloads_fresh_if_missing_or_deleted(self):
+ """Ensure download of usda.sqlite3.tar.xz, if usda.sqlite3 is missing"""
+
+ # TODO: similar for nt.sqlite3?
+ # Define development standards.. rebuilding, deleting, preserving
+ # remove whole `.nutra` in a special test?
+ try:
+ # TODO: export USDA_DB_PATH at package level,
+ # don't pepper os.path.join() throughout code?
+ usda_path = os.path.join(NUTRA_HOME, USDA_DB_NAME)
+ os.remove(usda_path)
+ except (FileNotFoundError, PermissionError) as err:
+ # TODO: resolve PermissionError on Windows
+ print(repr(err))
+ _usda_sql(
+ "INSERT INTO version (version) VALUES (?)",
+ values=(__db_target_usda__,),
+ version_check=False,
+ )
+ pytest.xfail("PermissionError, are you using Microsoft Windows?")
+
+ # mocks input, could also pass `-y` flag or set yes=True
+ usda.input = lambda x: "y"
+
+ code, successful = init()
+ assert code == 0
+ assert successful is True
+
+ def test_900_nut_rda_bar(self):
+ """Verifies colored/visual output is successfully generated"""
+ analysis = usda_funcs.sql_analyze_foods(food_ids={1001})
+ nutrients = usda_funcs.sql_nutrients_overview()
+ output = nutprogbar.nutprogbar(
+ food_amts={1001: 100}, food_analyses=analysis, nutrients=nutrients
+ )
+ assert output
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+Created on Sat Jul 23 09:28:33 2022
+
+@author: shane
+"""
+import unittest
+
+import pytest
+
+from ntclient.services.recipe import RECIPE_STOCK
+from ntclient.utils import tree
+
+
+class TestTree(unittest.TestCase):
+ """Try to test remaining bits of tree.py"""
+
+ def test_tree_main(self):
+ """Tests the main function (mostly a command line utility) for tree.py"""
+ with pytest.raises(FileNotFoundError):
+ tree.main_tree()
+
+ def test_tree_main_with_args(self):
+ """Tests the main function (mostly a command line utility) for tree.py"""
+ exit_code = tree.main_tree(_args=["tree.py", RECIPE_STOCK])
+ assert exit_code == 0