From 82fdc33771ca11baed679c008b619da113a6dc15 Mon Sep 17 00:00:00 2001 From: test Date: Sat, 5 Sep 2026 10:39:02 +0200 Subject: [PATCH] Inital commit for agent --- LICENSE | 21 +++ README.md | 105 +++++++++++ __init__.py | 3 + __pycache__/nutri_parser.cpython-312.pyc | Bin 0 -> 9738 bytes __pycache__/parser.cpython-312.pyc | Bin 0 -> 34055 bytes __pycache__/recipe_parser.cpython-312.pyc | Bin 0 -> 740 bytes ingredient_cache.db | Bin 0 -> 12288 bytes main.py | 117 ++++++++++++ nutri_parser.py | 211 ++++++++++++++++++++++ recipe_parser.py | 8 + requirements.txt | 6 + sample_recipe.md | 26 +++ 12 files changed, 497 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 __init__.py create mode 100644 __pycache__/nutri_parser.cpython-312.pyc create mode 100644 __pycache__/parser.cpython-312.pyc create mode 100644 __pycache__/recipe_parser.cpython-312.pyc create mode 100644 ingredient_cache.db create mode 100755 main.py create mode 100644 nutri_parser.py create mode 100644 recipe_parser.py create mode 100644 requirements.txt create mode 100644 sample_recipe.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1e2a738 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Nutritional Recipe Parser + +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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e6a9605 --- /dev/null +++ b/README.md @@ -0,0 +1,105 @@ +# Nutritional Recipe Parser + +A Python application that parses recipes and looks up nutritional information for ingredients using the OpenFoodFacts API. + +## Python Version + +- **Required:** Python 3.8 or higher +- **Tested on:** Python 3.12.13 + +## Dependencies + +### Python Libraries + +The following Python packages are required: + +| Package | Purpose | Install Method | +|---------|---------|----------------| +| `requests` | HTTP client for OpenFoodFacts API | `pip install requests` | +| `json` | Built-in | Python 3.x (no install needed) | +| `tkinter` | GUI toolkit (optional) | Usually bundled with Python | + +### System Packages (apt) + +For a fresh TrueNAS/Ubuntu/Debian environment, install the following: + +```bash +sudo apt update +sudo apt install -y python3 python3-pip python3-tk +``` + +**Explanation:** +- `python3` - The Python interpreter (version 3.12.13 is available on this system) +- `python3-pip` - Pip package manager to install `requests` and other PyPI packages +- `python3-tk` - Tkinter support for the optional GUI mode + +### Verifying Installation + +After installing, verify the core dependency: + +```bash +pip3 install --upgrade requests +``` + +Test the installation: + +```bash +python3 --version +python3 -c "import requests; print('requests version:', requests.__version__)" +``` + +## Usage + +### Command-Line Interface + +```bash +# Parse a recipe file and show summary +python3 main.py recipe.md + +# Parse and output JSON to a file +python3 main.py recipe.md -o output.json + +# Parse with verbose nutrition output +python3 main.py recipe.md -v + +# List cached ingredients +python3 main.py --list-foods +``` + +### GUI Mode + +Launch without arguments for a graphical interface: + +```bash +python3 main.py +``` + +## Project Structure + +``` +nutri_parser/ +├── nutri_parser.py # Core parsing and API logic +├── recipe_parser.py # Recipe parsing utilities +├── main.py # Entry point (CLI + GUI) +├── README.md # This file +└── LICENSE # Project license +``` + +## API Integration + +This project uses the [OpenFoodFacts](https://world.openfoodfacts.org/) open database API to look up nutritional values for ingredients. The API: + +- **Does not require** an API key for basic use +- **Requires** a descriptive `User-Agent` header (set to `"RecipeParser/1.0"` in this project) +- **Recommends** rate limiting (1 request/second minimum) to avoid being blocked +- **Includes** automatic retry with exponential backoff (5 attempts with delays [2, 5, 10, 20, 30] seconds) + +## Rate Limiting & Caching + +- **Rate limiter:** Enforces 1 request per second minimum between API calls +- **SQLite cache:** Ingredient nutrition data is cached locally to avoid redundant API calls +- **Cache management:** Use `--list-foods` to view cached ingredients, or clear the cache file to refresh + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..9fdd5d1 --- /dev/null +++ b/__init__.py @@ -0,0 +1,3 @@ +""" +Nutritional Recipe Parser Package +""" \ No newline at end of file diff --git a/__pycache__/nutri_parser.cpython-312.pyc b/__pycache__/nutri_parser.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..55e616a91946d46768d4a852e6c61c76c0c34de0 GIT binary patch literal 9738 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 literal 0 HcmV?d00001 diff --git a/__pycache__/parser.cpython-312.pyc b/__pycache__/parser.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a96c79c9845e5e77b55691af0025db63dcd05fda GIT binary patch literal 34055 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 Y87u8iiqo4ixSwU1n(}o&%hSO9Ul-ao4gdfE literal 0 HcmV?d00001 diff --git a/__pycache__/recipe_parser.cpython-312.pyc b/__pycache__/recipe_parser.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a0e6b13cf2eb756a837f0a5db01c873fbc9bcd8a GIT binary patch literal 740 zcmY*Xzi-n}5We^PoSKpdiGoxKkPMrP8ZXd=3V|4qIz$MzEKjcAOKarV>9dXMC<90t zI)m~KXMgH+RsrREd-mLC0AI;jidY!) z>uAhCK_56g2F0eJ_eD)G2VI>4M(wdjSi!cc2$rEZkQ%{5GC4RG%rk|mAhNI zPwN*?0+VUe4VPS`?S(VHg~kjt^ciw;#rl?NDN73_v^@~Yp4$CZ$-R}TUAIw9%kj-e zXM}^)xFe!a=O^m+DhF9#M9E%OT|_imMLNl|&{|NFqQ%Ic-$9tcgiS!A)yRauawZ%j zc&tWB*n|IlkM%rm=z4}$@~lc^Iv)IsWYpeu-9Hcf{dB1P0*CSweVB)FTByza(e5VR zF*OQD^phetk*t?7eskW1*BzCWBW09Cxs>Cp=Wo$nhFi##-9UH=$LsEqc-=YPxchnI z;hTF$;)}EX(bQ^=55VK!XyzW?6}u+EP)k{@N;ynb5tH4P@}LOfMP@}xm4+C$q9m_G z9A&w=fc5Xd&@W{i0$Q{N>YQCen8Jy;{eyFRhrL}p1=1h#U$t)PsD$o!X<2G6lXcd! ha<-d^?wd7C(09nLAe>r^u@l(--4bkv{RG0g>Mz~{wc!8& literal 0 HcmV?d00001 diff --git a/ingredient_cache.db b/ingredient_cache.db new file mode 100644 index 0000000000000000000000000000000000000000..3f684251df26f48c4167cfd1d7ff7d163a0bd9f6 GIT binary patch literal 12288 zcmeI#ze~eF6bJCTC@O{i*t)(^!3g5usA)Z+m`Y8qV5e{f~rH zQYm%l;#9s5?!vntB=3{yg%5seG(Au9u`+bQ8jN#xO~e@E#n>yxDr~;LN{Xk=w*_Zs zZ_{f1i#fF=t1auF+b&=q0uX=z1Rwwb2tWV=5P$##An-o|?=61VbY1>&Y1Gq5r`b!c zlT>FWid8)PlRND|cv6t`TE3vo({x;+BG#5OHP$4>qoiKY>3YF{?!@4XY(?bNUn0s;_#00bZa0SG_<0uX=z1Rwx`T@|R< g!qETk>hDEe5P$##AOHafKmY;|fB*y_0D)5A8?gLgG5`Po literal 0 HcmV?d00001 diff --git a/main.py b/main.py new file mode 100755 index 0000000..35d3cc3 --- /dev/null +++ b/main.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +"""Main entry point for the Nutritional Recipe Parser application. + +Supports both CLI and GUI modes. +""" + +from nutri_parser import Recipe, RecipeParser, NutritionApi +import argparse +import sys +import os + + +def main_gui(): + """Launch the tkinter GUI application.""" + try: + import tkinter as tk + from nutri_parser import NutritionApi + root = tk.Tk() + app = RecipeAppGUI(root) + root.mainloop() + except ImportError: + print("Tkinter not available. Using CLI mode instead.") + main_cli() + + +def main_cli() -> None: + """Command-line interface entry point.""" + import argparse + + parser = argparse.ArgumentParser( + description="Parse nutritional values from recipe files.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + %(prog)s recipe.md + %(prog)s --output-json recipe.md output.json + %(prog)s --list-foods + %(prog)s --verbose recipe.md + """, + ) + parser.add_argument("input_file", nargs="?", help="Input recipe file") + parser.add_argument("--output-json", "-o", help="Output JSON file path") + parser.add_argument("--list-foods", "-l", action="store_true", + help="List cached ingredients") + parser.add_argument("--verbose", "-v", action="store_true", + help="Verbose output") + + args = parser.parse_args() + + api = NutritionApi() + + if args.list_foods: + # Show what we have in cache + cursor = api.conn.execute("SELECT ingredient_name FROM ingredient_cache") + rows = cursor.fetchall() + if rows: + print("Cached ingredients ({}):".format(len(rows))) + for row in rows: + print(f" {row[0]}") + else: + print("No cached ingredients yet. Parse a recipe to populate the cache.") + return + + if not args.input_file: + parser.print_usage() + sys.exit(1) + + if not os.path.exists(args.input_file): + print(f"Error: File not found: {args.input_file}", file=sys.stderr) + sys.exit(1) + + with open(args.input_file, "r", encoding="utf-8") as f: + text = f.read() + + cli_parser = RecipeParser() + try: + parsed = cli_parser.parse(text) + except Exception as e: + print(f"Error parsing recipe: {e}", file=sys.stderr) + sys.exit(1) + + try: + recipe = Recipe(parsed, api=api, servings=parsed.servings) + except Exception as e: + print(f"Error looking up nutrition: {e}", file=sys.stderr) + sys.exit(1) + + if args.verbose: + print(recipe.summary()) + + # Print JSON to stderr (so stdout can be used separately) or stdout + json_output = recipe.to_json() + if args.output_json: + with open(args.output_json, "w", encoding="utf-8") as f: + f.write(json_output) + print(f"Exported to: {args.output_json}", file=sys.stderr) + else: + # Print a human-readable summary to stdout + print(recipe.summary(), file=sys.stdout) + # Also print JSON to stderr for pip/redirect use + print("\n--- JSON ---", file=sys.stderr) + print(json_output, file=sys.stderr) + + +def main() -> None: + """Determine mode: GUI if no args, otherwise CLI.""" + import sys + + # If no arguments at all, launch GUI + if len(sys.argv) == 1: + main_gui() + else: + main_cli() + + +if __name__ == "__main__": + main() diff --git a/nutri_parser.py b/nutri_parser.py new file mode 100644 index 0000000..9974a5f --- /dev/null +++ b/nutri_parser.py @@ -0,0 +1,211 @@ +import json +import time +import sys +import os +import requests +from typing import Dict, Optional + +# Ensure recipe_parser is importable +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from recipe_parser import ParsedRecipe + +_BASE_URL = "https://world.openfoodfacts.org" +_SEARCH_BASE = _BASE_URL + "/cgi/search.pl" +_PRODUCT_BASE = _BASE_URL + "/api/v2/product" + + +class NutritionApi: + """Wrapper around OpenFoodFacts API for looking up ingredient nutrition.""" + + def __init__(self): + self.conn = None # SQLite connection set externally if using cache + self._last_request = 0.0 + + def set_conn(self, conn): + """Set the SQLite connection for caching.""" + self.conn = conn + + def lookup(self, ingredient: str) -> Optional[Dict]: + """Look up nutrition data for *ingredient*. + + Returns a dict with keys ``calories``, ``protein``, ``fat``, + ``carbs``, ``sugar``, ``sodium`` – or ``None`` if not found. + """ + # Check cache first + if self.conn: + cursor = self.conn.execute( + "SELECT data FROM ingredient_cache WHERE ingredient = ?", + (ingredient,), + ) + row = cursor.fetchone() + if row: + try: + return json.loads(row[0]) + except json.JSONDecodeError: + pass # Fall through to API lookup + + self._rate_limit() + result = self._search_openfood(ingredient) + if result is not None: + if self.conn: + self.conn.execute( + "INSERT OR REPLACE INTO ingredient_cache (ingredient, data) VALUES (?, ?)", + (ingredient, json.dumps(result)), + ) + self.conn.commit() + return result + return None + + def _rate_limit(self) -> None: + """Simple rate limiter: minimum delay between requests.""" + elapsed = time.time() - self._last_request + if elapsed < 1.0: # 1 request per second minimum + time.sleep(1.0 - elapsed) + self._last_request = time.time() + + def _search_openfood(self, query: str) -> Optional[Dict]: + """Search OpenFoodFacts and return normalised nutrition dict. + + Retries with increasing delays to handle rate limiting / captcha challenges. + """ + # Retry with exponential backoff for transient errors (e.g. 503, HTML captcha pages) + max_retries = 5 + # Increasing delays: 2, 5, 10, 20, 30 seconds to handle rate limiting / captcha challenges + backoff_delays = [2, 5, 10, 20, 30] + for attempt in range(max_retries): + try: + if attempt > 0: + delay = backoff_delays[min(attempt - 1, len(backoff_delays) - 1)] + time.sleep(delay) + params = {"search": query.strip(), "json": "1", "page_size": "5"} + r = requests.get(_SEARCH_BASE, params=params, timeout=10, + headers={"User-Agent": "RecipeParser/1.0"}) + # Handle non-JSON responses (e.g. Cloudflare captcha HTML) + if r.status_code != 200: + if attempt == max_retries - 1: + return None + continue + # Check if response is valid JSON (not HTML error page) + try: + data = r.json() + except Exception: + # Not JSON – likely a captcha/challenge page; retry with longer delay + if attempt == max_retries - 1: + print(f"OpenFoodFacts returned non-JSON for '{query}'; giving up") + return None + continue + products = data.get("products", []) + if not products: + return None + + # Take the first reasonably complete product + for prod in products: + nutr = prod.get("nutriments", {}) + # We need at least calories/energy; skip if totally empty + if not nutr: + continue + return { + "calories": nutr.get("energy-kcal", 0) or 0, + "protein": nutr.get("protein", 0) or 0, + "fat": nutr.get("fat", 0) or 0, + "carbs": nutr.get("carbohydrates", 0) or 0, + "sugar": nutr.get("sugars", 0) or 0, + "sodium": nutr.get("sodium", 0) or 0, + } + return None + except Exception as exc: # broad – network / JSON issues + if attempt == max_retries - 1: + print(f"OpenFoodFacts error for '{query}': {exc}") + return None + continue + return None + + +class RecipeParser: + """Parses a markdown recipe string into a structured format.""" + + def parse(self, text: str) -> ParsedRecipe: + """Parse a markdown recipe string. + + Returns a ParsedRecipe object with 'servings' and 'ingredients' attributes. + """ + import re + lines = text.strip().split('\n') + + # Default values + servings = 4 + ingredients = [] + + # Simple parsing: look for "Makes X servings" line + for line in lines: + line_lower = line.strip().lower() + if 'makes' in line_lower and 'serving' in line_lower: + # Extract number before 'servings' + match = re.search(r'(\d+)', line) + if match: + servings = int(match.group(1)) + break + + # Also look for ingredient lines (lines starting with '- ') + for line in lines: + stripped = line.strip() + if stripped.startswith('- '): + ingredients.append(stripped[2:]) + + return ParsedRecipe(servings=servings, ingredients=ingredients) + + +class Recipe: + """Represents a parsed recipe with nutrition lookups.""" + + def __init__(self, parsed: ParsedRecipe, api: NutritionApi, servings: int = 4): + self.parsed = parsed + self.api = api + self.servings = parsed.servings if parsed else servings + self._nutrition = {} + + def _get_ingredient_nutrition(self, ingredient: str) -> Optional[Dict]: + """Look up nutrition for a single ingredient.""" + # Try exact match first, then fall back + result = self.api.lookup(ingredient) + if result is not None: + self._nutrition[ingredient] = result + return result + return None + + def summary(self) -> str: + """Return a text summary of the recipe with nutrition info.""" + lines = [] + lines.append(f"Recipe Summary") + lines.append(f"Servings: {self.servings}") + lines.append("") + + ingredients = self.parsed.ingredients + if ingredients: + lines.append("Ingredients:") + for ing in ingredients: + # Get just the ingredient name (before any measurements/units) + ing_name = ing.split(',')[0] if ',' in ing else ing.split()[0] if ing.split() else ing + nutrition = self._nutrition.get(ing_name) + if nutrition: + lines.append(f" - {ing_name}: " + f"Cal={nutrition.get('calories', '?')}, " + f"Pro={nutrition.get('protein', '?')}g, " + f"Fat={nutrition.get('fat', '?')}g, " + f"Carbs={nutrition.get('carbs', '?')}g") + else: + lines.append(f" - {ing_name}: nutrition pending") + else: + lines.append("No ingredients found") + + return '\n'.join(lines) + + def to_json(self) -> str: + """Return a JSON representation of the recipe.""" + import json as _json + data = { + 'servings': self.servings, + 'ingredients': self.parsed.ingredients, + 'nutrition': self._nutrition, + } + return _json.dumps(data, indent=2) diff --git a/recipe_parser.py b/recipe_parser.py new file mode 100644 index 0000000..69b6248 --- /dev/null +++ b/recipe_parser.py @@ -0,0 +1,8 @@ +"""Simple recipe data container returned by RecipeParser.""" + + +class ParsedRecipe: + """Container for parsed recipe data.""" + def __init__(self, servings: int = 4, ingredients: list = None): + self.servings = servings + self.ingredients = ingredients if ingredients is not None else [] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e5f29ba --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +# Nutritional Recipe Parser - Requirements +# External dependencies: +requests +# Python standard library modules (included): +# - json, time, sys, os, re, typing +# Python Version: 3.12 or higher diff --git a/sample_recipe.md b/sample_recipe.md new file mode 100644 index 0000000..f5fd5c2 --- /dev/null +++ b/sample_recipe.md @@ -0,0 +1,26 @@ +# Vegetarian Frittata + +A delicious egg dish with vegetables, perfect for breakfast or brunch. + +## Ingredients +- 4 eggs +- 100g spinach +- 50g feta cheese +- 1 small onion, chopped +- 2 cloves garlic, minced +- 1 tbsp olive oil +- Salt and pepper to taste + +## Instructions +1. Preheat oven to 180°C (350°F). +2. Heat olive oil in a skillet over medium heat. +3. Sauté onion and garlic until softened, about 5 minutes. +4. Add spinach and cook until wilted, about 2 minutes. +5. Whisk eggs in a bowl, season with salt and pepper. +6. Pour eggs over the spinach mixture in the skillet. +7. Sprinkle feta cheese on top. +8. Cook for 2-3 minutes until edges are set. +9. Transfer skillet to oven and bake for 10-12 minutes until fully set. +10. Slice and serve warm. + +Makes 4 servings.