diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b3040cb --- /dev/null +++ b/.gitignore @@ -0,0 +1,163 @@ +# 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/ +share/python-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 +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints +*.ipynb + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +Pipfile.lock +Pipfile + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# 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 + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0542b31 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Ingenjörsarbete För Klimatet + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 9f7fc00..29a2d3e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,48 @@ -# co2-meal-planner -co2-meal-planner +

ifk-co2-meal-planner

+ +

+ + build + + + coverage + + + docs + + + lint + + + type + +

+ +

+ + Python: 3.9 - 3.12 + + + Testing framework: pytest + + + Linter and formatter: ruff + + + Type checker: mypy + +

+ +## Description + +Welcome to ifk-co2-meal-planner. + +## Configuration + +When you start a new project, apart from the obvious changes, +there are some details to consider. + +- Build workflow, follow the guide in [dynamic badges](https://github.com/Schneegans/dynamic-badges-action). +- To build docs, change `branch` in `github/workflows/github-action-docs.yaml` +from `["SET_TO_MAIN"]` to `["main"]`. You also need to activate Pages under repo +Settings/Pages. diff --git a/docs/assets/favicon.png b/docs/assets/favicon.png new file mode 100644 index 0000000..44ea633 Binary files /dev/null and b/docs/assets/favicon.png differ diff --git a/docs/assets/ifk_logo_black.png b/docs/assets/ifk_logo_black.png new file mode 100644 index 0000000..6f81d61 Binary files /dev/null and b/docs/assets/ifk_logo_black.png differ diff --git a/docs/assets/ifk_logo_white.png b/docs/assets/ifk_logo_white.png new file mode 100644 index 0000000..5a0816b Binary files /dev/null and b/docs/assets/ifk_logo_white.png differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..ecdd4a7 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,3 @@ +# Welcome to ifk-co2-meal-planner + +Docs for ifk-co2-meal-planner. diff --git a/material/partials/copyright.html b/material/partials/copyright.html new file mode 100644 index 0000000..d05edfe --- /dev/null +++ b/material/partials/copyright.html @@ -0,0 +1,14 @@ +{#- +This file was automatically generated - do not edit +-#} + diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..5552478 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,37 @@ +site_name: ifk-co2-meal-planner +repo_url: https://github.com/Ingenjorsarbete-For-Klimatet/ifk-co2-meal-planner +site_author: IFK +site_url: https://ingenjorsarbete-for-klimatet.github.io/ifk-co2-meal-planner/ +repo_name: Ingenjorsarbete-For-Klimatet/ifk-co2-meal-planner + +nav: +- Home: index.md + +watch: +- src + +theme: + name: material + custom_dir: material + palette: + scheme: default + primary: black + accent: light green + language: en + icon: + repo: fontawesome/brands/git-alt + logo: assets/ifk_logo_white.png + favicon: assets/favicon.png + features: + - navigation.tabs + +plugins: +- search +- mkdocstrings + +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences diff --git a/notebooks/example.py b/notebooks/example.py new file mode 100644 index 0000000..12050b0 --- /dev/null +++ b/notebooks/example.py @@ -0,0 +1,134 @@ +# %% +"""Example. + +https://www.livsmedelsverket.se/om-oss/psidata/livsmedelsdatbasenalfa + +ClimateHub have open source on CHG, but only free for internal use. +""" + +import pandas as pd + +# from IPython.display import display +from ifk_co2_meal_planner.rdi import rdi +from ifk_co2_meal_planner.slv_wrapper import SlvWrapper + +slv = SlvWrapper() + + +# %% +slv.search_food("paranöt") + +menu = { + "Mango": {"number": 574, "weight": 100}, + "cashew": {"number": 1557, "weight": 65 / 4}, + "hirs": {"number": 834, "weight": 1180.0 / 5}, + "kikärtor": {"number": 3815, "weight": 250 / 4}, + "basilika": {"number": 379, "weight": 3}, + "apelsin": {"number": 551, "weight": 80}, +} + +slv.get_minerals_from_number(1568) + + +# %% + +mineral_dict = slv.populate_mineral_dict(menu) +mineral_df = slv.mineral_dict_to_df(mineral_dict) +mineral_df = slv.convert_betakaroten_to_retinol(mineral_df) +print(mineral_df) + +mineral_df = slv.calculate_minerals_for_weight(mineral_df) + +print(mineral_df) + +# calculate total +mineral_df.loc["total"] = mineral_df.sum() + +# create an own class for rdi? +rdi_dict = slv.init_mineral_dict() +for gen in rdi["Zink, Zn"].keys(): + for col in rdi_dict.keys(): + if col in rdi.keys(): + rdi_dict[col].append(rdi[col][gen]) + elif col == "name": + rdi_dict["name"].append(gen) + else: + rdi_dict[col].append(None) + +rdi_df = pd.DataFrame.from_dict(rdi_dict) +rdi_df.set_index("name", inplace=True) +mineral_df = pd.concat([mineral_df, rdi_df]) + +pd.set_option("display.max_columns", 500) +print(mineral_df) + + +# %% + +for gen in rdi["Zink, Zn"].keys(): + mineral_df.loc[gen + " percentage"] = rdi_per = ( + mineral_df.loc["total"] / mineral_df.loc[gen] * 100 + ) + +# ax = mineral_df['total'].plot.bar(rot=0) +rdi_per_labels = ["female percentage", "male percentage", "4-6 years percentage"] +ax = mineral_df.loc[rdi_per_labels].dropna(axis=1, how="all").transpose().plot.barh() +ax.legend(bbox_to_anchor=(1.42, 1)) +ax.axvline(x=100) +ax.set_xlim([0, 100]) +ax.grid() + + +# %% +# get classifications +a = slv.get_ingredients_from_number(300) + +classifications = [] +for i in range(1, 70): + classification = slv.get_calssification_from_number(i) + try: + if len(slv.get_ingredients_from_number(i)) == 1: + if classification[-1]["kod"] not in classifications: + classifications.append(classification[-1]["kod"]) + except Exception: + print("did not find number: ", i) + +for x in classifications: + print(x) + +# %% +menu = { + "Mango": {"number": 574, "weight": 50}, + "Cashewnötter rostade u. salt": {"number": 1557, "weight": 20}, + "Hirs kokt m. salt fullkorn": {"number": 834, "weight": 100}, + "Potatisgratäng m. lättmjölk ost mager hemlagad": {"number": 279, "weight": 300}, +} + +# breakdown to ingredients +menu_ingredients = {} +for key, value_dict in menu.items(): + ingredients = slv.get_ingredients_from_number(value_dict["number"]) + if len(ingredients) == 1: + if key in menu_ingredients.keys(): + menu_ingredients[key]["weight"] = ( + menu_ingredients[key]["weight"] + value_dict["weight"] + ) + else: + menu_ingredients[key] = value_dict + else: + total_weight = value_dict["weight"] + for food in ingredients: + number = slv.get_number_from_name(food["namn"]) + weight = (total_weight * food["andel"]) / 100 + if food["namn"] in menu_ingredients.keys(): + menu_ingredients[food["namn"]]["weight"] = ( + menu_ingredients[food["namn"]]["weight"] + weight + ) + else: + menu_ingredients[food["namn"]] = {"number": number, "weight": weight} + + +# %% + + +a = slv.search_food("Mjölk") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ea97c17 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,56 @@ +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[project] +name = "ifk-co2-meal-planner" +version = "0.1.0" +description = "For evaluation of nutrition and CHG of meals." +license = { text = "MIT" } +readme = "README.md" +authors = [ + { name = "IFK", email = "info@ingenjorsarbeteforklimatet.se" }, +] +requires-python = ">=3.9" +dependencies = ["requests==2.31.0", + "pandas==2.2.0", + "matplotlib==3.8.2", + "pyarrow==15.0.0" +] + +[project.optional-dependencies] +lint = [ + "ruff ~= 0.1", +] +type = ["mypy ~= 1.7", "types-requests ~= 2.28", "pandas-stubs ~= 1.5"] +test = ["pytest ~= 7.1", "coverage ~= 6.5", "pytest-cov ~= 4.0"] +doc = [ + "mkdocs ~= 1.4", + "mkdocs-material ~= 8.5", + "mkdocstrings[python] ~= 0.19", +] +dev = [ + "ifk-co2-meal-planner[lint]", + "ifk-co2-meal-planner[type]", + "ifk-co2-meal-planner[test]", + "ifk-co2-meal-planner[doc]", + "pre-commit ~= 2.20", + "ipykernel ~= 6.26", +] + +[tool.setuptools.packages.find] +where = ["src"] +exclude = ["material"] + +[tool.ruff] +line-length = 88 +extend-include = ["*.ipynb"] + +[tool.ruff.lint] +select = ["E4", "E7", "E9", "W", "F", "B", "D", "I", "S"] +ignore = ["D203", "D213", "S101"] +unfixable = ["B", "S"] +pydocstyle.convention = "google" + +[tool.mypy] +ignore_missing_imports = true diff --git a/src/ifk_co2_meal_planner/rdi.py b/src/ifk_co2_meal_planner/rdi.py new file mode 100644 index 0000000..fcb3a4a --- /dev/null +++ b/src/ifk_co2_meal_planner/rdi.py @@ -0,0 +1,24 @@ +"""RDI list.""" + +# Zink ett intag på 25-30% högre rekommenderas vid veg kost + +rdi = { + "Zink, Zn": {"male": 12.7, "female": 9.7, "4-6 years": 5.8}, + "Vitamin E": {"male": 11, "female": 10, "4-6 years": 8}, + "Vitamin D": {"male": 10, "female": 10, "4-6 years": 10}, + "Vitamin C": {"male": 110, "female": 95, "4-6 years": 35}, + "Vitamin B6": {"male": 1.8, "female": 1.6, "4-6 years": 0.7}, + "Vitamin B12": {"male": 4, "female": 4, "4-6 years": 1.7}, + "Tiamin": {"male": 0.1, "female": 0.1, "4-6 years": 0.1}, + "Selen, Se": {"male": 90, "female": 75, "4-6 years": 25}, + "Riboflavin": {"male": 1.6, "female": 1.6, "4-6 years": 0.7}, + "Retinol": {"male": 800, "female": 700, "4-6 years": 350}, + "Fosfor, P": {"male": 520, "female": 520, "4-6 years": 440}, + "Niacinekvivalenter": {"male": 1.6, "female": 1.6, "4-6 years": 1.6}, + "Magnesium, Mg": {"male": 350, "female": 300, "4-6 years": 230}, + "Kalium, K": {"male": 3500, "female": 3500, "4-6 years": 1150}, + "Jod, I": {"male": 150, "female": 150, "4-6 years": 100}, + "Folat": {"male": 330, "female": 330, "4-6 years": 140}, + "Järn, Fe": {"male": 9, "female": 15, "4-6 years": 7}, + "Kalcium, Ca": {"male": 950, "female": 950, "4-6 years": 800}, +} diff --git a/src/ifk_co2_meal_planner/slv_wrapper.py b/src/ifk_co2_meal_planner/slv_wrapper.py new file mode 100644 index 0000000..218612a --- /dev/null +++ b/src/ifk_co2_meal_planner/slv_wrapper.py @@ -0,0 +1,160 @@ +""""Request data from livsmedelsverket.""" + +import pandas as pd +import requests + + +class SlvWrapper: + """Class for fetcing and process slv data.""" + + def __init__(self) -> None: + """Initialization.""" + self.version = 1 + list_of_foods_url = f"https://dataportal.livsmedelsverket.se/livsmedel/api/v{self.version}/livsmedel?offset=0&limit=2556&sprak=1" + all_foods = requests.get(list_of_foods_url) # noqa: S113 + self.all_foods = all_foods.json()["livsmedel"] + + def search_food(self, food: str) -> None: + """Search for string in slv. + + Args: + food: name of food to search for. + """ + for livsmedel in self.all_foods: + if food.lower() in livsmedel["namn"].lower(): + print(livsmedel["namn"], livsmedel["nummer"]) + + pass + + def get_number_from_name(self, food: str) -> int: + """Get number from name. + + Args: + food: name of food to search for. + + Retruns: + number of food. + """ + number = -1 + for livsmedel in self.all_foods: + if food == livsmedel["namn"]: + number = livsmedel["nummer"] + break + + return number + + def get_minerals_from_number(self, number: int) -> dict: + """Get minerals and vitamins for given number. + + Args: + number: number corresponding to a specific food + + Returns: + dict with mineals etc corresponding to food number + """ + url = f"https://dataportal.livsmedelsverket.se/livsmedel/api/v{self.version}/livsmedel/{number}/naringsvarden" + temp_minerals = requests.get(url).json() # noqa: S113 + return temp_minerals + + def get_ingredients_from_number(self, number: int) -> dict: + """Get ingredients for given number. + + Args: + number: number corresponding to a specific food + + Returns: + dict with ingredients + """ + url = f"https://dataportal.livsmedelsverket.se/livsmedel/api/v{self.version}/livsmedel/{number}/ravaror" + ingredients = requests.get(url).json() # noqa: S113 + return ingredients + + def get_classification_from_number(self, number: int) -> dict: + """Get classification from number. + + Args: + number: number corresponding to a specific food + + Returns: + dict classification + """ + url = f"https://dataportal.livsmedelsverket.se/livsmedel/api/v{self.version}/livsmedel/{number}/klassificeringar" + classification_from_number = requests.get(url).json() # noqa: S113 + return classification_from_number + + def init_mineral_dict(self) -> dict: + """Initialization of mineral dict. + + Returns: + dict with keys corresponding to minerals etc. + """ + temp = self.get_minerals_from_number(1) + mineral_dict: dict = {mineral["namn"]: [] for mineral in temp} + mineral_dict["name"] = [] + mineral_dict["weight"] = [] + return mineral_dict + + def populate_mineral_dict(self, menu: dict) -> dict: + """Populate mineral dict with minearls from set of foods. + + Args: + menu: dict with foods on format {name: {number: x, weight: y}} + + Returns: + dict with minerals etc (/100g) corresponding to menu + + """ + mineral_dict = self.init_mineral_dict() + for key, item in menu.items(): + mineral_dict["name"].append(key) + mineral_dict["weight"].append(item["weight"]) + temp_minerals = self.get_minerals_from_number(item["number"]) + for mineral in temp_minerals: + mineral_dict[mineral["namn"]].append(mineral["varde"]) + + return mineral_dict + + def mineral_dict_to_df(self, mineral_dict: dict) -> pd.DataFrame: + """Convert minearl dict to dataframe. + + Args: + mineral_dict: dict with minearals etc. + + Returns: + Dataframe with minerals etc. + """ + mineral_df = pd.DataFrame.from_dict(mineral_dict) + mineral_df.set_index("name", inplace=True) + mineral_df = mineral_df.astype(float) + return mineral_df + + def convert_betakaroten_to_retinol(self, mineral_df: pd.DataFrame) -> pd.DataFrame: + """Convert betakaroten to retinol. + + Factor according to slv. + + Args: + mineral_df: dataframe with minerals etc. + + Returns: + Dataframe with Retinol modified. + """ + factor = 12 + mineral_df["Retinol"] = ( + mineral_df["Retinol"] + mineral_df["Betakaroten/β-Karoten"] / factor + ) + + return mineral_df + + def calculate_minerals_for_weight(self, mineral_df: pd.DataFrame) -> pd.DataFrame: + """Adjust minerals by weight. + + Args: + mineral_df: dataframe with minerals etc + + Returns: + dataframe with minerals etc adjusted. + """ + for index, row in mineral_df.iterrows(): + mineral_df.loc[index] = mineral_df.loc[index] * row["weight"] / 100.0 + return mineral_df diff --git a/tests/test_unit.py b/tests/test_unit.py new file mode 100644 index 0000000..2eca37e --- /dev/null +++ b/tests/test_unit.py @@ -0,0 +1,8 @@ +"""Temporary unit test.""" + +from co2_meal_planner.slv_wrapper import temp + + +def test_unit_temp(): + """Unit test of temp function.""" + assert temp() is True