From 78a06765c2e6478ab268123ac5fa89b1c4a77ba2 Mon Sep 17 00:00:00 2001 From: Zev Averbach Date: Thu, 8 Feb 2024 23:15:05 +0100 Subject: [PATCH] halfway to databased --- README.md | 3 +- inventory.json | 14 +- poetry.lock | 237 ++++++++++++++++++++++++++++++++- pyproject.toml | 1 + src/sup/_configs_to_sql.py | 174 +++++++++++++++++++++--- src/sup/_create_tables.py | 56 +++++--- src/sup/cli.py | 39 ++---- src/sup/commands.py | 51 ++++++- src/sup/main.py | 8 +- src/sup/models.py | 2 +- src/sup/queries.py | 59 +++++++- src/sup/scripts/create_user.py | 15 --- src/sup/sql_funcs.py | 16 ++- supps.toml | 11 +- 14 files changed, 571 insertions(+), 115 deletions(-) delete mode 100644 src/sup/scripts/create_user.py diff --git a/README.md b/README.md index 6a180cb..6acedea 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,8 @@ Okay, next fill-up set to (configured in `sup.toml`::FILL TODO: talk about product_aliases -To configure what you take, how much, and when, add entries to `supps.toml`. The fields are configured as such in the `Supp` class: +To configure what you take, how much, and when, add entries to `supps.toml`. +The fields are configured as such in the `Supp` class: ```python name: str diff --git a/inventory.json b/inventory.json index fb99647..6284fe9 100644 --- a/inventory.json +++ b/inventory.json @@ -83,7 +83,7 @@ { "orderDate": "2024-01-23T23:00:00.000Z", "name": "Liquid D-3 & MK-7", - "quantity": 81.5, + "quantity": 82, "quantityUnits": "iu", "servingUnit": "iu", "numUnitsInServing": 5000, @@ -410,7 +410,7 @@ "quantity": 59, "quantityUnits": "ml", "servingUnit": "ml", - "numUnitsInServing": 0.126, + "numUnitsInServing": 126, "numBottles": 1 }, { @@ -445,7 +445,7 @@ "quantity": 90, "numUnitsInServing": 25, "servingUnit": "mg", - "quantityUnit": "caps", + "quantityUnits": "caps", "orderDate": "2024-02-03", "numBottles": 1 }, @@ -480,7 +480,7 @@ "orderDate": "2024-02-04T23:00:00.000Z", "name": "Ultimate Omega", "quantity": 640, - "quantityUnits": "mg per Soft Gel", + "quantityUnits": "mg", "servingUnit": "mg", "numUnitsInServing": 1280, "numBottles": 1 @@ -525,9 +525,9 @@ "orderDate": "2024-02-04T23:00:00.000Z", "name": "Liquid Iodine Plus", "quantity": 59, - "quantityUnits": "ml", - "servingUnit": "ml", - "numUnitsInServing": 0.126, + "quantityUnits": "mcl", + "servingUnit": "mcl", + "numUnitsInServing": 126, "numBottles": 1 }, { diff --git a/poetry.lock b/poetry.lock index 18bc05f..9b63150 100644 --- a/poetry.lock +++ b/poetry.lock @@ -22,6 +22,24 @@ files = [ {file = "astroid-3.0.2.tar.gz", hash = "sha256:4a61cf0a59097c7bb52689b0fd63717cd2a8a14dc9f1eee97b82d814881c8c91"}, ] +[[package]] +name = "asttokens" +version = "2.4.1" +description = "Annotate AST trees with source code positions" +optional = false +python-versions = "*" +files = [ + {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, + {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, +] + +[package.dependencies] +six = ">=1.12.0" + +[package.extras] +astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] +test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] + [[package]] name = "black" version = "24.1.1" @@ -91,6 +109,17 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +optional = false +python-versions = ">=3.5" +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + [[package]] name = "dill" version = "0.3.8" @@ -106,6 +135,20 @@ files = [ graph = ["objgraph (>=1.7.2)"] profile = ["gprof2dot (>=2022.7.29)"] +[[package]] +name = "executing" +version = "2.0.1" +description = "Get the currently executing AST node of a frame, and other information" +optional = false +python-versions = ">=3.5" +files = [ + {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, + {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, +] + +[package.extras] +tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] + [[package]] name = "greenlet" version = "3.0.3" @@ -177,6 +220,41 @@ files = [ docs = ["Sphinx", "furo"] test = ["objgraph", "psutil"] +[[package]] +name = "ipython" +version = "8.21.0" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.10" +files = [ + {file = "ipython-8.21.0-py3-none-any.whl", hash = "sha256:1050a3ab8473488d7eee163796b02e511d0735cf43a04ba2a8348bd0f2eaf8a5"}, + {file = "ipython-8.21.0.tar.gz", hash = "sha256:48fbc236fbe0e138b88773fa0437751f14c3645fb483f1d4c5dee58b37e5ce73"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +prompt-toolkit = ">=3.0.41,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5" + +[package.extras] +all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.23)", "pandas", "pickleshare", "pytest (<8)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +black = ["black"] +doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest (<8)", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["pickleshare", "pytest (<8)", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "pickleshare", "pytest (<8)", "pytest-asyncio (<0.22)", "testpath", "trio"] + [[package]] name = "isort" version = "5.13.2" @@ -191,6 +269,25 @@ files = [ [package.extras] colors = ["colorama (>=0.4.6)"] +[[package]] +name = "jedi" +version = "0.19.1" +description = "An autocompletion tool for Python that can be used for text editors." +optional = false +python-versions = ">=3.6" +files = [ + {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, + {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, +] + +[package.dependencies] +parso = ">=0.8.3,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -215,6 +312,20 @@ profiling = ["gprof2dot"] rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] +[[package]] +name = "matplotlib-inline" +version = "0.1.6" +description = "Inline Matplotlib backend for Jupyter" +optional = false +python-versions = ">=3.5" +files = [ + {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, + {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, +] + +[package.dependencies] +traitlets = "*" + [[package]] name = "mccabe" version = "0.7.0" @@ -300,6 +411,21 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] +[[package]] +name = "parso" +version = "0.8.3" +description = "A Python Parser" +optional = false +python-versions = ">=3.6" +files = [ + {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, + {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, +] + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] + [[package]] name = "pathspec" version = "0.12.1" @@ -311,6 +437,20 @@ files = [ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] +[[package]] +name = "pexpect" +version = "4.9.0" +description = "Pexpect allows easy control of interactive console applications." +optional = false +python-versions = "*" +files = [ + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + [[package]] name = "platformdirs" version = "4.2.0" @@ -326,6 +466,45 @@ files = [ docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +[[package]] +name = "prompt-toolkit" +version = "3.0.43" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "prompt_toolkit-3.0.43-py3-none-any.whl", hash = "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6"}, + {file = "prompt_toolkit-3.0.43.tar.gz", hash = "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +optional = false +python-versions = "*" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "pure-eval" +version = "0.2.2" +description = "Safely evaluate AST nodes without side effects" +optional = false +python-versions = "*" +files = [ + {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, + {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, +] + +[package.extras] +tests = ["pytest"] + [[package]] name = "pydantic" version = "2.6.1" @@ -533,6 +712,17 @@ files = [ {file = "ruff-0.2.0.tar.gz", hash = "sha256:63856b91837606c673537d2889989733d7dffde553828d3b0f0bacfa6def54be"}, ] +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + [[package]] name = "sqlalchemy" version = "2.0.25" @@ -635,6 +825,25 @@ files = [ pydantic = ">=1.10.13,<3.0.0" SQLAlchemy = ">=2.0.0,<2.1.0" +[[package]] +name = "stack-data" +version = "0.6.3" +description = "Extract data from python stack frames and tracebacks for informative displays" +optional = false +python-versions = "*" +files = [ + {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, + {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, +] + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + [[package]] name = "toml" version = "0.10.2" @@ -657,6 +866,21 @@ files = [ {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"}, ] +[[package]] +name = "traitlets" +version = "5.14.1" +description = "Traitlets Python configuration system" +optional = false +python-versions = ">=3.8" +files = [ + {file = "traitlets-5.14.1-py3-none-any.whl", hash = "sha256:2e5a030e6eff91737c643231bfcf04a65b0132078dad75e4936700b213652e74"}, + {file = "traitlets-5.14.1.tar.gz", hash = "sha256:8585105b371a04b8316a43d5ce29c098575c2e477850b62b848b964f1444527e"}, +] + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"] + [[package]] name = "typer" version = "0.9.0" @@ -689,7 +913,18 @@ files = [ {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, ] +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "79f33e8ba6a5c94a5af2c291bedb84d6131bd7bde31396e2f4348c1fdacfa3b2" +content-hash = "f1694f707d895f0211021d90acef4c7449085c1c2cac19bbb35f14a7820dcaa6" diff --git a/pyproject.toml b/pyproject.toml index ee7ad17..5c85e1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ python-dotenv = "^1.0.1" pylint = "^3.0.3" ruff = "^0.2.0" black = "^24.1.1" +ipython = "^8.21.0" [build-system] requires = ["poetry-core"] diff --git a/src/sup/_configs_to_sql.py b/src/sup/_configs_to_sql.py index 6cfb709..296f879 100644 --- a/src/sup/_configs_to_sql.py +++ b/src/sup/_configs_to_sql.py @@ -1,38 +1,180 @@ +import collections from dataclasses import asdict +import datetime as dt +import hashlib import json import pathlib as pl import tomllib -from sup.commands import add_user_supp_consumption +from mysql.connector.errors import IntegrityError + +from sup.commands import ( + add_user_supp_consumption, + add_inventory, + add_product, + add_alias, + set_fill_every_x_days, + set_last_fill_date, + create_user, +) from sup.models import Supp -from sup.queries import get_product_id_from_alias +from sup.queries import product_is_in_products_table, alias_exists, inventory_exists, consumption_exists +from sup.sql_funcs import commit + +def load_config(): + return tomllib.loads(pl.Path('supps.toml').read_text()) + +CONFIG = load_config() def transfer_inventory_to_sql(): - ... + inventory = json.loads(pl.Path('inventory.json').read_text()) + for i in inventory: + add_inventory( + user_id='zev@averba.ch', + product_name=i['name'], + order_date=i['orderDate'][:10], + num_bottles=i['numBottles'] + ) -def transfer_supps_consumption_toml_to_sql(): - toml = tomllib.loads(pl.Path('supps.toml').read_text()) - for sup in toml["supps"]: +def populate_products(): + inventory = json.loads(pl.Path('inventory.json').read_text()) + for i in inventory: + try: + add_product( + name=i['name'], + quantity=i['quantity'], + num_units_in_serving=i['numUnitsInServing'], + quantity_units=i['quantityUnits'], + serving_units=i['servingUnit'], + ) + except KeyError: + print(i) + raise + except IntegrityError: + print('already exists') + continue + commit() + + +def transfer_supps_consumption_toml_to_sql() -> None: + aliases = {v: k for k, v in CONFIG['product_aliases'].items()} + product_names = [i['name'] for i in json.loads(pl.Path('inventory.json').read_text())] + for sup in CONFIG["supps"]: si = Supp(**sup) si_dict = asdict(si) - name = si_dict['name'] + _name = si_dict['name'] del si_dict['name'] - product_id = get_product_id_from_alias(name) - - add_user_supp_consumption(**si_dict) + name = None + if _name.lower() in aliases: + name = aliases[_name.lower()] + else: + for pn in product_names: + if _name.lower() in pn.lower(): + name = pn + break + name = name or _name + print(name) + add_user_supp_consumption(name=name, user_id="zev@averba.ch", **si_dict) + commit() def transfer_config_to_sql(): """fill every x days and last fill""" - ... + fill_every_x_days = CONFIG['FILL_EVERY_X_DAYS'] + last_fill_date = CONFIG['LAST_FILL_DATE'] + set_fill_every_x_days(value=fill_every_x_days, user_id="zev@averba.ch") + set_last_fill_date(value=last_fill_date, user_id="zev@averba.ch") + commit() + def transfer_aliases_to_sql(): - ... + aliases = CONFIG['product_aliases'] + for name, alias in aliases.items(): + add_alias("zev@averba.ch", name, alias) + commit() -transfer_config_to_sql() -transfer_aliases_to_sql() -transfer_supps_consumption_toml_to_sql() -transfer_inventory_to_sql() +def validate_products_against_toml(): + inventory = json.loads(pl.Path('inventory.json').read_text()) + for i in inventory: + if not product_is_in_products_table(name=i['name'], q=round(i['quantity']), num_units=i['numUnitsInServing']): + raise Exception + + +def do_create_user(): + email = "zev@averba.ch" + pw = "@m_6Lwx.CjqvfwG@hmT" + first_name = "Zev" + last_name = "Averbach" + pw_hash = hashlib.new("SHA256") + pw_hash.update(pw.encode()) + create_user(email, pw_hash.hexdigest(), first_name, last_name, dt.date.today(), 30) + commit() + print("okay, created user") + + +def validate_product_aliases(): + aliases = CONFIG['product_aliases'] + for name, alias in aliases.items(): + if not alias_exists(name=name, alias=alias, user_id="zev@averba.ch"): + raise Exception + + +def validate_inventory(): + inventory = json.loads(pl.Path('inventory.json').read_text()) + for i in inventory: + if not inventory_exists( + user_id="zev@averba.ch", + product_name=i['name'], + order_date=i['orderDate'][:10], + quantity=i['quantity'], + serving_q=i["numUnitsInServing"], + ): + raise Exception + # PRIMARY KEY (user_id, product_id, order_date) + +def validate_consumption(): + """ + Something is wrong here; several of the items in supps.toml are missing from + the consumption table. + """ + aliases = collections.defaultdict(list) + for k, v in CONFIG['product_aliases'].items(): + aliases[v].append(k) + product_names = [i['name'] for i in json.loads(pl.Path('inventory.json').read_text())] + for sup in CONFIG["supps"]: + si = Supp(**sup) + si_dict = asdict(si) + _name = si_dict['name'] + del si_dict['name'] + name = None + names = None + if _name in aliases: + names = aliases[_name] + print(f"{names=}") + else: + for pn in product_names: + if _name.lower() in pn.lower(): + name = pn + break + if names: + if not any(consumption_exists(name=n, user_id="zev@averba.ch", **si_dict) for n in names): + raise Exception + else: + name = name or _name + if not consumption_exists(name=name, user_id="zev@averba.ch", **si_dict): + raise Exception + + +# do_create_user() +# populate_products() +# transfer_inventory_to_sql() +# transfer_aliases_to_sql() +# transfer_supps_consumption_toml_to_sql() +# transfer_config_to_sql() +# validate_products_against_toml() +# validate_product_aliases() +# validate_inventory() +# validate_consumption() diff --git a/src/sup/_create_tables.py b/src/sup/_create_tables.py index 6c67dcc..c416ba9 100644 --- a/src/sup/_create_tables.py +++ b/src/sup/_create_tables.py @@ -26,10 +26,14 @@ atexit.register(close_everything) def do_query(create_statement: str) -> None: + table_name = None try: table_name = create_statement.lower().split("create table ")[1].split(" ")[0] except IndexError: - table_name = create_statement.lower().split("drop table ")[1].split(" ")[0] + try: + table_name = create_statement.lower().split("drop table ")[1].split(" ")[0] + except IndexError: + pass print(f"{table_name=}") print(create_statement) try: @@ -63,17 +67,18 @@ def create_table_products(): id INT NOT NULL AUTO_INCREMENT, name VARCHAR(150) NOT NULL, quantity INT NOT NULL, - quantity_units ENUM('caps', 'mg', 'g', 'ml', 'mcg', 'iu'), - serving_units ENUM('caps', 'mg', 'g', 'ml', 'mcg', 'iu'), + quantity_units ENUM('caps', 'mg', 'g', 'ml', 'mcg', 'mcl', 'iu'), + serving_units ENUM('caps', 'mg', 'g', 'ml', 'mcg', 'mcl', 'iu'), num_units_in_serving INT NOT NULL, - CONSTRAINT pk_product PRIMARY KEY (id) + CONSTRAINT pk_product_name PRIMARY KEY (id, name), + CONSTRAINT uq_name_quantity_numunitsinserving UNIQUE KEY (name, quantity, num_units_in_serving) )""").strip() do_query(query) def create_table_product_aliases(): query = (""" - create table user_product_aliases ( + CREATE TABLE user_product_aliases ( user_id VARCHAR(80) NOT NULL, product_id INT NOT NULL, alias VARCHAR(30) NOT NULL, @@ -90,11 +95,11 @@ def create_table_user_supplements_consumption(): user_id VARCHAR(80) NOT NULL, product_id INT NOT NULL, morning INT DEFAULT 0, - lunch INT NULL, - dinner INT NULL, - bedtime INT NULL, + lunch INT DEFAULT 0, + dinner INT DEFAULT 0, + bedtime INT DEFAULT 0, days_per_week INT DEFAULT 7, - units ENUM('caps', 'mg', 'g', 'ml', 'mcg', 'iu'), + units ENUM('caps', 'mg', 'g', 'ml', 'mcg', 'mcl', 'iu'), winter_only BOOL DEFAULT false, CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES users(id), CONSTRAINT fk_product_id FOREIGN KEY (product_id) REFERENCES products(id), @@ -103,7 +108,7 @@ def create_table_user_supplements_consumption(): do_query(query) -def create_table_user_supplements_inventory(): +def create_table_user_supplements_orders(): query = (""" create table user_supplements_orders ( user_id VARCHAR(80) NOT NULL, @@ -130,19 +135,30 @@ def create_tables1(): return def create_tables2(): - # try: - # create_table_user_supplements_consumption() - # except Exception: - # return - # create_table_user_supplements_inventory() - create_table_product_aliases() + try: + create_table_user_supplements_consumption() + except Exception: + return + try: + create_table_user_supplements_orders() + except Exception: + do_query("drop table user_supplements_consumption") + try: + create_table_product_aliases() + except Exception: + do_query("drop table user_supplements_consumption") + do_query("drop table user_supplements_orders") + def drop_tables(): - do_query("drop table users") - do_query("drop table products") do_query("drop table user_supplements_consumption") do_query("drop table user_supplements_orders") + do_query("drop table user_product_aliases") + do_query("drop table users") + do_query("drop table products") -# drop_tables() -create_tables2() +if __name__ == "__main__": + drop_tables() + create_tables1() + create_tables2() diff --git a/src/sup/cli.py b/src/sup/cli.py index 4277244..97bce19 100644 --- a/src/sup/cli.py +++ b/src/sup/cli.py @@ -12,10 +12,9 @@ from sup.main import ( load_config, SUPP_CONSUMPTION_FP, load_inventory, - CONFIG, - ALIASES_REV, ) from sup.models import Supp +from sup.queries import get_user app = typer.Typer() @@ -97,20 +96,20 @@ def status(): TODO: this doesn't seem to sense pending orders which have been added to inventory.json maybe because the delivery date is in the present/future? """ - validate_matches() + # TODO: make this an arg + user_id = "zev@averba.ch" - config = load_config() - - num_days_of_inventory_needed = config["FILL_EVERY_X_DAYS"] - + user = get_user(user_id) + num_days_of_inventory_needed = user["fill_every_x_days"] last_fill_date = dt.datetime.strptime( - config["LAST_FILL_DATE"], "%Y-%m-%d" + user["last_fill_date"], "%Y-%m-%d" ).date() next_fill_date = last_fill_date + dt.timedelta(num_days_of_inventory_needed) print() print(f"{next_fill_date=}") print() - inventory = load_inventory() + # TODO: this is where you left off + inventory = load_inventory(user_id) needs = [] @@ -185,7 +184,7 @@ def add( date = dt.datetime.now().date() # type: ignore else: date = date.date() # type: ignore - ordered_supps = load_ordered_supps() + ordered_supps = load_ordered_supps(user_id) order_dict = dict( name=name, quantity=quantity, @@ -204,27 +203,7 @@ def save_ordered_supps(ordered_supps: list[dict]) -> None: ORDERED_SUPPS_FP.write_text(json.dumps(ordered_supps, indent=2)) -def _prettify_json(): - ORDERED_SUPPS_FP.write_text(json.dumps(json.loads(ORDERED_SUPPS_FP.read_text()), indent=2)) - - def save_config(config: dict) -> None: SUPP_CONSUMPTION_FP.write_text(toml.dumps(config)) -def validate_matches() -> None: - missing = [] - - ordered_supp_names_lower = [i["name"].lower() for i in load_ordered_supps()] - for i in CONFIG["supps"]: - if ( - not any( - i["name"].lower() in ordered_supp_name - for ordered_supp_name in ordered_supp_names_lower - ) - and i["name"].lower() not in ALIASES_REV - ): - missing.append(i["name"]) - - if missing: - raise Missing(", ".join(missing)) diff --git a/src/sup/commands.py b/src/sup/commands.py index 51c0aab..2c0e48f 100644 --- a/src/sup/commands.py +++ b/src/sup/commands.py @@ -1,19 +1,60 @@ import datetime as dt +import typing as t from sup.sql_funcs import do_query +def set_fill_every_x_days(value: int, user_id: str) -> None: + do_query(query=f"update users set fill_every_x_days = {value} where id = '{user_id}'") + + +def set_last_fill_date(value: int, user_id: str) -> None: + do_query(query=f"update users set last_fill_date = '{value}' where id = '{user_id}'") + + def create_user(email: str, pw_hash: str, first_name: str, last_name: str, last_fill_date: dt.date, fill_every_x_days=30): - do_query( - "insert into users (id, pw_hash, first_name, last_name, last_fill_date, fill_every_x_days) values " + do_query(commit=True, + query="insert into users (id, pw_hash, first_name, last_name, last_fill_date, fill_every_x_days) values " f"('{email}', '{pw_hash}', '{first_name}', '{last_name}', '{last_fill_date}', '{fill_every_x_days}')" ) -def add_user_supp_consumption(user_id: str, product_id: int, morning: int, lunch: int, dinner: int, bedtime: int, days_per_week: int, units: str, winter_only: bool): +def add_user_supp_consumption(user_id: str, name: str, morning: int, lunch: int, dinner: int, bedtime: int, days_per_week: int, units: str, winter_only: bool): do_query( "insert into user_supplements_consumption " - "(user_id, product_id, morning, lunch, dinner, bedtime, days_per_week, units, winter_only) values (" - f"('{user_id}', {product_id}, {morning}, {lunch}, {dinner}, {bedtime}, {days_per_week}, '{units}', {winter_only}" + "(user_id, product_id, morning, lunch, dinner, bedtime, days_per_week, units, winter_only) " + f"select '{user_id}', id, {morning}, {lunch}, {dinner}, {bedtime}, {days_per_week}, '{units}', {winter_only} " + f"from products where name = '{name}'" + ) + + +def add_inventory(user_id: str, product_name: str, order_date: str, num_bottles: int) -> None: + do_query( + "insert into user_supplements_orders (user_id, product_id, order_date, num_bottles) " + f"select '{user_id}', id, '{order_date}', {num_bottles} " + f"from products where name = '{product_name}'" + ) + + +def add_product( + name: str, + quantity: int, + quantity_units: t.Literal['caps', 'mg', 'g', 'ml', 'mcg', 'iu'], + serving_units: t.Literal['caps', 'mg', 'g', 'ml', 'mcg', 'iu'], + num_units_in_serving: int, + ): + query = ( + "insert into products (name, quantity, quantity_units, serving_units, num_units_in_serving) values (" + f"'{name}', {quantity}, '{quantity_units}', '{serving_units}', {num_units_in_serving}" ")" ) + do_query(query) + + +def add_alias(user_id: str, name: str, alias: str) -> None: + query = ( + "insert into user_product_aliases (user_id, product_id, alias) " + f"select '{user_id}', id, '{alias}' " + f"from products where name = '{name}'" + ) + do_query(query) diff --git a/src/sup/main.py b/src/sup/main.py index ed34286..059f605 100644 --- a/src/sup/main.py +++ b/src/sup/main.py @@ -18,14 +18,14 @@ def load_config(): return tomllib.loads(SUPP_CONSUMPTION_FP.read_text()) -def load_ordered_supps() -> list[dict]: +def load_ordered_supps(user_id: str) -> list[dict]: return json.loads(ORDERED_SUPPS_FP.read_text()) CONFIG = load_config() ALIASES = CONFIG["product_aliases"] -ordered_supps = load_ordered_supps() +ordered_supps = load_ordered_supps(user_id) for i in CONFIG["supps"]: for ordered_supp in ordered_supps: @@ -35,9 +35,9 @@ for i in CONFIG["supps"]: ALIASES_REV = {v: k for k, v in ALIASES.items()} -def load_inventory(): +def load_inventory(user_id: str): inventory = collections.defaultdict(list) - for s in load_ordered_supps(): + for s in load_ordered_supps(user_id): if s["name"] not in CONFIG["discontinued"]: inventory[ALIASES[s["name"]]].append(s) return inventory diff --git a/src/sup/models.py b/src/sup/models.py index a2730a1..42a7363 100644 --- a/src/sup/models.py +++ b/src/sup/models.py @@ -10,7 +10,7 @@ class Supp: dinner: int | float = 0 bedtime: int | float = 0 days_per_week: int = 7 - units: t.Literal["caps", "mg", "g", "ml", "mcg", "iu"] = "mg" + units: t.Literal["caps", "mg", "g", "ml", "mcg", "mcl", "iu"] = "mg" winter_only: bool = False def __mul__(self, other: int) -> float: diff --git a/src/sup/queries.py b/src/sup/queries.py index 5fe85ed..e37b183 100644 --- a/src/sup/queries.py +++ b/src/sup/queries.py @@ -1,10 +1,59 @@ from sup.sql_funcs import do_query -class NoResult(Exception): - pass +def get_user(user_id: str): + return do_query(f"select * from users where id = '{user_id}'", get_result=True, return_dict=True)[0] # type: ignore -def get_product_id_from_alias(alias: str): - res = do_query(f"select - raise NoResult(f"no product_id for alias '{alias}'") +def product_is_in_products_table(name: str, q: int, num_units: int) -> bool: + query = f"select count(*) from products where name = '{name}' and quantity = {q} and num_units_in_serving = {num_units}" + res = do_query(get_result=True, query=query) + if res[0][0] != 1: # type: ignore + print(res[0][0]) # type: ignore + return False + return True + + +def alias_exists(name: str, alias: str, user_id: str) -> bool: + query = ( + f"select count(*) from user_product_aliases where user_id = '{user_id}' " + f"and alias = '{alias}' and product_id = (select id from products where name = '{name}')" + ) + res = do_query(get_result=True, query=query) + if res[0][0] != 1: # type: ignore + print(res[0][0]) # type: ignore + return False + return True + + +def inventory_exists(user_id: str, product_name: str, order_date: str, quantity: int, serving_q: int): + query = ( + f"select count(*) from user_supplements_orders where user_id = '{user_id}' and order_date = '{order_date}' " + "and product_id = (" + f"select distinct id from products where name = '{product_name}' and quantity = {quantity} " + f"and num_units_in_serving = {serving_q}" + ")" + ) + res = do_query(get_result=True, query=query) + if res[0][0] != 1: # type: ignore + print(res[0][0]) # type: ignore + return False + return True + + +def consumption_exists(name: str, user_id: str, morning: int, lunch: int, dinner: int, bedtime: int, days_per_week: int, units: str, winter_only: bool): + name = name.lower() + query = ( + f"select count(*) from user_supplements_consumption where user_id = '{user_id}' and morning = {morning} " + f"and lunch = {lunch} and dinner = {dinner} and bedtime = {bedtime} and days_per_week = {days_per_week} " + f"and units = '{units}' and winter_only = {winter_only} " + f"and product_id = coalesce(" + f"(select id from products where name = '{name}' limit 1), " + f"(select product_id from user_product_aliases where alias = '{name}' and user_id = '{user_id}' limit 1)" + ")" + ) + res = do_query(get_result=True, query=query) + if res[0][0] != 1: # type: ignore + print(res[0][0]) # type: ignore + return False + return True diff --git a/src/sup/scripts/create_user.py b/src/sup/scripts/create_user.py deleted file mode 100644 index 2cf2f3b..0000000 --- a/src/sup/scripts/create_user.py +++ /dev/null @@ -1,15 +0,0 @@ -import datetime as dt -import hashlib - -from sup.commands import create_user - - -if __name__ == "__main__": - email = input('email? ') - pw = input('pw? ') - first_name = input('first name? ') - last_name = input('last name? ') - pw_hash = hashlib.new("SHA256") - pw_hash.update(pw.encode()) - create_user(email, pw_hash.hexdigest(), first_name, last_name, dt.date.today(), 30) - print("okay, created user") diff --git a/src/sup/sql_funcs.py b/src/sup/sql_funcs.py index 93ef10f..377c706 100644 --- a/src/sup/sql_funcs.py +++ b/src/sup/sql_funcs.py @@ -15,7 +15,8 @@ cnx = mysql.connector.connect( host=os.getenv("PLANETSCALE_HOST"), database=DB_NAME, ) -cursor = cnx.cursor() +cursor = cnx.cursor(buffered=True) +dict_cursor = cnx.cursor(dictionary=True) def close_everything(): cnx.close() @@ -24,11 +25,18 @@ def close_everything(): atexit.register(close_everything) -def do_query(query: str) -> None: +def do_query(query: str, get_result: bool = False, commit: bool = False, return_dict: bool = False) -> list | None: + print(query) + c = cursor if not return_dict else dict_cursor try: - cursor.execute(query) + c.execute(query) except mysql.connector.Error as err: print(err.msg) raise - # TODO: return something if appropriate + if commit: + cnx.commit() + if get_result: + return cursor.fetchall() +def commit(): + cnx.commit() diff --git a/supps.toml b/supps.toml index 267de25..1155811 100644 --- a/supps.toml +++ b/supps.toml @@ -16,7 +16,7 @@ morning = 12 [[supps]] name = "b complex" -morning = 0.5 +morning = 1 days_per_week = 2 units = "caps" @@ -42,9 +42,8 @@ morning = 500 [[supps]] name = "creatine" -lunch = 2.5 +lunch = 2500 days_per_week = 4 -units = "g" [[supps]] name = "Ca-AKG" @@ -73,7 +72,7 @@ morning = 67 days_per_week = 3 [[supps]] -name = "EPA/DHA" +name = "epa/dha" morning = 1280 dinner = 640 @@ -106,8 +105,8 @@ morning = 300 [[supps]] name = "iodine" -morning = 0.126 -units = "ml" +morning = 126 +units = "mcl" [[supps]] name = "iron"