Inital commit for agent

This commit is contained in:
test committed 2026-09-05 10:39:02 +02:00
commit 82fdc33771
12 files changed
+497

No files matched your search

+21
View File
@@ -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.
+105
View File
@@ -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.
+3
View File
@@ -0,0 +1,3 @@
"""
Nutritional Recipe Parser Package
"""
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
+117
View File
@@ -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()
+211
View File
@@ -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)
+8
View File
@@ -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 []
+6
View File
@@ -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
+26
View File
@@ -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.