From: Shane Jaroch Date: Sun, 10 Jul 2022 22:11:51 +0000 (-0400) Subject: initial commit X-Git-Tag: v0.2.3~6 X-Git-Url: https://git.nutra.tk/v2?a=commitdiff_plain;h=de5f764c0c01df999a8ff21a2b7fea53539d1d84;p=nutratech%2Fcli.git initial commit --- de5f764c0c01df999a8ff21a2b7fea53539d1d84 diff --git a/.banditrc b/.banditrc new file mode 100644 index 0000000..f5fba94 --- /dev/null +++ b/.banditrc @@ -0,0 +1,3 @@ +assert_used: # B101 + skips: ["*/test_*.py"] + diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d9f1e38 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,36 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true + +max_line_length = 100 + + +[{nutra,*.py}] +max_line_length = 88 + + +[*.{yaml,yml}] +indent_size = 2 + + +[Makefile] +indent_style = tab +max_line_length = 120 + + +[*.md] +max_line_length = 90 +trim_trailing_whitespace = false + + +[*.rst] +max_line_length = 79 + + +[{COMMIT_EDITMSG,MERGE_MSG,SQUASH_MSG,git-rebase-todo}] +max_line_length = 72 diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..1c21d04 --- /dev/null +++ b/.envrc @@ -0,0 +1,2 @@ +source .venv/bin/activate +unset PS1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..4091385 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,33 @@ +--- +name: CI +"on": + push: {} + +jobs: + test: + 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 + # NOTE: container lacks OS dist for testresources/launchpadlib + run: | + pip install testresources==2.0.1 + make _deps + + - name: Lint + run: make _lint + + - name: Test + run: make _test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c99d0b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,128 @@ +# macOS/backup files +.~* +._* +.DS_Store +*.swp + +# Our files +.vscode/ +.idea/ +__sha__.py +*.sqlite + +#################### +## Python Ignores ## +#################### + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ +Pipfile* + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..37de861 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "ntclient/ntsqlite"] + path = ntclient/ntsqlite + url = https://github.com/nutratech/nt-sqlite.git diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..e7345a8 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,10 @@ +[MASTER] + +fail-under=9.5 + + +[MESSAGES CONTROL] + +disable= + fixme, + consider-using-f-string, diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 0000000..757ef82 --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,44 @@ +--- +rules: + braces: + min-spaces-inside: 0 + max-spaces-inside: 1 + brackets: + min-spaces-inside: 0 + max-spaces-inside: 0 + colons: + max-spaces-before: 0 + max-spaces-after: 1 + commas: + max-spaces-before: 0 + min-spaces-after: 1 + max-spaces-after: 1 + comments: + level: warning + require-starting-space: yes + min-spaces-from-content: 1 + comments-indentation: + level: warning + document-end: disable + document-start: + level: warning + present: yes + empty-lines: + max: 2 + max-start: 0 + max-end: 0 + hyphens: + max-spaces-after: 1 + indentation: + spaces: 2 + indent-sequences: yes + check-multi-line-strings: no + key-duplicates: {} + line-length: + level: warning + max: 80 + allow-non-breakable-words: yes + new-line-at-end-of-file: { level: error } + new-lines: + type: unix + trailing-spaces: {} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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. + + + Copyright (C) + + 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 . + +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: + + Copyright (C) + 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 +. + + 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 +. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..da653db --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,14 @@ +include requirements.txt +include requirements-*.txt + +include ntclient/LICENSE +include ntclient/CHANGELOG.md + +include ntclient/ntsqlite/LICENSE +include ntclient/ntsqlite/README.rst +include ntclient/ntsqlite/CHANGELOG.md +graft ntclient/ntsqlite/sql/ + +global-exclude *.sqlite3 + +global-exclude __pycache__/* diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f414d91 --- /dev/null +++ b/Makefile @@ -0,0 +1,155 @@ +SHELL=/bin/bash + +.DEFAULT_GOAL := _help + +# NOTE: must put a character and two pound "\t##" to show up in this list. Keep it brief! IGNORE_ME +.PHONY: _help +_help: + @grep -h "##" $(MAKEFILE_LIST) | grep -v IGNORE_ME | sed -e 's/##//' | column -t -s $$'\t' + + + +# --------------------------------------- +# init & venv +# --------------------------------------- +.PHONY: init +init: ## Set up a Python virtual environment + git submodule update --init + if [ ! -d .venv ]; then \ + /usr/bin/python3 -m venv .venv; \ + fi + - direnv allow + @echo -e "\r\nNOTE: activate venv, and run 'make deps'\r\n" + @echo -e "HINT: run 'source .venv/bin/activate'" + + +PYTHON ?= $(shell which python) +PWD ?= $(shell pwd) +.PHONY: _venv +_venv: + # Test to enforce venv usage across important make targets + [ "$(PYTHON)" = "$(PWD)/.venv/bin/python" ] || [ "$(PYTHON)" = "$(PWD)/.venv/Scripts/python" ] + + +# --------------------------------------- +# Install requirements +# --------------------------------------- + +PIP := python -m pip +REQ_OPT := requirements-optional.txt +REQ_LINT := requirements-lint.txt +REQ_TEST := tests/requirements.txt +REQ_OLD := tests/requirements-win_xp-ubu1604.txt +.PHONY: _deps +_deps: + $(PIP) install wheel + $(PIP) install -r requirements.txt + - $(PIP) install -r $(REQ_OPT) + - $(PIP) install -r $(REQ_LINT) + - $(PIP) install -r $(REQ_TEST) || (echo "\r\nTEST REQs failed... try old version" && $(PIP) install -r $(REQ_OLD)) + +.PHONY: deps +deps: _venv _deps ## Install requirements + + +# --------------------------------------- +# Format, lint, test +# --------------------------------------- + +.PHONY: format +format: + isort $(LINT_LOCS) + autopep8 --recursive --in-place --max-line-length 88 $(LINT_LOCS) + black $(LINT_LOCS) + + +LINT_LOCS := ntclient/ tests/ scripts/ nutra setup.py +YAML_LOCS := ntclient/ntsqlite/.*.yml .github/workflows/ .*.yml +# NOTE: yamllint ntclient/ntsqlite/.travis.yml ? (submodule) +# NOTE: doc8 ntclient/ntsqlite/README.rst ? (submodule) +.PHONY: _lint +_lint: + # check formatting: Python + pycodestyle --max-line-length=99 --statistics $(LINT_LOCS) + autopep8 --recursive --diff --max-line-length 88 --exit-code $(LINT_LOCS) + isort --diff --check $(LINT_LOCS) + black --check $(LINT_LOCS) + # lint RST (last param is search term, NOT ignore) + doc8 --quiet *.rst ntclient/ntsqlite/*.rst + # lint YAML + yamllint $(YAML_LOCS) + # lint Python + bandit -q -c .banditrc -r $(LINT_LOCS) + mypy $(LINT_LOCS) + flake8 $(LINT_LOCS) + pylint $(LINT_LOCS) + +.PHONY: lint +lint: _venv _lint ## Lint code and documentation + + +TEST_HOME := tests/ +MIN_COV := 80 +.PHONY: _test +_test: + coverage run -m pytest -v -s -p no:cacheprovider -o log_cli=true $(TEST_HOME) + coverage report + +.PHONY: test +test: _venv _test ## Run CLI unittests + + +# --------------------------------------- +# SQLite submodule: nt-sqlite +# --------------------------------------- + +# TODO: why does this still work? Is this what `ntserv.ntdb.sql` should do? + +.PHONY: ntsqlite/build +ntsqlite/build: + python ntclient/ntsqlite/sql/__init__.py + +# TODO: nt-sqlite/test + + +# --------------------------------------- +# Python build stuff +# --------------------------------------- + +.PHONY: _build +_build: + python setup.py --quiet sdist + +.PHONY: build +build: _venv _build clean ## Create sdist binary *.tar.gz + +.PHONY: _install +_install: + python -m pip install wheel + python -m pip install . + python -m pip show nutra + - python -c 'import shutil; print(shutil.which("nutra"));' + nutra -v + +.PHONY: install +install: _venv _install ## pip install nutra + +.PHONY: _uninstall +_uninstall: + python -m pip uninstall -y nutra + +.PHONY: uninstall +uninstall: _venv _uninstall ## pip uninstall nutra + + +# --------------------------------------- +# Clean +# --------------------------------------- + +.PHONY: clean +clean: ## Clean up __pycache__ and leftover bits + rm -f .coverage ntclient/ntsqlite/sql/nt.sqlite3 + 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 diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..47f0390 --- /dev/null +++ b/README.rst @@ -0,0 +1,171 @@ +************** + nutratracker +************** + +.. image:: https://badgen.net/pypi/v/nutra + :target: https://pypi.org/project/nutra/ + :alt: Latest version unknown| +.. image:: https://github.com/nutratech/cli/actions/workflows/test.yml/badge.svg + :target: https://github.com/nutratech/cli/actions/workflows/test.yml + :alt: Build status unknown| +.. image:: https://pepy.tech/badge/nutra/month + :target: https://pepy.tech/project/nutra + :alt: Monthly downloads unknown| +.. image:: https://img.shields.io/pypi/pyversions/nutra.svg + :alt: Python3 (3.4 - 3.10)| +.. image:: https://badgen.net/badge/code%20style/black/000 + :target: https://github.com/ambv/black + :alt: Code style: black| +.. image:: https://badgen.net/pypi/license/nutra + :target: https://www.gnu.org/licenses/gpl-3.0.en.html + :alt: License GPL-3 + +Extensible command-line tools for nutrient analysis. + +*Requires:* + +- Python 3.4.0 or later (lzma, ssl & sqlite3 modules) [Win XP / Ubuntu 16.04] +- Packages: see ``setup.py``, ``requirements.txt``, and ``config`` folder +- 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 + +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. + +Linux may need to install ``python-dev`` package to build +``python-Levenshtein``. + +Windows users may not be able to install ``python-Levenshtein``. + +Mac and Linux developers will do well to install ``direnv``. + +Main program works 100%, but test and lint may not on older operating +systems (Ubuntu 16.04, Windows XP). + +Install PyPi release (from pip) +=============================== + +.. code-block:: bash + + pip install 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 + + git clone https://github.com/nutratech/cli.git + cd cli + make init + # source .venv/bin/activate # uncomment if NOT using direnv + make deps + + ./nutra -h + +Initialize the DBs (nt and usda). + +.. code-block:: bash + + # source .venv/bin/activate # uncomment if NOT using direnv + ./nutra init + + # Or install and run as package script + make install + nutra init + +If installed (or inside ``cli``) folder, the program can also run +with ``python -m ntclient`` + +Building the PyPi release +######################### + +.. code-block:: bash + + # source .venv/bin/activate # uncomment if NOT using direnv + 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: + +.. code-block:: bash + + # source .venv/bin/activate # uncomment if NOT using direnv + make format lint test + +Argcomplete (tab completion on Linux/macOS) +=========================================== + +After installing nutra, argcomplete package should also be installed, + +Simply run the following out of a ``bash`` shell: + +.. code-block:: bash + + activate-global-python-argcomplete + +Then you can press tab to fill in or complete subcommands +and to list argument flags. + +**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] `` + + +Commands +######## + +:: + + usage: nutra [-h] [-v] [-d] [--no-pager] + {init,nt,search,sort,anl,day,recipe,bio} ... + + 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,bio} + 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 + bio view, add, remove biometric logs diff --git a/ntclient/CHANGELOG.md b/ntclient/CHANGELOG.md new file mode 100644 index 0000000..c690c1d --- /dev/null +++ b/ntclient/CHANGELOG.md @@ -0,0 +1,67 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Download cache & checksum verification +- Basic functionality of `import` and `export` subcommands +- `[DEVELOPMENT]` Added `Makefile` with easy commands for `init`, `lint`, `test`, etc + +## [0.2.2] - 2022-04-08 + +### Added + +- Limit search & sort results to top `n` results (e.g. top 10 or top 100) +- Enhanced terminal sizing (buffer termination) +- Pydoc PAGING flag via `--no-pager` command line arg (with `set_flags()` method) +- Check for appropriate `ntsqlite` database version +- `[DEVELOPMENT]` Special `file_or_dir_path` and `file_path` custom type validators + for argparse +- `[DEVELOPMENT]` Added special requirements files for + (`test`, `lint`, `optional` [Levenshtein], and `win_xp-test` [Python 3.4]) +- `[DEVELOPMENT]` Added `CHANGELOG.md` file + +### Changed + +- Print `exit_code` in DEBUG mode (`--debug` flag/arg) +- Moved `subparsers` module in `ntclient.argparser` to `__init__` +- Moved tests out of `ntclient/` and into `tests/` folder + +## [0.2.1] - 2021-05-30 + +### Added + +- Python 3.4.3 support (Windows XP and Ubuntu 16.04) +- Debug flag (`--debug | -d`) for all commands + +### Changed + +- Overall structure with main file and argparse methods +- Use soft pip requirements `~=` instead of `==` +- `DEFAULT` and `OVER` colors + +### Removed + +- guid columns from `ntsqlite` submodule + +## [0.2.0] - 2021-05-21 + +### Added + +- SQLite support for `usda` and `nt` schemas (removed API calls to remote server) +- Preliminary support for `recipe` and `bio` subcommands +- On-boarding process with `init` subcommand +- Support for `argcomplete` on `bash` (Linux/macOS) +- Tests + +## [0.0.38] - 2020-08-01 + +### Added + +- Support for analysis of day CSV files diff --git a/ntclient/LICENSE b/ntclient/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/ntclient/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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. + + + Copyright (C) + + 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 . + +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: + + Copyright (C) + 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 +. + + 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 +. diff --git a/ntclient/__init__.py b/ntclient/__init__.py new file mode 100755 index 0000000..b2cb6cc --- /dev/null +++ b/ntclient/__init__.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +""" +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 + +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 . +""" + +import argparse +import os +import platform +import shutil +import sys + +from ntclient.ntsqlite.sql import NT_DB_NAME + +# Package info +__title__ = "nutra" +__version__ = "0.2.3.dev0" +__author__ = "Shane Jaroch" +__email__ = "nutratracker@gmail.com" +__license__ = "GPL v3" +__copyright__ = "Copyright 2018-2022 Shane Jaroch" +__url__ = "https://github.com/nutratech/cli" + +# Sqlite target versions +__db_target_nt__ = "0.0.4" +__db_target_usda__ = "0.0.8" + +# Global variables +ROOT_DIR = os.path.abspath(os.path.dirname(__file__)) +NUTRA_DIR = os.path.join(os.path.expanduser("~"), ".nutra") +USDA_DB_NAME = "usda.sqlite" +# NT_DB_NAME = "nt.sqlite" # defined in ntclient.ntsqlite.sql +DEBUG = False +PAGING = True + +NTSQLITE_BUILDPATH = os.path.join(ROOT_DIR, "ntsqlite", "sql", NT_DB_NAME) +NTSQLITE_DESTINATION = os.path.join(NUTRA_DIR, NT_DB_NAME) + +# Check Python version +PY_MIN_VER = (3, 4, 0) +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: nutra requires Python %s or later to run" % PY_MIN_STR) + print("HINT: You're running Python %s" % PY_SYS_STR) + sys.exit(1) + +# Buffer truncation +BUFFER_WD = shutil.get_terminal_size()[0] +BUFFER_HT = shutil.get_terminal_size()[1] + +DEFAULT_RESULT_LIMIT = BUFFER_HT - 4 + +DEFAULT_DAY_H_BUFFER = BUFFER_WD - 4 if BUFFER_WD > 12 else 8 + +DECREMENT = 1 if platform.system() == "Windows" else 0 +DEFAULT_SORT_H_BUFFER = ( + BUFFER_WD - (38 + DECREMENT) if BUFFER_WD > 50 else (12 - DECREMENT) +) +DEFAULT_SEARCH_H_BUFFER = ( + BUFFER_WD - (50 + DECREMENT) if BUFFER_WD > 70 else (20 - DECREMENT) +) + + +def set_flags(args: argparse.Namespace) -> None: + """ + Sets + DEBUG flag + PAGING flag + from main (after arg parse). Accessible throughout package + """ + global DEBUG, PAGING # pylint: disable=global-statement + DEBUG = args.debug + PAGING = not args.no_paging + + if DEBUG: + print("Console size: %sh x %sw" % (BUFFER_HT, BUFFER_WD)) + + +# TODO: +# nested nutrient tree, like: http://www.whfoods.com/genpage.php?tname=nutrientprofile&dbid=132 +# attempt to record errors in failed try/catch block (bottom of __main__.py) +# make use of argcomplete.warn(msg) ? diff --git a/ntclient/__main__.py b/ntclient/__main__.py new file mode 100644 index 0000000..e500e9c --- /dev/null +++ b/ntclient/__main__.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- +""" +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 + +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 . +""" +import argparse +import sys +import time +from typing import Sequence +from urllib.error import HTTPError, URLError + +import argcomplete +from colorama import init as colorama_init + +from ntclient import ( + __db_target_nt__, + __db_target_usda__, + __title__, + __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: + """Adds all subparsers and parsing logic""" + + arg_parser = argparse.ArgumentParser(prog=__title__) + arg_parser.add_argument( + "-v", + "--version", + action="version", + version="{0} cli version {1} ".format(__title__, __version__) + + "[DB usda v{0}, nt v{1}]".format(__db_target_usda__, __db_target_nt__), + ) + + arg_parser.add_argument( + "-d", "--debug", action="store_true", help="enable detailed error messages" + ) + arg_parser.add_argument( + "--no-pager", + dest="no_paging", + action="store_true", + help="disable paging (print full output)", + ) + + # Subparsers + subparsers = arg_parser.add_subparsers(title="%s subcommands" % __title__) + build_subcommands(subparsers) + + return arg_parser + + +def main(args: Sequence[str] = None) -> int: + """Main method for CLI""" + + start_time = time.time() + arg_parser = build_argparser() + argcomplete.autocomplete(arg_parser) + + def parse_args() -> argparse.Namespace: + """Returns parsed args""" + if args is None: + return arg_parser.parse_args() + return arg_parser.parse_args(args=args) + + 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() + + args_dict = dict(vars(parser)) + for expected_arg in ["func", "debug", "no_paging"]: + args_dict.pop(expected_arg) + + # Run function + if args_dict: + return parser.func(args=parser) + return parser.func() + + # Otherwise print help + arg_parser.print_help() + return 1, None + + # Build the parser, set flags + _parser = parse_args() + set_flags(_parser) + from ntclient import DEBUG # pylint: disable=import-outside-toplevel + + # TODO: bug reporting? + # Try to run the function + exit_code = None + 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: + 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: + raise + except URLError as url_error: + print("Connection error, check your internet: " + repr(url_error.reason)) + if DEBUG: + raise + except Exception as exception: # pylint: disable=broad-except + print("There was an unforeseen error: " + repr(exception)) + if DEBUG: + raise + finally: + if 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()) diff --git a/ntclient/argparser/__init__.py b/ntclient/argparser/__init__.py new file mode 100644 index 0000000..1e62671 --- /dev/null +++ b/ntclient/argparser/__init__.py @@ -0,0 +1,176 @@ +"""Main module for things related to argparse""" + +from ntclient.argparser import funcs as parser_funcs +from ntclient.argparser import types + + +def build_subcommands(subparsers) -> None: + """Attaches subcommands to main parser""" + build_init_subcommand(subparsers) + build_nt_subcommand(subparsers) + build_search_subcommand(subparsers) + build_sort_subcommand(subparsers) + build_analyze_subcommand(subparsers) + build_day_subcommand(subparsers) + build_recipe_subcommand(subparsers) + build_biometric_subcommand(subparsers) + + +################################################################################ +# Methods to build subparsers, and attach back to main arg_parser +################################################################################ +def build_init_subcommand(subparsers) -> None: + """Self running init command""" + init_parser = subparsers.add_parser( + "init", help="setup profiles, USDA and NT database" + ) + init_parser.add_argument( + "-y", + dest="yes", + action="store_true", + help="automatically agree to (potentially slow) USDA download", + ) + init_parser.set_defaults(func=parser_funcs.init) + + +def build_nt_subcommand(subparsers) -> 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) + + +def build_search_subcommand(subparsers) -> None: + """Search: terms [terms ... ]""" + search_parser = subparsers.add_parser( + "search", help="search foods by name, list overview info" + ) + search_parser.add_argument( + "terms", + nargs="+", + help='search query, e.g. "grass fed beef" or "ultraviolet mushrooms"', + ) + search_parser.add_argument( + "-t", + dest="top", + metavar="N", + type=int, + help="show top N results (defaults to console height)", + ) + search_parser.add_argument( + "-g", + dest="fdgrp_id", + type=int, + help="filter by a specific food group ID", + ) + search_parser.set_defaults(func=parser_funcs.search) + + +def build_sort_subcommand(subparsers) -> 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", + action="store_true", + help="sort by value per 200 kcal, instead of per 100 g", + ) + sort_parser.add_argument( + "-t", + dest="top", + metavar="N", + type=int, + help="show top N results (defaults to console height)", + ) + sort_parser.add_argument("nutr_id", type=int) + sort_parser.set_defaults(func=parser_funcs.sort) + + +def build_analyze_subcommand(subparsers) -> None: + """Analyzes (foods only for now)""" + analyze_parser = subparsers.add_parser("anl", help="analyze food(s)") + analyze_parser.add_argument( + "-g", + dest="grams", + type=float, + help="scale to custom number of grams (default is 100g)", + ) + analyze_parser.add_argument("food_id", type=int, nargs="+") + analyze_parser.set_defaults(func=parser_funcs.analyze) + + +def build_day_subcommand(subparsers) -> 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.add_argument( + "food_log", + metavar="food_log.csv", + nargs="+", + type=types.file_or_dir_path, + help="path to CSV file of food log", + ) + day_parser.add_argument( + "-r", + dest="rda", + metavar="rda.csv", + type=types.file_path, + help="provide a custom RDA file in csv format", + ) + day_parser.set_defaults(func=parser_funcs.day) + + +def build_recipe_subcommand(subparsers) -> None: + """View, add, edit, delete recipes""" + recipe_parser = subparsers.add_parser("recipe", help="list and analyze recipes") + recipe_subparsers = recipe_parser.add_subparsers(title="recipe subcommands") + + 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" + ) + 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)", + ) + recipe_import_parser.set_defaults(func=parser_funcs.recipe_import) + + # TODO: edit.. support renaming, and overwriting/re-importing food_amts (from CSV) + + recipe_delete_parser = recipe_subparsers.add_parser( + "delete", help="delete a recipe(s) by ID or range" + ) + recipe_delete_parser.add_argument("recipe_id", type=int, help="delete recipe by ID") + recipe_delete_parser.set_defaults(func=parser_funcs.recipe_delete) + + recipe_parser.set_defaults(func=parser_funcs.recipes) + + +def build_biometric_subcommand(subparsers) -> None: + """View biometrics, and view, add, edit, delete log entries""" + bio_parser = subparsers.add_parser("bio", help="view, add, remove biometric logs") + bio_subparsers = bio_parser.add_subparsers(title="biometric subcommands") + + bio_log_parser = bio_subparsers.add_parser("log", help="manage biometric logs") + bio_log_subparsers = bio_log_parser.add_subparsers( + title="biometric log subcommands" + ) + bio_log_parser.set_defaults(func=parser_funcs.bio_log) + + bio_log_add_parser = bio_log_subparsers.add_parser( + "add", help="add a biometric log" + ) + bio_log_add_parser.add_argument( + "biometric_val", help="id,value pairs, e.g. 22,59 23,110 24,65 ", nargs="+" + ) + bio_log_add_parser.set_defaults(func=parser_funcs.bio_log_add) + + bio_parser.set_defaults(func=parser_funcs.bio) diff --git a/ntclient/argparser/funcs.py b/ntclient/argparser/funcs.py new file mode 100644 index 0000000..d1a8605 --- /dev/null +++ b/ntclient/argparser/funcs.py @@ -0,0 +1,99 @@ +"""Current home to subparsers and service-level logic""" +import os + +from ntclient import services + + +def init(args): + """Wrapper init method for persistence stuff""" + return services.init(yes=args.yes) + + +################################################################################ +# Nutrients, search and sort +################################################################################ +def nutrients(): + """List nutrients""" + return services.usda.list_nutrients() + + +def search(args): + """Searches all dbs, foods, recipes, recents and favorites.""" + if args.top: + return 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) + + +def sort(args): + """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) + + +################################################################################ +# Analysis and Day scoring +################################################################################ +def analyze(args): + """Analyze a food""" + food_ids = args.food_id + grams = args.grams + + return services.analyze.foods_analyze(food_ids, grams) + + +def day(args): + """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 + + return services.analyze.day_analyze(day_csv_paths, rda_csv_path=rda_csv_path) + + +################################################################################ +# Biometrics +################################################################################ +def bio(): + """List biometrics""" + return services.biometrics.biometrics() + + +def bio_log(): + """List biometric logs""" + return services.biometrics.biometric_logs() + + +def bio_log_add(args): + """Add a biometric log entry""" + bio_vals = { + int(x.split(",")[0]): float(x.split(",")[1]) for x in args.biometric_val + } + + return services.biometrics.biometric_add(bio_vals) + + +################################################################################ +# Recipes +################################################################################ +def recipes(): + """Return recipes""" + return services.recipe.recipes_overview() + + +def recipe(args): + """Return recipe view (analysis)""" + return services.recipe.recipe_overview(args.recipe_id) + + +def recipe_import(args): + """Add a recipe""" + # TODO: custom serving sizes, not always in grams? + return services.recipe.recipe_import(args.path) + + +def recipe_delete(args): + """Delete a recipe""" + return services.recipe.recipe_delete(args.recipe_id) diff --git a/ntclient/argparser/types.py b/ntclient/argparser/types.py new file mode 100644 index 0000000..1943cf7 --- /dev/null +++ b/ntclient/argparser/types.py @@ -0,0 +1,17 @@ +"""Custom types for argparse validation""" +import argparse +import os + + +def file_path(string): + """Returns file if it exists, else raises argparse error""" + if os.path.isfile(string): + return string + raise argparse.ArgumentTypeError('FileNotFoundError: "%s"' % string) + + +def file_or_dir_path(string): + """Returns path if it exists, else raises argparse error""" + if os.path.exists(string): + return string + raise argparse.ArgumentTypeError('FileNotFoundError: "%s"' % string) diff --git a/ntclient/core/__init__.py b/ntclient/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ntclient/core/nnest.py b/ntclient/core/nnest.py new file mode 100755 index 0000000..c22d7c7 --- /dev/null +++ b/ntclient/core/nnest.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Sat Aug 29 19:43:55 2020 + +@author: shane +""" + +nnest = { + "basics": ["Protein", "Carbs", "Fats", "Fiber", "Calories"], + "macro_details": {"Carbs": {}, "Fat": {}}, + "micro_nutrients": { + "Vitamins": {"Water-Soluble": {}, "Fat-Soluble": {}}, + "Minerals": [], + }, + "fatty_acids": {}, + "amino_acids": set(), + "other_components": {}, +} diff --git a/ntclient/core/nnr2.py b/ntclient/core/nnr2.py new file mode 100755 index 0000000..dd94458 --- /dev/null +++ b/ntclient/core/nnr2.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Fri Jul 31 21:23:51 2020 + +@author: shane +""" + +# NOTE: based on diff --git a/ntclient/core/nutprogbar.py b/ntclient/core/nutprogbar.py new file mode 100644 index 0000000..38aa10d --- /dev/null +++ b/ntclient/core/nutprogbar.py @@ -0,0 +1,38 @@ +"""Temporary [wip] module for more visual (& colorful) RDA output""" + + +def nutprogbar(food_amts, food_analyses, nutrients): + """Returns progress bars, colorized, for foods analyses""" + + def tally(): + for nut in nut_percs: + # TODO: get RDA values from nt DB, tree node nested organization + print(nut) + + food_analyses = { + x[0]: {y[1]: y[2] for y in food_analyses if y[0] == x[0]} for x in food_analyses + } + + # print(food_ids) + # print(food_analyses) + + nut_amts = {} + + for food_id, grams in food_amts.items(): + # r = grams / 100.0 + analysis = food_analyses[food_id] + for nutrient_id, amt in analysis.items(): + if nutrient_id not in nut_amts: + nut_amts[nutrient_id] = amt + else: + nut_amts[nutrient_id] += amt + + nut_percs = {} + + for nutrient_id, amt in nut_amts.items(): + # TODO: if not rda, show raw amounts? + if isinstance(nutrients[nutrient_id][1], float): + nut_percs[nutrient_id] = round(amt / nutrients[nutrient_id][1], 3) + + tally() + return nut_percs diff --git a/ntclient/ntsqlite b/ntclient/ntsqlite new file mode 160000 index 0000000..a245574 --- /dev/null +++ b/ntclient/ntsqlite @@ -0,0 +1 @@ +Subproject commit a2455748d628963df01afeecd2b058b2cdd8344a diff --git a/ntclient/persistence/__init__.py b/ntclient/persistence/__init__.py new file mode 100644 index 0000000..014dcc3 --- /dev/null +++ b/ntclient/persistence/__init__.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat Mar 23 13:09:07 2019 + +@author: shane +""" + +import json +import os + +from ntclient import NUTRA_DIR + +# 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_DIR, "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" + ) diff --git a/ntclient/persistence/sql/__init__.py b/ntclient/persistence/sql/__init__.py new file mode 100644 index 0000000..58d6f4e --- /dev/null +++ b/ntclient/persistence/sql/__init__.py @@ -0,0 +1,70 @@ +"""Main SQL persistence module, need to rethink circular imports and shared code""" +import sqlite3 +from typing import Union + +# FIXME: maybe just use separate methods for calls with vs. without headers +# avoid the mypy headaches, and the liberal comments # type: ignore + + +def sql_entries(sql_result: sqlite3.Cursor, headers=False) -> Union[list, tuple]: + """Formats and returns a `sql_result()` for console digestion and output""" + # TODO: return object: metadata, command, status, errors, etc? + rows = sql_result.fetchall() + + if headers: + headers = [x[0] for x in sql_result.description] + return headers, rows + + return rows + + +def version(con: sqlite3.Connection) -> str: + """Gets the latest entry from version table""" + + cur = con.cursor() + result = cur.execute("SELECT * FROM version;").fetchall() + close_con_and_cur(con, cur, commit=False) + return result[-1][1] + + +def close_con_and_cur( + con: sqlite3.Connection, cur: sqlite3.Cursor, commit=True +) -> None: + """Cleans up, commits, and closes after an SQL command is run""" + + cur.close() + if commit: + con.commit() + con.close() + + +def _sql( + con: sqlite3.Connection, + query: str, + db_name: str, + values: Union[tuple, list] = None, + headers=False, +) -> Union[list, tuple]: + from ntclient import DEBUG # pylint: disable=import-outside-toplevel + + cur = con.cursor() + + if DEBUG: + print("%s.sqlite3: %s" % (db_name, query)) + if values: + # TODO: better debug logging, more "control-findable", distinguish from most prints() + print(values) + + # TODO: separate `entry` & `entries` entity for single vs. bulk insert? + if values: + if isinstance(values, list): + rows = cur.executemany(query, values) + else: # tuple + rows = cur.execute(query, values) + else: + rows = cur.execute(query) + + # TODO: print " SELECTED", or other info BASED ON command SELECT/INSERT/DELETE/UPDATE + result = sql_entries(rows, headers=headers) + close_con_and_cur(con, cur) + return result diff --git a/ntclient/persistence/sql/nt/__init__.py b/ntclient/persistence/sql/nt/__init__.py new file mode 100644 index 0000000..8dee265 --- /dev/null +++ b/ntclient/persistence/sql/nt/__init__.py @@ -0,0 +1,41 @@ +"""Nutratracker DB specific sqlite module""" +import os +import sqlite3 + +from ntclient import NT_DB_NAME, NUTRA_DIR, __db_target_nt__ +from ntclient.persistence.sql import _sql, version +from ntclient.utils.exceptions import SqlConnectError, SqlInvalidVersionError + + +def nt_sqlite_connect(version_check=True): + """Connects to the nt.sqlite3 file, or throws an exception""" + db_path = os.path.join(NUTRA_DIR, NT_DB_NAME) + if os.path.isfile(db_path): + con = sqlite3.connect(db_path) + con.row_factory = sqlite3.Row + + # Verify version + if version_check and nt_ver() != __db_target_nt__: + raise SqlInvalidVersionError( + "ERROR: nt target [{0}] mismatch actual [{1}] ".format( + __db_target_nt__, nt_ver() + ) + + "upgrades not supported, please remove '~/.nutra/nt.sqlite3'" + "and re-run 'nutra init'" + ) + return con + + # Else it's not on disk + raise SqlConnectError("ERROR: nt database doesn't exist, please run `nutra init`") + + +def nt_ver(): + """Gets version string for nt.sqlite3 database""" + con = nt_sqlite_connect(version_check=False) + return version(con) + + +def sql(query, values=None, headers=False): + """Executes a SQL command to nt.sqlite3""" + con = nt_sqlite_connect() + return _sql(con, query, db_name="nt", values=values, headers=headers) diff --git a/ntclient/persistence/sql/nt/funcs.py b/ntclient/persistence/sql/nt/funcs.py new file mode 100644 index 0000000..2ee2d70 --- /dev/null +++ b/ntclient/persistence/sql/nt/funcs.py @@ -0,0 +1,93 @@ +"""nt.sqlite3 functions module""" +from ntclient.persistence import PROFILE_ID +from ntclient.persistence.sql.nt import nt_sqlite_connect, sql + + +def sql_nt_next_index(table=None): + """Used for previewing inserts""" + query = "SELECT MAX(id) FROM %s;" % table # nosec: B608 + return int(sql(query)[0]["MAX(id)"]) + + +################################################################################ +# Recipe functions +################################################################################ +def sql_recipe(recipe_id): + """Selects columns for recipe_id""" + query = "SELECT * FROM recipes WHERE id=?;" + return sql(query, values=(recipe_id,)) + + +def sql_recipes(): + """Show recipes with selected details""" + query = """ +SELECT + id, + tagname, + name, + COUNT(recipe_id) AS n_foods, + SUM(grams) AS grams, + created +FROM + recipes + LEFT JOIN recipe_dat ON recipe_id = id +GROUP BY + id; +""" + return sql(query, headers=True) + + +def sql_analyze_recipe(recipe_id): + """Output (nutrient) analysis columns for a given recipe_id""" + query = """ +SELECT + id, + name, + food_id, + grams +FROM + recipes + INNER JOIN recipe_dat ON recipe_id = id + AND id = ?; +""" + return sql(query, values=(recipe_id,)) + + +def sql_recipe_add(): + """TODO: method for adding recipe""" + query = """ +""" + return sql(query) + + +################################################################################ +# Biometric functions +################################################################################ +def sql_biometrics(): + """Selects biometrics""" + query = "SELECT * FROM biometrics;" + return sql(query, headers=True) + + +def sql_biometric_logs(profile_id): + """Selects biometric logs""" + query = "SELECT * FROM biometric_log WHERE profile_id=?" + return sql(query, values=(profile_id,), headers=True) + + +def sql_biometric_add(bio_vals): + """Insert biometric log item""" + con = nt_sqlite_connect() + cur = con.cursor() + + # TODO: finish up + query1 = "INSERT INTO biometric_log(profile_id, tags, notes) VALUES (?, ?, ?)" + sql(query1, (PROFILE_ID, "", "")) + log_id = cur.lastrowid + print(log_id) + query2 = "INSERT INTO bio_log_entry(log_id, biometric_id, value) VALUES (?, ?, ?)" + records = [ + (log_id, biometric_id, value) for biometric_id, value in bio_vals.items() + ] + cur.executemany(query2, records) + return log_id diff --git a/ntclient/persistence/sql/usda/__init__.py b/ntclient/persistence/sql/usda/__init__.py new file mode 100644 index 0000000..a2f27aa --- /dev/null +++ b/ntclient/persistence/sql/usda/__init__.py @@ -0,0 +1,101 @@ +"""USDA DB specific sqlite module""" +import os +import sqlite3 +import tarfile +import urllib.request +from typing import Union + +from ntclient import NUTRA_DIR, USDA_DB_NAME, __db_target_usda__ +from ntclient.persistence.sql import _sql, version +from ntclient.utils.exceptions import SqlConnectError, SqlInvalidVersionError + + +def usda_init(yes=False) -> None: + """On-boarding function. Downloads tarball and unpacks usda.sqlite3 file""" + + def input_agree() -> str: + return input("\nAgree to USDA download, may take minutes? [Y/n] ") + + def download_extract_usda() -> None: + """Download USDA tarball from BitBucket and extract to storage folder""" + + if yes or input_agree().lower() == "y": + # TODO: save with version in filename? Don't re-download tarball, just extract? + save_path = os.path.join(NUTRA_DIR, "%s.tar.xz" % USDA_DB_NAME) + + # Download usda.sqlite3.tar.xz + print("curl -L %s -o %s.tar.xz" % (url, USDA_DB_NAME)) + urllib.request.urlretrieve(url, save_path) # nosec: B310 + + # Extract the archive + with tarfile.open(save_path, mode="r:xz") as usda_sqlite_file: + print("\ntar xvf %s.tar.xz" % USDA_DB_NAME) + usda_sqlite_file.extractall(NUTRA_DIR) + + print("==> done downloading %s" % USDA_DB_NAME) + + # TODO: handle resource moved on Bitbucket or version mismatch due to manual overwrite? + url = ( + "https://bitbucket.org/dasheenster/nutra-utils/downloads/{0}-{1}.tar.xz".format( + USDA_DB_NAME, __db_target_usda__ + ) + ) + + if USDA_DB_NAME not in os.listdir(NUTRA_DIR): + print("INFO: usda.sqlite3 doesn't exist, is this a fresh install?") + download_extract_usda() + elif usda_ver() != __db_target_usda__: + print( + "INFO: usda.sqlite3 target [{0}] doesn't match actual [{1}], ".format( + __db_target_usda__, usda_ver() + ) + + "static resource (no user data lost).. downloading and extracting correct version" + ) + download_extract_usda() + + if usda_ver() != __db_target_usda__: + raise SqlInvalidVersionError( + "ERROR: usda target [{0}] failed to match actual [{1}], ".format( + __db_target_usda__, usda_ver() + ) + + "please contact support or try again" + ) + + +def usda_sqlite_connect(version_check=True) -> sqlite3.Connection: + """Connects to the usda.sqlite3 file, or throws an exception""" + + # TODO: support as customizable env var ? + db_path = os.path.join(NUTRA_DIR, USDA_DB_NAME) + if os.path.isfile(db_path): + con = sqlite3.connect(db_path) + # con.row_factory = sqlite3.Row # see: https://chrisostrouchov.com/post/python_sqlite/ + + # Verify version + if version_check and usda_ver() != __db_target_usda__: + raise SqlInvalidVersionError( + "ERROR: usda target [{0}] mismatch actual [{1}], ".format( + __db_target_usda__, usda_ver() + ) + + "remove '~/.nutra/usda.sqlite3' and run 'nutra init'" + ) + return con + + # Else it's not on disk + raise SqlConnectError("ERROR: usda database doesn't exist, please run `nutra init`") + + +def usda_ver() -> str: + """Gets version string for usda.sqlite3 database""" + + con = usda_sqlite_connect(version_check=False) + return version(con) + + +def sql(query, values=None, headers=False, version_check=True) -> Union[list, tuple]: + """Executes a SQL command to usda.sqlite3""" + + con = usda_sqlite_connect(version_check=version_check) + + # TODO: support argument: _sql(..., params=params, ...) + return _sql(con, query, db_name="usda", values=values, headers=headers) diff --git a/ntclient/persistence/sql/usda/funcs.py b/ntclient/persistence/sql/usda/funcs.py new file mode 100644 index 0000000..ff21588 --- /dev/null +++ b/ntclient/persistence/sql/usda/funcs.py @@ -0,0 +1,158 @@ +"""usda.sqlite functions module""" +from ntclient.persistence.sql.usda import sql +from ntclient.utils import NUTR_ID_KCAL + + +################################################################################ +# Basic functions +################################################################################ +def sql_fdgrp(): + """Shows food groups""" + + query = "SELECT * FROM fdgrp;" + result = sql(query) + return {x[0]: x for x in result} + + +def sql_food_details(food_ids=None) -> list: + """Readable human details for foods""" + + if food_ids is None: + query = "SELECT * FROM food_des;" + else: + # TODO: does sqlite3 driver support this? cursor.executemany() ? + query = "SELECT * FROM food_des WHERE id IN (%s);" + food_ids = ",".join(str(x) for x in set(food_ids)) + query = query % food_ids + + return sql(query) # type: ignore + + +def sql_nutrients_overview() -> dict: + """Shows nutrients overview""" + + query = "SELECT * FROM nutrients_overview;" + result = sql(query) + return {x[0]: x for x in result} + + +def sql_nutrients_details() -> tuple: + """Shows nutrients 'details'""" + + query = "SELECT * FROM nutrients_overview;" + return sql(query, headers=True) # type: ignore + + +def sql_servings(food_ids) -> list: + """Food servings""" + # TODO: apply connective logic from `sort_foods()` IS ('None') ? + query = """ +SELECT + serv.food_id, + serv.msre_id, + serv_desc.msre_desc, + serv.grams +FROM + serving serv + LEFT JOIN serv_desc ON serv.msre_id = serv_desc.id +WHERE + serv.food_id IN (%s); +""" + food_ids = ",".join(str(x) for x in set(food_ids)) + return sql(query % food_ids) # type: ignore + + +def sql_analyze_foods(food_ids) -> list: + """Nutrient analysis for foods""" + query = """ +SELECT + id, + nutr_id, + nutr_val +FROM + food_des + INNER JOIN nut_data ON food_des.id = nut_data.food_id +WHERE + food_des.id IN (%s); +""" + # TODO: parameterized queries + food_ids = ",".join(str(x) for x in set(food_ids)) + return sql(query % food_ids) # type: ignore + + +################################################################################ +# Sort +################################################################################ +def sql_sort_helper1(nutrient_id) -> list: + """Selects relevant bits from nut_data for sorting""" + + query = """ +SELECT + food_id, + nutr_id, + nutr_val +FROM + nut_data +WHERE + nutr_id = %s + OR nutr_id = %s +ORDER BY + food_id; +""" + + return sql(query % (NUTR_ID_KCAL, nutrient_id)) # type: ignore + + +def sql_sort_foods(nutr_id) -> list: + """Sort foods by nutr_id per 100 g""" + + query = """ +SELECT + nut_data.food_id, + fdgrp_id, + nut_data.nutr_val, + kcal.nutr_val AS kcal, + long_desc +FROM + nut_data + INNER JOIN food_des food ON food.id = nut_data.food_id + INNER JOIN nutr_def ndef ON ndef.id = nut_data.nutr_id + INNER JOIN fdgrp ON fdgrp.id = fdgrp_id + LEFT JOIN nut_data kcal ON food.id = kcal.food_id + AND kcal.nutr_id = 208 +WHERE + nut_data.nutr_id = %s +ORDER BY + nut_data.nutr_val DESC; +""" + + return sql(query % nutr_id) # type: ignore + + +def sql_sort_foods_by_kcal(nutr_id) -> list: + """Sort foods by nutr_id per 200 kcal""" + + # TODO: use parameterized queries + query = """ +SELECT + nut_data.food_id, + fdgrp_id, + ROUND((nut_data.nutr_val * 200 / kcal.nutr_val), 2) AS nutr_val, + kcal.nutr_val AS kcal, + long_desc +FROM + nut_data + INNER JOIN food_des food ON food.id = nut_data.food_id + INNER JOIN nutr_def ndef ON ndef.id = nut_data.nutr_id + INNER JOIN fdgrp ON fdgrp.id = fdgrp_id + -- filter out NULL kcal + INNER JOIN nut_data kcal ON food.id = kcal.food_id + AND kcal.nutr_id = 208 + AND kcal.nutr_val > 0 +WHERE + nut_data.nutr_id = %s +ORDER BY + (nut_data.nutr_val / kcal.nutr_val) DESC; +""" + + return sql(query % nutr_id) # type: ignore diff --git a/ntclient/services/__init__.py b/ntclient/services/__init__.py new file mode 100644 index 0000000..e889809 --- /dev/null +++ b/ntclient/services/__init__.py @@ -0,0 +1,65 @@ +"""Services module, currently only home to SQL/persistence init method""" +import os + +from ntclient import ( + NTSQLITE_BUILDPATH, + NTSQLITE_DESTINATION, + NUTRA_DIR, + __db_target_nt__, +) +from ntclient.ntsqlite.sql import build_ntsqlite +from ntclient.persistence.sql.nt import nt_ver +from ntclient.persistence.sql.usda import usda_init +from ntclient.services import analyze, biometrics, recipe, usda +from ntclient.utils.exceptions import SqlInvalidVersionError + + +def init(yes=False): + """ + TODO: Check for: + 1. .nutra folder + 2. usda + 3a. nt + 3b. default profile? + 4. prefs.json + """ + print("Nutra directory ", end="") + if not os.path.isdir(NUTRA_DIR): + os.makedirs(NUTRA_DIR, 0o755) + print("..DONE!") + + # TODO: print off checks, return False if failed + print("USDA db ", end="") + usda_init(yes=yes) + print("..DONE!") + + print("Nutra db ", end="") + build_ntsqlite() + # TODO: don't overwrite, + # verbose toggle for download, + # option to upgrade + if os.path.isfile(NTSQLITE_DESTINATION): + if nt_ver() != __db_target_nt__: + # TODO: hard requirement? raise error? + print( + "WARN: upgrades/downgrades not supported " + + "(actual: {0} vs. target: {1}), ".format(nt_ver(), __db_target_nt__) + + "please remove `~/.nutra/nt.sqlite3` file or ignore this warning" + ) + print("..DONE!") + os.remove(NTSQLITE_BUILDPATH) # clean up + else: + # TODO: is this logic (and these error messages) the best? + # what if .isdir() == True ? Fails with stacktrace? + os.rename(NTSQLITE_BUILDPATH, NTSQLITE_DESTINATION) + if not nt_ver() == __db_target_nt__: + raise SqlInvalidVersionError( + "ERROR: nt target [{0}] mismatch actual [{1}], ".format( + __db_target_nt__, nt_ver() + ) + + ", please contact support or try again" + ) + print("..DONE!") + + print("\nAll checks have passed!") + return 0, True diff --git a/ntclient/services/analyze.py b/ntclient/services/analyze.py new file mode 100755 index 0000000..656bfb9 --- /dev/null +++ b/ntclient/services/analyze.py @@ -0,0 +1,354 @@ +# -*- coding: utf-8 -*- +""" +Created on Sun Nov 11 23:57:03 2018 + +@author: shane +""" + +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, + NUTR_ID_CARBS, + NUTR_ID_FAT_TOT, + NUTR_ID_FIBER, + NUTR_ID_KCAL, + NUTR_ID_PROTEIN, + THRESH_CRIT, + THRESH_OVER, + THRESH_WARN, +) + + +################################################################################ +# Foods +################################################################################ +def foods_analyze(food_ids, grams=None): + """ + Analyze a list of food_ids against stock RDA values + TODO: from ntclient.utils.nutprogbar import nutprogbar + TODO: support -t (tabular/non-visual) output flag + """ + + ################################################################################ + # Get analysis + ################################################################################ + raw_analyses = sql_analyze_foods(food_ids) + analyses = {} + for analysis in raw_analyses: + food_id = analysis[0] + if grams is not None: + anl = (analysis[1], round(analysis[2] * grams / 100, 2)) + else: + anl = (analysis[1], analysis[2]) + if food_id not in analyses: + analyses[food_id] = [anl] + else: + analyses[food_id].append(anl) + + serving = sql_servings(food_ids) + food_des = sql_food_details(food_ids) + food_des = {x[0]: x for x in food_des} + nutrients = sql_nutrients_overview() + rdas = {x[0]: x[1] for x in nutrients.values()} + + ################################################################################ + # Food-by-food analysis (w/ servings) + ################################################################################ + servings_rows = [] + nutrients_rows = [] + for food_id in analyses: + food_name = food_des[food_id][2] + print( + "\n======================================\n" + + "==> {0} ({1})\n".format(food_name, food_id) + + "======================================\n" + ) + print("\n=========================\nSERVINGS\n=========================\n") + + ################################################################################ + # Serving table + ################################################################################ + headers = ["msre_id", "msre_desc", "grams"] + serving_rows = [(x[1], x[2], x[3]) for x in serving if x[0] == food_id] + # Print table + servings_table = tabulate(serving_rows, headers=headers, tablefmt="presto") + print(servings_table) + servings_rows.append(serving_rows) + + refuse = next( + ((x[7], x[8]) for x in food_des.values() if x[0] == food_id and x[7]), None + ) + if refuse: + print("\n=========================\nREFUSE\n=========================\n") + print(refuse[0]) + print(" ({0}%, by mass)".format(refuse[1])) + + print("\n=========================\nNUTRITION\n=========================\n") + + ################################################################################ + # Nutrient table + ################################################################################ + headers = ["id", "nutrient", "rda", "amount", "units"] + nutrient_rows = [] + for nutrient_id, amount in analyses[food_id]: + # Skip zero values + if not amount: + continue + + nutr_desc = nutrients[nutrient_id][4] or nutrients[nutrient_id][3] + unit = nutrients[nutrient_id][2] + + # Insert RDA % into row + if rdas[nutrient_id]: + rda_perc = str(round(amount / rdas[nutrient_id] * 100, 1)) + "%" + else: + rda_perc = None + row = [nutrient_id, nutr_desc, rda_perc, round(amount, 2), unit] + + nutrient_rows.append(row) + + ################################################################################ + # Print table + ################################################################################ + table = tabulate(nutrient_rows, headers=headers, tablefmt="presto") + print(table) + nutrients_rows.append(nutrient_rows) + + return 0, nutrients_rows, servings_rows + + +################################################################################ +# Day +################################################################################ +def day_analyze(day_csv_paths, rda_csv_path=None): + """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 is not None: + with open(rda_csv_path, encoding="utf-8") as file_path: + rda_csv_input = csv.DictReader( + row for row in file_path if not row.startswith("#") + ) + rdas = list(rda_csv_input) + else: + rdas = [] + + logs = [] + food_ids = set() + for day_csv_path in day_csv_paths: + with open(day_csv_path, encoding="utf-8") as file_path: + rows = [row for row in file_path if not row.startswith("#")] + day_csv_input = csv.DictReader(rows) + log = list(day_csv_input) + for entry in log: + if entry["id"]: + food_ids.add(int(entry["id"])) + logs.append(log) + + # Inject user RDAs + nutrients = [list(x) for x in sql_nutrients_overview().values()] + for rda in rdas: + nutrient_id = int(rda["id"]) + _rda = float(rda["rda"]) + for nutrient in nutrients: + if nutrient[0] == nutrient_id: + nutrient[1] = _rda + if 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} + + # Analyze foods + foods_analysis = {} + for food in sql_analyze_foods(food_ids): + food_id = food[0] + anl = food[1], food[2] + if food_id not in foods_analysis: + foods_analysis[food_id] = [anl] + else: + foods_analysis[food_id].append(anl) + + # Compute totals + nutrients_totals = [] + for log in logs: + nutrient_totals = OrderedDict() # dict()/{} is NOT ORDERED before 3.6/3.7 + for entry in log: + if entry["id"]: + food_id = int(entry["id"]) + grams = float(entry["grams"]) + for nutrient in foods_analysis[food_id]: + nutr_id = nutrient[0] + nutr_per_100g = nutrient[1] + nutr_val = grams / 100 * nutr_per_100g + if nutr_id not in nutrient_totals: + nutrient_totals[nutr_id] = nutr_val + else: + nutrient_totals[nutr_id] += nutr_val + nutrients_totals.append(nutrient_totals) + + ####### + # Print + buffer = BUFFER_WD - 4 if BUFFER_WD > 4 else BUFFER_WD + for analysis in nutrients_totals: + day_format(analysis, nutrients, buffer=buffer) + return 0, nutrients_totals + + +def day_format(analysis, nutrients, buffer=None): + """Formats day analysis for printing to console""" + + def print_header(header): + print(Fore.CYAN, end="") + print("~~~~~~~~~~~~~~~~~~~~~~~~~~~") + print("--> %s" % header) + print("~~~~~~~~~~~~~~~~~~~~~~~~~~~") + print(Style.RESET_ALL) + + def print_macro_bar(_fat, _net_carb, _pro, _kcals_max, _buffer=None): + _kcals = fat * 9 + net_carb * 4 + _pro * 4 + + p_fat = (_fat * 9) / _kcals + p_car = (_net_carb * 4) / _kcals + p_pro = (_pro * 4) / _kcals + + # TODO: handle rounding cases, tack on to, or trim off FROM LONGEST ? + mult = _kcals / _kcals_max + n_fat = round(p_fat * _buffer * mult) + n_car = round(p_car * _buffer * mult) + n_pro = round(p_pro * _buffer * mult) + + # Headers + f_buf = " " * (n_fat // 2) + "Fat" + " " * (n_fat - n_fat // 2 - 3) + c_buf = " " * (n_car // 2) + "Carbs" + " " * (n_car - n_car // 2 - 5) + p_buf = " " * (n_pro // 2) + "Pro" + " " * (n_pro - n_pro // 2 - 3) + print( + " " + + Fore.YELLOW + + f_buf + + Fore.BLUE + + c_buf + + Fore.RED + + p_buf + + Style.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 + ">") + + # Calorie footers + k_fat = str(round(fat * 9)) + k_car = str(round(net_carb * 4)) + k_pro = str(round(pro * 4)) + f_buf = " " * (n_fat // 2) + k_fat + " " * (n_fat - n_fat // 2 - len(k_fat)) + c_buf = " " * (n_car // 2) + k_car + " " * (n_car - n_car // 2 - len(k_car)) + p_buf = " " * (n_pro // 2) + k_pro + " " * (n_pro - n_pro // 2 - len(k_pro)) + print( + " " + + Fore.YELLOW + + f_buf + + Fore.BLUE + + c_buf + + Fore.RED + + p_buf + + Style.RESET_ALL + ) + + def print_nute_bar(_n_id, amount, _nutrients): + nutrient = _nutrients[_n_id] + rda = nutrient[1] + tag = nutrient[3] + unit = nutrient[2] + # anti = nutrient[5] + + if not rda: + return False, nutrient + 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 + else: + color = COLOR_DEFAULT + + # Print + detail_amount = "{0}/{1} {2}".format(round(amount, 1), rda, unit).ljust(18) + detail_amount = "{0} -- {1}".format(detail_amount, tag) + left_index = 20 + left_pos = round(left_index * attain) if attain < 1 else left_index + 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) + + return True, perc + + # Actual values + kcals = round(analysis[NUTR_ID_KCAL]) + pro = analysis[NUTR_ID_PROTEIN] + net_carb = analysis[NUTR_ID_CARBS] - analysis[NUTR_ID_FIBER] + fat = analysis[NUTR_ID_FAT_TOT] + kcals_449 = round(4 * pro + 4 * net_carb + 9 * fat) + + # Desired values + kcals_rda = round(nutrients[NUTR_ID_KCAL][1]) + pro_rda = nutrients[NUTR_ID_PROTEIN][1] + net_carb_rda = nutrients[NUTR_ID_CARBS][1] - nutrients[NUTR_ID_FIBER][1] + fat_rda = nutrients[NUTR_ID_FAT_TOT][1] + + # Print calories and macronutrient bars + print_header("Macronutrients") + kcals_max = max(kcals, kcals_rda) + rda_perc = round(kcals * 100 / kcals_rda, 1) + print( + "Actual: {0} kcal ({1}% RDA), {2} by 4-4-9".format( + kcals, rda_perc, kcals_449 + ) + ) + print_macro_bar(fat, net_carb, pro, kcals_max, _buffer=buffer) + print( + "\nDesired: {0} kcal ({1} kcal)".format( + kcals_rda, "%+d" % (kcals - kcals_rda) + ) + ) + print_macro_bar( + fat_rda, + net_carb_rda, + pro_rda, + kcals_max, + _buffer=buffer, + ) + + # Nutrition detail report + print_header("Nutrition detail report") + for n_id in analysis: + print_nute_bar(n_id, analysis[n_id], nutrients) + # TODO: below + print( + "work in progress...some minor fields with negligible data, they are not shown here" + ) diff --git a/ntclient/services/biometrics.py b/ntclient/services/biometrics.py new file mode 100644 index 0000000..9ac8de1 --- /dev/null +++ b/ntclient/services/biometrics.py @@ -0,0 +1,53 @@ +"""Biometrics SQL functions""" +from tabulate import tabulate + +from ntclient.persistence import PROFILE_ID +from ntclient.persistence.sql.nt.funcs import ( + sql_biometric_add, + sql_biometric_logs, + sql_biometrics, +) + + +def biometrics(): + """Shows biometrics""" + headers, rows = sql_biometrics() + table = tabulate(rows, headers=headers, tablefmt="presto") + print(table) + return 0, rows + + +def biometric_logs(): + """Shows biometric logs""" + headers, rows = sql_biometric_logs(PROFILE_ID) + + table = tabulate(rows, headers=headers, tablefmt="presto") + print(table) + return 0, rows + + +def biometric_add(bio_vals): + """Add a biometric type""" + print() + # print("New biometric log: " + name + "\n") + + bio_names = {x[0]: x for x in sql_biometrics()[1]} + + results = [] + for biometric_id, value in bio_vals.items(): + bio = bio_names[biometric_id] + results.append( + {"id": biometric_id, "name": bio[1], "value": value, "unit": bio[2]} + ) + + table = tabulate(results, headers="keys", tablefmt="presto") + print(table) + + # TODO: print current profile and date? + + confirm = input("\nConfirm add biometric? [Y/n] ") + + if confirm.lower() == "y": + sql_biometric_add(bio_vals) + print("not implemented ;]") + return 1, False diff --git a/ntclient/services/recipe.py b/ntclient/services/recipe.py new file mode 100644 index 0000000..8eb73e2 --- /dev/null +++ b/ntclient/services/recipe.py @@ -0,0 +1,129 @@ +#!/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(): + """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): + """Shows single recipe overview""" + recipe = sql_analyze_recipe(recipe_id) + name = recipe[0][1] + print(name) + + food_ids = {x[2]: x[3] for x in recipe} + food_names = {x[0]: x[3] for x in sql_food_details(food_ids.keys())} + food_analyses = sql_analyze_foods(food_ids.keys()) + + table = tabulate( + [[food_names[food_id], grams] for food_id, grams in food_ids.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, food_analyses, nutrients) + print(progbars) + + return 0, recipe + + +def recipe_import(file_path): + """Import a recipe to SQL database""" + + def extract_id_from_filename(path): + 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 None + + if os.path.isfile(file_path): + # TODO: better logic than this + recipe_id = extract_id_from_filename(file_path) or sql_nt_next_index("recipes") + 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, food_amts): + """Add a recipe to SQL database""" + print() + print("New recipe: " + name + "\n") + + food_names = {x[0]: x[2] for x in sql_food_details(food_amts.keys())} + + 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): + """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 diff --git a/ntclient/services/usda.py b/ntclient/services/usda.py new file mode 100644 index 0000000..e4199ff --- /dev/null +++ b/ntclient/services/usda.py @@ -0,0 +1,244 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat Oct 27 20:28:06 2018 + +@author: shane +""" + +import pydoc + +from tabulate import tabulate + +from ntclient import ( + DEFAULT_RESULT_LIMIT, + DEFAULT_SEARCH_H_BUFFER, + DEFAULT_SORT_H_BUFFER, +) +from ntclient.persistence.sql.usda.funcs import ( + sql_analyze_foods, + sql_food_details, + sql_nutrients_details, + sql_nutrients_overview, + sql_sort_helper1, +) +from ntclient.utils import NUTR_ID_KCAL, NUTR_IDS_AMINOS, NUTR_IDS_FLAVONES + + +def list_nutrients(): + """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") + nutrients = [list(x) for x in nutrients] + for nutrient in nutrients: + rda = nutrient[1] + val = nutrient[6] + if rda: + nutrient.append(round(100 * val / rda, 1)) + else: + nutrient.append(None) + + table = tabulate(nutrients, headers=headers, tablefmt="simple") + if PAGING: + pydoc.pager(table) + else: + print(table) + + return 0, nutrients + + +################################################################################ +# Sort +################################################################################ +def sort_foods(nutrient_id, by_kcal, limit=DEFAULT_RESULT_LIMIT): + """Sort, by nutrient, either (amount / 100 g) or (amount / 200 kcal)""" + + # TODO: sub shrt_desc for long if available, and support config.FOOD_NAME_TRUNC + + def print_results(_results, _nutrient_id): + """Prints truncated list for sort""" + nutrients = sql_nutrients_overview() + nutrient = nutrients[_nutrient_id] + unit = nutrient[2] + + val_header = "grams" if by_kcal else "kcal" + headers = ["food", "fdgrp", "val (%s)" % unit, val_header, "long_desc"] + + table = tabulate(_results, headers=headers, tablefmt="simple") + print(table) + return _results + + # Gets values for nutrient_id and kcal=208 + nut_data = sql_sort_helper1(nutrient_id) + + # Assembles duplicate tuples into single dict entry + food_dat = {} + for food_id, nutr_id, nutr_val in nut_data: + entry = nutr_id, nutr_val + if food_id not in food_dat: + food_dat[food_id] = [entry] + else: + food_dat[food_id].append(entry) + + # Builds main results list + foods = [] + for food_id, _food in food_dat.items(): + kcal = None + nutr_val = 0.0 + for _nutr_id, _nutr_val in _food: + if _nutr_id == NUTR_ID_KCAL: + kcal = _nutr_val + else: + nutr_val = _nutr_val + food = [food_id, nutr_val, kcal] + foods.append(food) + if by_kcal is True: + foods = list(filter(lambda x: x[2], foods)) # removes kcal = 0 case + foods = list( + map( + lambda x: [x[0], round(x[1] * 200 / x[2], 2), round(200 / x[2] * 100)], + foods, + ) + ) + foods.sort(key=lambda x: x[1], reverse=True) + foods = foods[:limit] + food_ids = {x[0] for x in foods} + + # Gets fdgrp and long_desc + food_des = {x[0]: x for x in sql_food_details(food_ids)} + for food in foods: + food_id = food[0] + fdgrp = food_des[food_id][1] + long_desc = food_des[food_id][2] + food.insert(1, fdgrp) + food.append(long_desc[:DEFAULT_SORT_H_BUFFER]) + + print_results(foods, nutrient_id) + return 0, foods # , nutrient_id + + +################################################################################ +# Search +################################################################################ +def search(words, fdgrp_id=None, limit=DEFAULT_RESULT_LIMIT): + """Searches foods for input""" + + def tabulate_search(_results): + """Makes search results more readable""" + # Current terminal size + # TODO: display "nonzero/total" report nutrients, aminos, and flavones.. + # sometimes zero values are not useful + # TODO: macros, ANDI score, and other metrics on preview + + headers = [ + "food", + "fdgrp", + "kcal", + "food_name", + "Nutr", + "Amino", + "Flav", + ] + rows = [] + for i, result in enumerate(_results): + if i == limit: + break + _food_id = result["food_id"] + # TODO: dynamic buffer + # food_name = r["long_desc"][:45] + # food_name = r["long_desc"][:BUFFER_WD] + food_name = result["long_desc"][:DEFAULT_SEARCH_H_BUFFER] + # TODO: decide on food group description? + # fdgrp_desc = r["fdgrp_desc"] + fdgrp = result["fdgrp_id"] + + nutrients = result["nutrients"] + kcal = nutrients.get(NUTR_ID_KCAL) + len_aminos = len( + [nutrients[n_id] for n_id in nutrients if int(n_id) in NUTR_IDS_AMINOS] + ) + len_flavones = len( + [ + nutrients[n_id] + for n_id in nutrients + if int(n_id) in NUTR_IDS_FLAVONES + ] + ) + + row = [ + _food_id, + fdgrp, + kcal, + food_name, + len(nutrients), + len_aminos, + len_flavones, + ] + rows.append(row) + # avail_buffer = bufferwidth - len(food_id) - 15 + # if len(food_name) > avail_buffer: + # rows.append([food_id, food_name[:avail_buffer] + "..."]) + # else: + # rows.append([food_id, food_name]) + table = tabulate(rows, headers=headers, tablefmt="simple") + print(table) + return rows + + ### + # MAIN SEARCH METHOD + from fuzzywuzzy import fuzz # pylint: disable=import-outside-toplevel + + food_des = sql_food_details() + if fdgrp_id is not None: + food_des = list(filter(lambda x: x[1] == fdgrp_id, food_des)) + + query = " ".join(words) + scores = {f[0]: fuzz.token_set_ratio(query, f[2]) for f in food_des} + scores = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:limit] + + food_ids = {x[0] for x in scores} + nut_data = sql_analyze_foods(food_ids) + + # Tally foods + foods_nutrients = {} + for food_id, nutr_id, nutr_val in nut_data: + if food_id not in foods_nutrients: + foods_nutrients[food_id] = {nutr_id: nutr_val} # init dict + else: + foods_nutrients[food_id][nutr_id] = nutr_val + + def search_results(_scores): + """Generates search results, consumable by tabulate""" + _results = [] + for score in _scores: + _food_id = score[0] + score = score[1] + + food = food_des[_food_id] + _fdgrp_id = food[1] + long_desc = food[2] + shrt_desc = food[3] + + nutrients = foods_nutrients[_food_id] + result = { + "food_id": _food_id, + "fdgrp_id": _fdgrp_id, + # TODO: get more details from another function, maybe enhance food_details() ? + # is that useful tho? + # "fdgrp_desc": cache.fdgrp[fdgrp_id]["fdgrp_desc"], + # "data_src": cache.data_src[data_src_id]["name"], + "long_desc": shrt_desc if shrt_desc else long_desc, + "score": score, + "nutrients": nutrients, + } + _results.append(result) + return _results + + # TODO: include C/F/P macro ratios as column? + food_des = {f[0]: f for f in food_des} + results = search_results(scores) + + tabulate_search(results) + return 0, results diff --git a/ntclient/utils/__init__.py b/ntclient/utils/__init__.py new file mode 100644 index 0000000..a6ef413 --- /dev/null +++ b/ntclient/utils/__init__.py @@ -0,0 +1,96 @@ +"""Constants and default settings""" +from colorama import Fore + +################################################################################ +# Colors and buffer settings +################################################################################ + +THRESH_WARN = 0.7 +COLOR_WARN = Fore.YELLOW + +THRESH_CRIT = 0.4 +COLOR_CRIT = Fore.RED + +THRESH_OVER = 1.9 +COLOR_OVER = Fore.MAGENTA # Fore.LIGHTMAGENTA_EX, Fore.LIGHTBLACK_EX + +COLOR_DEFAULT = Fore.LIGHTCYAN_EX # Fore.BLUE, Fore.LIGHTBLUE_EX + +################################################################################ +# 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, +] diff --git a/ntclient/utils/exceptions.py b/ntclient/utils/exceptions.py new file mode 100644 index 0000000..290618e --- /dev/null +++ b/ntclient/utils/exceptions.py @@ -0,0 +1,20 @@ +"""Custom exception classes, used for bubbling up more specific errors""" + + +class SqlException(Exception): + """Base class for Sql errors""" + + +class SqlConnectError(SqlException): + """Typically when it can't find the *.sqlite3 file(s) on disk""" + + +class SqlInvalidVersionError(SqlException): + """Raised when the expected version differs from actual, either for nt or usda DB""" + + +class SqlCrossDatabaseValidationError(SqlException): + """ + Raised when data-bindings (e.g. food_id) in one db (typically nt) + can't be found in another (typically usda) + """ diff --git a/nutra b/nutra new file mode 100755 index 0000000..ba0eb31 --- /dev/null +++ b/nutra @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# PYTHON_ARGCOMPLETE_OK +""" +Created on Fri Sep 28 22:25:38 2018 + +@author: gamesguru +""" + +import sys + +from ntclient.__main__ import main + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/requirements-lint.txt b/requirements-lint.txt new file mode 100644 index 0000000..642306b --- /dev/null +++ b/requirements-lint.txt @@ -0,0 +1,8 @@ +autopep8~=1.6 +bandit~=1.7 +black~=22.3 +doc8~=0.11 +flake8~=4.0 +mypy~=0.960 +pylint~=2.13 +yamllint~=1.26 diff --git a/requirements-optional.txt b/requirements-optional.txt new file mode 100644 index 0000000..3e3e163 --- /dev/null +++ b/requirements-optional.txt @@ -0,0 +1 @@ +python-Levenshtein==0.12.2 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..67ad948 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +argcomplete==2.0.0 +colorama==0.4.5 +fuzzywuzzy==0.18.0 +tabulate==0.8.10 diff --git a/scripts/nutra b/scripts/nutra new file mode 100755 index 0000000..a1b3619 --- /dev/null +++ b/scripts/nutra @@ -0,0 +1,12 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# PYTHON_ARGCOMPLETE_OK +"""Executable script, copied over by pip""" +import re +import sys + +from ntclient.__main__ import main + +if __name__ == "__main__": + sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) + sys.exit(main()) diff --git a/scripts/nutra.py b/scripts/nutra.py new file mode 100755 index 0000000..a1b3619 --- /dev/null +++ b/scripts/nutra.py @@ -0,0 +1,12 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# PYTHON_ARGCOMPLETE_OK +"""Executable script, copied over by pip""" +import re +import sys + +from ntclient.__main__ import main + +if __name__ == "__main__": + sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) + sys.exit(main()) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..adf2686 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,45 @@ +[coverage:run] +source = ntclient + +[coverage:report] +fail_under = 80 + +show_missing = True +skip_empty = True +skip_covered = True + + +[pycodestyle] +max-line-length = 88 + + +[flake8] +per-file-ignores = + # Allow unused imports in __init__.py files + __init__.py:F401 + +max-line-length = 100 + +ignore = + E501, # line-length (currently handled by pycodestyle) + W503, # line break before binary operator + + +[isort] +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 + + +[mypy] +ignore_missing_imports = True +show_error_codes = True +disable_error_code = import +disallow_untyped_calls = True +disallow_untyped_decorators = True +strict_optional = False +warn_redundant_casts = True +warn_unused_ignores = True diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..fffe1b8 --- /dev/null +++ b/setup.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat Oct 13 16:30:30 2018 + +@author: shane +""" + +import glob +import os +import platform + +from setuptools import find_packages, setup + +from ntclient import PY_MIN_STR, __author__, __email__, __title__, __version__ + +# cd to parent dir of setup.py +os.chdir(os.path.dirname(os.path.abspath(__file__))) + +CLASSIFIERS = [ + "Environment :: Console", + "Intended Audience :: End Users/Desktop", + "Intended Audience :: Science/Research", + "Intended Audience :: Healthcare Industry", + "Intended Audience :: Education", + "Development Status :: 3 - Alpha", + "Natural Language :: English", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Framework :: Flake8", + "Framework :: Pytest", + "Operating System :: OS Independent", + "Operating System :: Microsoft :: Windows :: Windows XP", + "Operating System :: Microsoft :: Windows :: Windows 10", + "Operating System :: MacOS :: MacOS X", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.11", + "Programming Language :: SQL", + "Programming Language :: Unix Shell", +] + +with open("README.rst", encoding="utf-8") as file: + README = file.read() + +with open("requirements.txt", encoding="utf-8") as file: + REQUIREMENTS = file.read().split() + +if platform.system() != "Windows": + # python-Levenshtein builds natively on unix, requires vcvarsall.bat or vc++10 on Windows + with open("requirements-optional.txt", encoding="utf-8") as file: + optional_reqs = file.read().split() + REQUIREMENTS.extend(optional_reqs) + +setup( + name=__title__, + author=__author__, + author_email=__email__, + classifiers=CLASSIFIERS, + install_requires=REQUIREMENTS, + python_requires=">=%s" % PY_MIN_STR, + zip_safe=False, + packages=find_packages(exclude=["tests"]), + include_package_data=True, + platforms=["linux", "darwin", "win32"], + scripts=glob.glob("scripts/*"), + # entry_points={"console_scripts": ["nutra=ntclient.__main__:main"]}, + description="Home and office nutrient tracking software", + long_description=README, + long_description_content_type="text/x-rst", + url="https://github.com/nutratech/cli", + license="GPL v3", + version=__version__, +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/__main__.py b/tests/__main__.py new file mode 100644 index 0000000..895d6d6 --- /dev/null +++ b/tests/__main__.py @@ -0,0 +1,38 @@ +""" +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()) diff --git a/tests/requirements-win_xp-ubu1604.txt b/tests/requirements-win_xp-ubu1604.txt new file mode 100644 index 0000000..39fde64 --- /dev/null +++ b/tests/requirements-win_xp-ubu1604.txt @@ -0,0 +1,3 @@ +# Don't update these, they are the last supported versions on winXP, Ubuntu 16.04 & Python 3.4 +coverage==5.5 +pytest==3.2.5 diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..311678e --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,2 @@ +coverage~=6.0 +pytest~=7.0 diff --git a/tests/resources/day/dog-simple.csv b/tests/resources/day/dog-simple.csv new file mode 100644 index 0000000..e91283b --- /dev/null +++ b/tests/resources/day/dog-simple.csv @@ -0,0 +1,14 @@ +meal,subcat,id,grams,notes,desired_kcal +breakfast,base,20044,90,"rice, uncooked [55g cooked]",120 +breakfast,base,16042,50,"pinto beans, raw [cooked: ¼ cup or 56g]",60 +breakfast,base,5062,50,chicken breast,50 +breakfast,cooked (veg),11507,25,"sweet potato, raw",20 +breakfast,cooked (veg),11233,10,"kale, raw OR broccoli, boiled, w/o salt",5 +breakfast,soaked,12220,10,flax [½ tbsp],30 +breakfast,chopped,11297,3,"parsley, fresh",1 +breakfast,supplements,,0.075,milk thistle extract, +breakfast,supplements,,0.15,"calcium citrate, [1/8 tsp]", +,,,,, +,,,,, +,,,,, +TOTAL,,,,,286 \ No newline at end of file diff --git a/tests/resources/day/dog.csv b/tests/resources/day/dog.csv new file mode 100644 index 0000000..89c9151 --- /dev/null +++ b/tests/resources/day/dog.csv @@ -0,0 +1,20 @@ +meal,id,grams,notes,desired_kcal +breakfast,20044,19,"rice, uncooked",70 +breakfast,8120,13,"oats, raw",50 +breakfast,16042,14,"pinto beans, raw",50 +breakfast,5062,33,chicken breast,40 +breakfast,11304,16,"peas, green, raw",15 +breakfast,11507,23,"sweet potato, raw",20 +breakfast,11091,14,"broccoli, boiled, w/o salt",5 +breakfast,12155,6,walnut,40 +breakfast,12220,6,flax,30 +breakfast,1289,35,kefir,15 +breakfast,9050,18,blueberries,10 +breakfast,9040,23,banana,20 +breakfast,11297,7,"parsley, fresh", +breakfast,2064,5,"peppermint, fresh", +breakfast,,0.075,milk thistle extract, +,,,, +,,,, +,,,, +TOTAL,,,,365 diff --git a/tests/resources/day/human-test.csv b/tests/resources/day/human-test.csv new file mode 100644 index 0000000..a2cd57e --- /dev/null +++ b/tests/resources/day/human-test.csv @@ -0,0 +1,23 @@ +meal,id,grams,serving_id,serving_qty +breakfast,13047,100,, +breakfast,1270,28,, +breakfast,9038,55,, +breakfast,11251,20,, +breakfast,11529,35,, +breakfast,11282,15,, +breakfast,11828,210,, +breakfast,28313,40,, +breakfast,9112,100,, +lunch,20137,140,, +lunch,5062,100,, +lunch,12136,45,, +lunch,11821,50,, +lunch,44005,15,, +dinner,20545,150,, +dinner,16146,85,, +dinner,1270,40,, +dinner,9037,60,, +dinner,15076,100,, +dinner,11090,60,, +dinner,11938,35,, +dinner,11282,25,, diff --git a/tests/resources/day/original-dog-simple.csv b/tests/resources/day/original-dog-simple.csv new file mode 100644 index 0000000..8206c40 --- /dev/null +++ b/tests/resources/day/original-dog-simple.csv @@ -0,0 +1,10 @@ +meal,id,grams,notes +breakfast,20046,500,"rice, uncooked" +breakfast,5306,400,ground turkey +breakfast,23572,400,ground beef +breakfast,5028,400,chicken liver +breakfast,5062,400,chicken breast +breakfast,15048,400,canned mackerel +breakfast,11422,800,"pumpkin, raw [canned]" +breakfast,11304,400,"Peas, green, raw" +breakfast,11090,400,broccoli diff --git a/tests/resources/prefs.json b/tests/resources/prefs.json new file mode 100644 index 0000000..27b8920 --- /dev/null +++ b/tests/resources/prefs.json @@ -0,0 +1,3 @@ +{ + "current_user": 1 +} diff --git a/tests/resources/rda/dog-18lbs.csv b/tests/resources/rda/dog-18lbs.csv new file mode 100644 index 0000000..3614f94 --- /dev/null +++ b/tests/resources/rda/dog-18lbs.csv @@ -0,0 +1,83 @@ +# Revised Mon 10 Aug 2020 10:11:09 AM EDT,,,, +# nutra version 0.1.0.dev1,,,, +#,,,, +id,rda,units,tagname,nutr_desc +203,18,g,PRO,Protein +204,25,g,FAT,Total lipid (fat) +205,60,g,CARB,"Carbohydrate, by difference" +208,501,kcal,CAL,Energy +269,15,g,SUGAR,"Sugars, total" +291,9,g,FIBTG,"Fiber, total dietary" +301,300,mg,CA,"Calcium, Ca" +303,4,mg,FE,"Iron, Fe" +304,100,mg,MG,"Magnesium, Mg" +305,250,mg,P,"Phosphorus, P" +306,1300,mg,K,"Potassium, K" +307,350,mg,NA,"Sodium, Na" +309,3,mg,ZN,"Zinc, Zn" +312,0.2,mg,CU,"Copper, Cu" +315,0.6,mg,MN,"Manganese, Mn" +317,15,µg,SE,"Selenium, Se" +318,1000,IU,VITA_IU,"Vitamin A, IU" +320,250,µg,VITA_RAE,"Vitamin A, RAE" +324,100,IU,VITD_IU,Vitamin D +328,5,µg,VITD,Vitamin D (D2 + D3) +337,900,µg,LYCPN,Lycopene +338,2000,µg,LUTZEA,Lutein + zeaxanthin +401,30,mg,VITC,"Vitamin C, total ascorbic acid" +404,0.3,mg,B1,Thiamin +405,0.35,mg,B2,Riboflavin +406,4,mg,B3,Niacin +410,1.25,mg,B5,Pantothenic acid +415,0.4,mg,B6,Vitamin B-6 +417,100,µg,B9,"Folate, total" +418,0.6,µg,B12,Vitamin B-12 +421,110,mg,CHO,"Choline, total" +430,30,µg,VITK,Vitamin K (phylloquinone) +501,0.1,g,TRP_G,Tryptophan +502,0.3,g,THR_G,Threonine +503,0.5,g,ILE_G,Isoleucine +504,1,g,LEU_G,Leucine +505,0.8,g,LYS_G,Lysine +506,0.3,g,MET_G,Methionine +508,0.3,g,PHE_G,Phenylalanine +509,0.4,g,TYR_G,Tyrosine +510,0.5,g,VAL_G,Valine +511,0.3,g,ARG_G,Arginine +512,0.2,g,HISTN_G,Histidine +513,0.3,g,ALA_G,Alanine +514,0.3,g,ASP_G,Aspartic acid +515,0.5,g,GLU_G,Glutamic acid +516,0.2,g,GLY_G,Glycine +517,0.2,g,PRO_G,Proline +518,0.4,g,SER_G,Serine +601,50,mg,CHOLEST,Cholesterol +605,0,g,FATRN,"Fatty acids, total trans" +606,7,g,FASAT,"Fatty acids, total saturated" +621,0.1,g,F22D6,22:6 n-3 (DHA) +629,0.05,g,F20D5,20:5 n-3 (EPA) +645,10,g,FAMS,"Fatty acids, total monounsaturated" +646,6,g,FAPU,"Fatty acids, total polyunsaturated" +710,8,mg,DAID,Daidzein +711,4,mg,GENI,Genistein +713,11,mg,TOTISO,Total isoflavones +731,5,mg,CYAD,Cyanidin +734,10,mg,PAdimer,Proanthocyanidin dimers +735,5,mg,PAtrimer,Proanthocyanidin trimers +736,5,mg,PA4-6mer,Proanthocyanidin 4-6mers +737,2,mg,PA7-10mer,Proanthocyanidin 7-10mers +738,1,mg,PApolymer,Proanthocyanidin polymers (>10mers) +749,10,mg,CATE,(+)-Catechin +750,10,mg,EPICATEGC,(-)-Epigallocatechin +751,10,mg,EPICATEC,(-)-Epicatechin +752,10,mg,EPICATECG3,(-)-Epicatechin 3-gallate +753,15,mg,EGCG,(-)-Epigallocatechin 3-gallate +759,20,mg,HESPT,Hesperetin +762,15,mg,NARING,Naringenin +770,8,mg,APIGEN,Apigenin +773,5,mg,LUTEOL,Luteolin +785,5,mg,ISORHAM,Isorhamnetin +786,1.5,mg,KAEMF,Kaempferol +788,0.5,mg,MYRIC,Myricetin +789,5,mg,QUERCE,Quercetin +851,0.3,g,F18D3CN3,"18:3 n-3 c,c,c (ALA)" diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..1d314cf --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,286 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Jan 31 15:19:53 2020 + +@author: shane +""" +import os +import sqlite3 +import sys + +import pytest + +from ntclient import ( + NTSQLITE_BUILDPATH, + NUTRA_DIR, + USDA_DB_NAME, + __db_target_nt__, + __db_target_usda__, + set_flags, +) +from ntclient.__main__ import build_argparser +from ntclient.__main__ import main as nt_main +from ntclient.core import nutprogbar +from ntclient.ntsqlite.sql import build_ntsqlite +from ntclient.persistence.sql.nt import funcs as nt_funcs +from ntclient.persistence.sql.nt import nt_ver +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.utils.exceptions import SqlInvalidVersionError + +# TODO: integration tests.. create user, recipe, log.. analyze & compare +arg_parser = build_argparser() +TEST_HOME = os.path.dirname(os.path.abspath(__file__)) + + +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__ + + headers, rows = nt_funcs.sql_biometrics() + assert headers == ["id", "name", "unit", "created"] + assert len(rows) == 29 + + +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_paging 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] + ) + 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 == 1 + + 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 + + +def test_600_sql_integrity_error__service_wip(): + """Provokes IntegrityError in nt.sqlite3""" + 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" + ) + + +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_DIR, 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?") + + 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