release-0.2.6 (#15) v0.2.6
authorShane Jaroch <chown_tee@proton.me>
Mon, 8 Aug 2022 21:37:12 +0000 (17:37 -0400)
committerGitHub <noreply@github.com>
Mon, 8 Aug 2022 21:37:12 +0000 (17:37 -0400)
resolve duplicated code around colorama init

add input validation. non-negative weight & reps

add quadratic term to brzycki equation

dos remedios full rep range; drop test & exc logic

add .wls script for regression fits

13 files changed:
.gitattributes [new file with mode: 0644]
CHANGELOG.rst
MANIFEST.in
ntclient/__init__.py
ntclient/argparser/__init__.py
ntclient/argparser/funcs.py
ntclient/resources/math/1rm-regressions.wls [new file with mode: 0755]
ntclient/services/analyze.py
ntclient/services/calculate.py
ntclient/utils/colors.py
ntclient/utils/tree.py
tests/services/test_calculate.py
tests/test_cli.py

diff --git a/.gitattributes b/.gitattributes
new file mode 100644 (file)
index 0000000..28924fe
--- /dev/null
@@ -0,0 +1 @@
+*.wls linguist-language=Mathematica
index bcebacb3cf07fdfd7c933ff149093401c0b8e586..41bc5c17259835e739ca95802675fc673ec5ca70 100644 (file)
@@ -9,19 +9,20 @@ and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0
 
 
 
-[Unreleased]
+[0.2.6] - 2022-08-08
 ########################################################################
 
 Added
 ~~~~~
 
 - Recipes are stored in a ``csv`` format now with ``uuid`` instead of ``int``,
-  and they can be viewed in a convenient ``tree`` output.
+  and they can be viewed in a convenient ``tree`` output
+  (**NOTE:** recipes is still a WIP).
 - 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``.
-- Dedicated test file for calculate service.
+- Dedicated test file for ``calculate.py`` service.
 
 Changed
 ~~~~~~~
@@ -33,9 +34,15 @@ Changed
 - Start to split apart some of the original ``test_cli`` functions.
 - Enable more verbose ``mypy`` flags.
 
+Fixed
+~~~~~
+
+- Faulty algebra in the ``orm_brzycki`` equation.
+- Missing rep ranges for ``dos_remedios`` equation.
 
 
-[0.2.4] - 2022-07-20
+
+[0.2.5] - 2022-07-20
 ########################################################################
 
 Added
@@ -53,6 +60,16 @@ Changed
 
 
 
+[0.2.4] - 2022-07-12
+########################################################################
+
+Fixed
+~~~~~
+
+- Error when doing a pip install: ``NTSQLITE_DESTINATION`` is not defined.
+
+
+
 [0.2.3] - 2022-07-12
 ########################################################################
 
index 01d33110c90a66c128d5fdb617c0f091a61fcf77..b9fc9b49177610983390861ee1c4bab0b4a9aa0b 100644 (file)
@@ -7,3 +7,4 @@ include ntclient/ntsqlite/sql/data/*.csv
 recursive-include ntclient/resources/ *.csv
 
 global-exclude *.sqlite3
+global-exclude *.wls
index 7407dfc9f9adb7a1d79b454533f2ed13752a6130..204a9e767ea9f32e6bd4342670020c0b986a5c84 100644 (file)
@@ -14,20 +14,12 @@ 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
+from ntclient.utils import colors
 
 # Package info
 __title__ = "nutra"
-__version__ = "0.2.6.dev5"
+__version__ = "0.2.6"
 __author__ = "Shane Jaroch"
 __email__ = "chown_tee@proton.me"
 __license__ = "GPL v3"
@@ -96,31 +88,18 @@ class RdaColors(Enum):
     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_WARN = colors.COLOR_WARN
+    COLOR_CRIT = colors.COLOR_CRIT
+    COLOR_OVER = colors.COLOR_OVER
 
-        COLOR_RESET_ALL = Style.RESET_ALL
+    COLOR_DEFAULT = colors.COLOR_DEFAULT
 
-        # 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
+    STYLE_RESET_ALL = colors.STYLE_RESET_ALL
 
-        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
+    # Used in macro bars
+    COLOR_YELLOW = colors.COLOR_YELLOW
+    COLOR_BLUE = colors.COLOR_BLUE
+    COLOR_RED = colors.COLOR_RED
 
 
 class _CliConfig:
@@ -140,7 +119,8 @@ class _CliConfig:
         self.color_over = RdaColors.COLOR_OVER.value
         self.color_default = RdaColors.COLOR_DEFAULT.value
 
-        self.color_reset_all = RdaColors.COLOR_RESET_ALL.value
+        self.style_reset_all = RdaColors.STYLE_RESET_ALL.value
+
         self.color_yellow = RdaColors.COLOR_YELLOW.value
         self.color_red = RdaColors.COLOR_RED.value
         self.color_blue = RdaColors.COLOR_BLUE.value
index f410178693f8e3bf12b3e81c0d1208a1aa33ab28..698c33902cb9ccf86433f491b1a0382e95ce7503 100644 (file)
@@ -187,7 +187,7 @@ 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"
+        "calc", help="calculate 1-rep max, body fat, BMR, etc."
     )
 
     calc_subparsers = calc_parser.add_subparsers(title="recipe subcommands")
index 7890169a5b6da0ccc615446c5fdba92c1ea4c480..a9ac713ebd7854ec170bbb97d0e935a75f20afb5 100644 (file)
@@ -110,6 +110,13 @@ def calc_1rm(args: argparse.Namespace) -> tuple:
     reps = int(args.reps)
     print("Reps:   %s" % reps)
 
+    if weight < 0:
+        print("ERROR: weight must be greater than zero")
+        return 1, None
+    if reps < 1 or reps > 20:
+        print("ERROR: reps must be between 1 and 20")
+        return 1, None
+
     _epley = calc.orm_epley(weight, reps)
     _brzycki = calc.orm_brzycki(weight, reps)
     _dos_remedios = calc.orm_dos_remedios(weight, reps)
@@ -122,18 +129,13 @@ def calc_1rm(args: argparse.Namespace) -> tuple:
     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)
+            # Round down for now
+            row.append(int(_values[_rep]))
         _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)
diff --git a/ntclient/resources/math/1rm-regressions.wls b/ntclient/resources/math/1rm-regressions.wls
new file mode 100755 (executable)
index 0000000..962ea89
--- /dev/null
@@ -0,0 +1,43 @@
+#!/usr/bin/env wolframscript
+(* ::Package:: *)
+
+(* ::Input:: *)
+(*(* Epley *)*)
+(*epley[n_]:=30/(29 +n)*)
+(**)
+(*(* Brzycki *)*)
+(*(*brzLin[n_]:=(37-n)/36*)*)
+(*brz[n_]:=(37-n+0.005n^2)/36*)
+(**)
+(*(* Dos Remedios *)*)
+(*dosPts={{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},{20,0.55}};*)
+(*(*ListLinePlot[{dosPts}, Filling->Axis]*)*)
+(*dos:=Interpolation[dosPts]*)
+(*(*dos[2]*)*)
+(*(*epley[20.]*)
+(*brz[20]*)
+(*dos[20]*)*)
+(*Plot[\!\(\**)
+(*TagBox[*)
+(*RowBox[{"{", *)
+(*RowBox[{*)
+(*RowBox[{"epley", "[", "n", "]"}], ",", *)
+(*RowBox[{"brz", "[", "n", "]"}], ",", *)
+(*RowBox[{"dos", "[", "n", "]"}]}], "}"}],*)
+(*Short[#, 2]& ]\),{n,1,20}, AxesLabel->{"# of reps", "% of 1-rep max"},PlotLegends->"Expressions"]*)
+(*dos[4]*)
+(*dos[7]*)
+(*dos[9]*)
+(*dos[11]*)
+(*dos[12] (* Known Value *)*)
+(*dos[13]*)
+(*dos[14]*)
+(*dos[15] (* Known Value *)*)
+(*dos[16]*)
+(*dos[17]*)
+(*dos[18]*)
+(*dos[19]*)
+(*dos[20]*)
+
+
+
index 738a3df57ff19d46facffdddb147f8c0511defb8..4b533dbb60a40fb1ff5684d7bc475d88476e1d54 100644 (file)
@@ -213,7 +213,7 @@ def day_format(analysis: dict, nutrients: dict, buffer: int = 0) -> None:
         print("~~~~~~~~~~~~~~~~~~~~~~~~~~~")
         print("--> %s" % header)
         print("~~~~~~~~~~~~~~~~~~~~~~~~~~~")
-        print(CLI_CONFIG.color_reset_all)
+        print(CLI_CONFIG.style_reset_all)
 
     def print_macro_bar(
         _fat: float, _net_carb: float, _pro: float, _kcals_max: float, _buffer: int = 0
@@ -242,7 +242,7 @@ def day_format(analysis: dict, nutrients: dict, buffer: int = 0) -> None:
             + c_buf
             + CLI_CONFIG.color_red
             + p_buf
-            + CLI_CONFIG.color_reset_all
+            + CLI_CONFIG.style_reset_all
         )
 
         # Bars
@@ -250,7 +250,7 @@ def day_format(analysis: dict, nutrients: dict, buffer: int = 0) -> None:
         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 + ">")
+        print(CLI_CONFIG.style_reset_all + ">")
 
         # Calorie footers
         k_fat = str(round(fat * 9))
@@ -267,7 +267,7 @@ def day_format(analysis: dict, nutrients: dict, buffer: int = 0) -> None:
             + c_buf
             + CLI_CONFIG.color_red
             + p_buf
-            + CLI_CONFIG.color_reset_all
+            + CLI_CONFIG.style_reset_all
         )
 
     def print_nute_bar(_n_id: int, amount: float, _nutrients: dict) -> tuple:
@@ -299,7 +299,7 @@ def day_format(analysis: dict, nutrients: dict, buffer: int = 0) -> None:
         print(" {0}<".format(color), end="")
         print("=" * left_pos + " " * (left_index - left_pos) + ">", end="")
         print(" {0}%\t[{1}]".format(perc, detail_amount), end="")
-        print(CLI_CONFIG.color_reset_all)
+        print(CLI_CONFIG.style_reset_all)
 
         return True, perc
 
index 7bbda91da1f30728c936001d1696bf5179c2e173..15a636b1624d376c1e52dad8a65f492d8900eef3 100644 (file)
@@ -33,7 +33,7 @@ def orm_epley(weight: float, reps: float) -> dict:
         return round(_un_rounded_result, 1)
 
     def weight_max_reps(target_reps: float) -> float:
-        _un_rounded_result = one_rm() / (1 + (target_reps - 1) / 30)
+        _un_rounded_result = one_rm() * 30 / (29 + target_reps)
         return round(_un_rounded_result, 1)
 
     maxes = {n_reps: weight_max_reps(n_reps) for n_reps in common_n_reps}
@@ -43,21 +43,25 @@ def orm_epley(weight: float, reps: float) -> dict:
 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)
+        for n_reps: (1, 2, 3, 5, 6, 8, 10, 12, 15, 20)
 
     1 RM = weight * 36 / (37 - reps)
 
+    NOTE: Adjusted formula is below, with quadratic term.
+
+    1 RM = weight * 36 / (37 - reps + 0.005 * reps^2)
+
     Source: https://workoutable.com/one-rep-max-calculator/
     """
 
     def _one_rm() -> float:
-        _un_rounded_result = weight * 36 / (37 - reps)
+        _un_rounded_result = weight * 36 / (37 - reps + 0.005 * reps**2)
         return round(_un_rounded_result, 1)
 
     one_rm = _one_rm()
 
     def weight_max_reps(target_reps: float) -> float:
-        _un_rounded_result = one_rm * (37 - target_reps) / 36
+        _un_rounded_result = one_rm * (37 - target_reps + 0.005 * target_reps**2) / 36
         return round(_un_rounded_result, 1)
 
     maxes = {n_reps: weight_max_reps(n_reps) for n_reps in common_n_reps}
@@ -67,7 +71,7 @@ def orm_brzycki(weight: float, reps: float) -> dict:
 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)
+        for n_reps: (1, 2, 3, 5, 6, 8, 10, 12, 15, 20)
 
     Or an {"errMsg": "INVALID_RANGE", ...}
 
@@ -79,12 +83,23 @@ def orm_dos_remedios(weight: float, reps: int) -> dict:
         1: 1,
         2: 0.92,
         3: 0.9,
+        4: 0.89,  # NOTE: I added this
         5: 0.87,
         6: 0.82,
+        7: 0.781,  # NOTE: I added this
         8: 0.75,
+        9: 0.72375,  # NOTE: I added this
         10: 0.7,
+        11: 0.674286,  # NOTE: I added this
         12: 0.65,
+        13: 0.628571,  # NOTE: I added this
+        14: 0.611429,  # NOTE: I added this
         15: 0.6,
+        16: 0.588,  # NOTE: I added this
+        17: 0.5775,  # NOTE: I added this
+        18: 0.568,  # NOTE: I added this
+        19: 0.559,  # NOTE: I added this
+        20: 0.55,  # NOTE: I added this, 20 reps is NOT in the original equation.
     }
 
     def _one_rm() -> float:
@@ -92,17 +107,11 @@ def orm_dos_remedios(weight: float, reps: int) -> dict:
         _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),
-        }
+    # Compute the 1-rep max
+    one_rm = _one_rm()
 
     def max_weight(target_reps: int) -> float:
+        """Used to calculate max weight based on actual reps, e.g. 5 or 12"""
         _multiplier = _common_n_reps[target_reps]
         _un_rounded_result = one_rm * _multiplier
         return round(_un_rounded_result, 1)
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ce9a81fa69e35a02b8d39b0c26be110974af5a9b 100644 (file)
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Mon Aug  8 14:35:43 2022
+
+@author: shane
+
+Allows the safe avoidance of ImportError on non-colorama capable systems.
+"""
+
+try:
+    from colorama import Fore, Style
+    from colorama import init as colorama_init
+
+    # Made it this far, so run the init function (which is needed on Windows)
+    colorama_init()
+
+    # Styles
+    STYLE_BRIGHT = Style.BRIGHT
+    STYLE_DIM = Style.DIM
+    STYLE_RESET_ALL = Style.RESET_ALL
+
+    # Colors
+    COLOR_WARN = Fore.YELLOW
+    COLOR_CRIT = Style.DIM + Fore.RED
+    COLOR_OVER = Style.DIM + Fore.MAGENTA
+
+    COLOR_DEFAULT = Fore.CYAN
+
+    # Used in macro bars
+    COLOR_YELLOW = Fore.YELLOW
+    COLOR_BLUE = Fore.BLUE
+    COLOR_RED = Fore.RED
+
+    # Used by `tree.py` utility
+    COLOR_GREEN = Fore.GREEN
+    COLOR_CYAN = Fore.CYAN
+
+except ImportError:
+    # These will all just be empty strings if colorama isn't installed
+
+    # Styles
+    STYLE_BRIGHT = str()
+    STYLE_DIM = str()
+    STYLE_RESET_ALL = str()
+
+    # Colors
+    COLOR_WARN = str()
+    COLOR_CRIT = str()
+    COLOR_OVER = str()
+
+    COLOR_DEFAULT = str()
+
+    COLOR_YELLOW = str()
+    COLOR_BLUE = str()
+    COLOR_RED = str()
+
+    COLOR_GREEN = str()
+    COLOR_CYAN = str()
index 0bc840218549253a6b2277022c4e31026da423c8..b994090e8d3f0aa86fa6b2c86e6de229ff1dc8e9 100644 (file)
@@ -3,14 +3,7 @@
 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
+from ntclient.utils import colors
 
 chars = {"nw": "\u2514", "nws": "\u251c", "ew": "\u2500", "ns": "\u2502"}
 
@@ -21,17 +14,11 @@ strs = [
     "    ",
 ]
 
-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()
+# Colors and termination strings
+COLOR_DIR = colors.STYLE_BRIGHT + colors.COLOR_BLUE
+COLOR_EXEC = colors.STYLE_BRIGHT + colors.COLOR_GREEN
+COLOR_LINK = colors.STYLE_BRIGHT + colors.COLOR_CYAN
+COLOR_DEAD_LINK = colors.STYLE_BRIGHT + colors.COLOR_RED
 
 
 def colorize(path: str, full: bool = False) -> str:
@@ -43,17 +30,17 @@ def colorize(path: str, full: bool = False) -> str:
             [
                 COLOR_LINK,
                 file,
-                Style.RESET_ALL,
+                colors.STYLE_RESET_ALL,
                 " -> ",
                 colorize(os.readlink(path), full=True),
             ]
         )
 
     if os.path.isdir(path):
-        return "".join([COLOR_DIR, file, Style.RESET_ALL])
+        return "".join([COLOR_DIR, file, colors.STYLE_RESET_ALL])
 
     if os.access(path, os.X_OK):
-        return "".join([COLOR_EXEC, file, Style.RESET_ALL])
+        return "".join([COLOR_EXEC, file, colors.STYLE_RESET_ALL])
 
     return file
 
@@ -76,7 +63,7 @@ def print_dir(_dir: str, pre: str = str()) -> tuple:
     n_size = 0
 
     if not pre:
-        print(COLOR_DIR + _dir + Style.RESET_ALL)
+        print(COLOR_DIR + _dir + colors.STYLE_RESET_ALL)
 
     dir_len = len(os.listdir(_dir)) - 1
     for i, file in enumerate(sorted(os.listdir(_dir), key=str.lower)):
index 299258ecb5d9977fabbcecb75e6f934a13124a2c..ec3367b4cd113b895840914289bd2c394c72cc2b 100644 (file)
@@ -14,34 +14,15 @@ import ntclient.services.calculate as calc
 @pytest.mark.parametrize("_eq", ["epley", "brzycki", "dos_remedios"])
 @pytest.mark.parametrize(
     "weight,reps",
-    [
-        (50.0, 1),
-        (50.0, 2),
-        (50.0, 3),
-        (50.0, 5),
-        (50.0, 6),
-        (50.0, 8),
-        (50.0, 10),
-        (50.0, 12),
-        (50.0, 15),
-        (50.0, 20),
-    ],
+    [(50.0, x) for x in (1, 2, 3, 5, 6, 8, 10, 12, 15, 20)],
 )
 def test_000_orm_same_in_same_out(_eq: str, weight: float, reps: int) -> None:
     """Test one rep max: Epley"""
-    if _eq == "epley":
-        result = calc.orm_epley(weight, reps)
-
-    elif _eq == "brzycki":
-        result = calc.orm_brzycki(weight, reps)
-
-    else:  # _eq == "dos_remedios"
-        result = calc.orm_dos_remedios(weight, reps)
-
-    try:
-        # Check results
-        assert result[reps] == weight
-    except KeyError:
-        # dose Remedios does not work for 20 reps currently
-        assert _eq == "dos_remedios"
-        assert reps == 20
+    result = {
+        "epley": calc.orm_epley(weight, reps),
+        "brzycki": calc.orm_brzycki(weight, reps),
+        "dos_remedios": calc.orm_dos_remedios(weight, reps),
+    }
+
+    # Check results
+    assert result[_eq][reps] == weight
index 887203fa7d55b4ad2665627539a63e4f101b7065..325b2e8bccd7e6984cfa821c23f06ffbf4f000fb 100644 (file)
@@ -209,12 +209,15 @@ class TestCli(unittest.TestCase):
         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"])
+        # Reps > 20 (or reps < 1)
+        args = arg_parser.parse_args(args=["calc", "1rm", "225", "25"])
         code, result = args.func(args)
-        assert code == 0
-        assert set(result.keys()) == {"epley", "brzycki", "dos_remedios"}
-        assert "errMsg" in result["dos_remedios"]
+        assert code == 1
+
+        # Weight < 0
+        args = arg_parser.parse_args(args=["calc", "1rm", "-10", "10"])
+        code, result = args.func(args)
+        assert code == 1
 
         # BMR
         # -----------------------------------