Skip to content

Commit

Permalink
Nutrition Label (#282)
Browse files Browse the repository at this point in the history
* WIP

* Working solution. Pending translation and tests.

* Fixed method signature

* Updated headers

* Another try

* Fixed more tests

* Update edit.vue

* Fixed tests

* Cleanup

* coverage tool

* Fixed some tests

* Fixed more tests

* More tests and UI fixes

* Added missing translation entry

* More fixes

* Fixed tests

* Final touches
  • Loading branch information
jlucaspains authored Mar 24, 2024
1 parent 8136c57 commit a845c5d
Show file tree
Hide file tree
Showing 28 changed files with 3,500 additions and 116 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ jobs:
run: |
cd api
python -m pip install --upgrade pip
pip install pytest
pip install pytest pytest-cov
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Test with pytest
run: |
cd api
pytest
pytest --cov
build_and_deploy_job:
if: github.event_name == 'pull_request' && github.event.action != 'closed'
Expand Down
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,8 @@ node_modules

playwright-report
test-results
salt
salt

notebooks/**/*.csv
notebooks/**/*.json
notebooks/**/*.zip
21 changes: 21 additions & 0 deletions api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,25 @@ class ImageResult(BaseModel):
},
},
"required": ["title", "ingredients", "steps"],
}


calcNutritionSchema = {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "string", "minLength": 1},
"quantity": {"type": "number", "minimum": 0},
"unit": {"type": "string", "minLength": 1},
},
"required": ["id", "quantity", "unit"],
}
}

lookupIngredientsSchema = {
"type": "array",
"items": {
"type": "string",
}
}
61 changes: 51 additions & 10 deletions api/parse_recipe/main.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import logging
import json
import re

import azure.functions as func
from contextlib import suppress

from recipe_scrapers import scrape_me
from recipe_scrapers import scrape_me, _abstract
from pint import UnitRegistry
from uuid import uuid4
from time import perf_counter

from ..util import parse_recipe_ingredient, parse_recipe_instruction, parse_recipe_image
from ..models import Recipe

_abstract.HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/123.0"
}

ureg = UnitRegistry()

Expand All @@ -28,20 +33,24 @@ def main(req: func.HttpRequest) -> func.HttpResponse:

ingredients = map(lambda x: parse_recipe_ingredient(x, lang, ureg), scraper.ingredients())
instructions = map(lambda x: parse_recipe_instruction(x, lang), scraper.instructions_list())

yields, yields_description = parse_yields(scraper.yields())
result = {
"title": scraper.title(),
"totalTime": scraper.total_time(),
"yields": scraper.yields(),
"yields": yields,
"yieldsDescription": yields_description,
"ingredients": list(ingredients),
"steps": list(instructions),
"image": scraper.image(),
"host": scraper.host()
"host": scraper.host(),
"language": scraper.language()
}

if download_image:
image_uri = parse_recipe_image(result["image"])
result["image"] = image_uri

# since nutrients are not always available, we need to suppress the exception
with suppress(NotImplementedError):
result["nutrients"] = parse_nutrients(scraper.nutrients())

result["image"] = parse_recipe_image(result["image"]) if download_image else result["image"]

return func.HttpResponse(json.dumps(result), status_code=200, mimetype="application/json")
except Exception as e:
Expand All @@ -50,4 +59,36 @@ def main(req: func.HttpRequest) -> func.HttpResponse:
return func.HttpResponse("Could not find a recipe in the web page", status_code=400)
finally:
end = perf_counter()
logging.info(f"Finished processing parse request id {correlation_id}. Time taken: {end - start:0.4f}s")
logging.info(f"Finished processing parse request id {correlation_id}. Time taken: {end - start:0.4f}s")

def parse_nutrients(nutrients: dict):
return {
"calories": parse_nutrient_value(nutrients.get("calories")),
"totalFat": parse_nutrient_value(nutrients.get("fatContent")),
"saturatedFat": parse_nutrient_value(nutrients.get("saturatedFatContent")),
"unsaturatedFat": parse_nutrient_value(nutrients.get("unsaturatedFatContent")),
"transFat": parse_nutrient_value(nutrients.get("transFatContent")),
"carbohydrates": parse_nutrient_value(nutrients.get("carbohydrateContent")),
"sugar": parse_nutrient_value(nutrients.get("sugarContent")),
"cholesterol": parse_nutrient_value(nutrients.get("cholesterolContent")),
"sodium": parse_nutrient_value(nutrients.get("sodiumContent")),
"protein": parse_nutrient_value(nutrients.get("proteinContent")),
"fiber": parse_nutrient_value(nutrients.get("fiberContent"))
}

def parse_yields(yields: str):
if not yields:
return 0, ""

parts = yields.split(" ")

return float(parts[0]), parts[1] if len(parts) > 1 else ""

def parse_nutrient_value(value: str) -> float:
if not value:
return 0

qty_re = re.search(r"^(?P<Value>\d{1,5})", value)
qty = qty_re.group("Value")

return float(qty) if qty else 0
3 changes: 2 additions & 1 deletion api/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ pillow-avif-plugin>=1.4.3
pytest
openai
azure-cosmos
jsonschema
jsonschema
azure-search-documents
9 changes: 9 additions & 0 deletions api/test/test_parse_recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ def test_recipe_parse():

assert parsed_response["image"].startswith("http")

assert parsed_response["language"] == "en"

assert parsed_response["nutrients"]["calories"] == 423
assert parsed_response["nutrients"]["totalFat"] == 19
assert parsed_response["nutrients"]["saturatedFat"] == 6
assert parsed_response["nutrients"]["carbohydrates"] == 39
assert parsed_response["nutrients"]["protein"] == 26
assert parsed_response["nutrients"]["cholesterol"] == 83
assert parsed_response["nutrients"]["sodium"] == 832

def test_recipe_parse_download_image():
request = func.HttpRequest(
Expand Down
13 changes: 13 additions & 0 deletions api/tools/calcNutrition.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// 200
POST http://localhost:7071/api/calc-nutrition HTTP/1.1
content-type: application/json

[
{"quantity": 100, "unit": "gram", "id": "748967"}
]

###
{"quantity": 100, "unit": "gram", "id": "2% milk"},
{"quantity": 100, "unit": "gram", "id": "bread flour"},
{"quantity": 100, "unit": "gram", "id": "table salt"},
{"quantity": 100, "unit": "gram", "id": "sugar"}
4 changes: 4 additions & 0 deletions api/tools/lookupIngredients.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
POST http://localhost:7071/api/lookup-ingredients
Content-Type: application/json

["bread flour", "eggs"]
7 changes: 7 additions & 0 deletions api/tools/parse_recipe.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
POST http://localhost:7071/api/parse-recipe
Content-Type: application/json

{
"url": "https://www.allrecipes.com/recipe/98554/brazilian-cheese-bread-pao-de-queijo/",
"downloadImage": false
}
2 changes: 1 addition & 1 deletion api/shareRecipe.http → api/tools/shareRecipe.http
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,4 @@ content-type: application/json

{
"code": "YKYKSF"
}
}
Loading

0 comments on commit a845c5d

Please sign in to comment.