diff --git a/references/commands.md b/references/commands.md index f0f9094..547c91f 100644 --- a/references/commands.md +++ b/references/commands.md @@ -130,4 +130,4 @@ tmx login ``` OAuth login flow with Cookidoo account. Opens browser for authentication. -Credentials stored in `secrets/cookidoo.env` (COOKIDOO_EMAIL, COOKIDOO_PASSWORD). +Credentials can be provided interactively or via environment variables (`COOKIDOO_EMAIL`, `COOKIDOO_PASSWORD`). diff --git a/tmx_cli.py b/tmx_cli.py index 91bb52e..c3ba30b 100644 --- a/tmx_cli.py +++ b/tmx_cli.py @@ -229,9 +229,15 @@ def sync_categories(progress_callback=None) -> tuple[dict[str, str], list[str]]: return categories, errors -# Alias for backward compatibility -CATEGORIES, _ = load_categories() -CATEGORY_NAMES = {v: k for k, v in CATEGORIES.items()} # Reverse lookup +_categories_cache = None + + +def get_categories() -> dict: + """Get categories, loading from cache/fallback on first access.""" + global _categories_cache + if _categories_cache is None: + _categories_cache, _ = load_categories() + return _categories_cache # ───────────────────────────────────────────────────────────────────────────── @@ -714,7 +720,7 @@ def search_recipes( if tm_version: filters.append(f"tmversion:{tm_version}") if category: - cat_id = CATEGORIES.get(category.lower()) + cat_id = get_categories().get(category.lower()) if cat_id: filters.append(f"categories.id:{cat_id}") @@ -773,6 +779,17 @@ def format_time(seconds: Optional[int]) -> str: return f"{hours}h" +def format_qty(qty) -> str: + """Format a quantity value safely, handling None/string/float.""" + if qty is None or qty == "": + return "" + try: + qty = float(qty) + except (TypeError, ValueError): + return str(qty) + return str(int(qty)) if qty == int(qty) else f"{qty:.1f}" + + def seconds_to_minutes(seconds: Optional[int]) -> Optional[int]: """Convert seconds to minutes, return None if input is None/0.""" if not seconds: @@ -1740,9 +1757,8 @@ def cmd_recipe_show(args): # Servings serving_size = data.get("servingSize", {}) servings = serving_size.get("quantity", {}).get("value", "") - if servings and servings == int(servings): - servings = int(servings) - + servings_str = format_qty(servings) if servings else "" + # Nutrition data - parse the complex structure nutrition = {} for ng in data.get("nutritionGroups", []): @@ -1783,8 +1799,8 @@ def cmd_recipe_show(args): print(f"⏱ {' | '.join(time_parts)}") # Servings - if servings: - print(f"👥 {servings} Portionen") + if servings_str: + print(f"👥 {servings_str} Portionen") # Difficulty and TM version info_parts = [] @@ -1844,13 +1860,7 @@ def cmd_recipe_show(args): optional = ing.get("optional", False) # Format quantity - if qty: - if qty == int(qty): - qty_str = str(int(qty)) - else: - qty_str = f"{qty:.1f}" - else: - qty_str = "" + qty_str = format_qty(qty) # Build ingredient line parts = [] @@ -1920,10 +1930,9 @@ def progress(msg): print(f"✅ {len(categories)} Kategorien synchronisiert!") print(f" Gespeichert in: {CATEGORIES_CACHE_FILE}") - # Reload global CATEGORIES - global CATEGORIES, CATEGORY_NAMES - CATEGORIES = categories - CATEGORY_NAMES = {v: k for k, v in CATEGORIES.items()} + # Reload global categories cache + global _categories_cache + _categories_cache = categories else: print("❌ Keine Kategorien synchronisiert.") @@ -2055,8 +2064,6 @@ def cmd_status(args): def cmd_cache_clear(args): """Clear cached data files.""" - import os - files = [ ("Wochenplan", WEEKPLAN_JSON), ("Such-Token", SEARCH_TOKEN_FILE), @@ -2073,7 +2080,7 @@ def cmd_cache_clear(args): deleted = 0 for name, path in files: if path.exists(): - os.remove(path) + path.unlink() print(f" ✅ {name} gelöscht") deleted += 1 else: @@ -2228,11 +2235,8 @@ def cmd_shopping_show(args): prep_str = f" ({prep})" if prep else "" opt_str = " (optional)" if optional else "" - if qty == int(qty): - qty_str = str(int(qty)) - else: - qty_str = f"{qty:.1f}" - + qty_str = format_qty(qty) + check = "✓" if is_owned else " " print(f" [{check}] {qty_str} {unit} {name}{prep_str}{opt_str}") @@ -2270,11 +2274,8 @@ def cmd_shopping_show(args): opt = " (optional)" if ing["optional"] else "" # Format quantity nicely - if qty == int(qty): - qty_str = str(int(qty)) - else: - qty_str = f"{qty:.1f}" - + qty_str = format_qty(qty) + print(f" [ ] {qty_str} {unit} {name}{prep}{opt}") if owned: @@ -2370,8 +2371,7 @@ def cmd_shopping_export(args): lines = [] if fmt == "json": - import json as json_module - output = json_module.dumps(data, indent=2, ensure_ascii=False) + output = json.dumps(data, indent=2, ensure_ascii=False) elif fmt == "markdown": if by_recipe: for recipe in recipes: @@ -2383,7 +2383,7 @@ def cmd_shopping_export(args): name = ing.get("ingredientNotation", "") qty = ing.get("quantity", {}).get("value", 0) unit = ing.get("unitNotation", "") - qty_str = str(int(qty)) if qty == int(qty) else f"{qty:.1f}" + qty_str = format_qty(qty) is_owned = ing.get("isOwned", False) check = "x" if is_owned else " " lines.append(f"- [{check}] {qty_str} {unit} {name}") @@ -2396,7 +2396,7 @@ def cmd_shopping_export(args): if ing["is_owned"]: continue qty = ing["quantity"] - qty_str = str(int(qty)) if qty == int(qty) else f"{qty:.1f}" + qty_str = format_qty(qty) lines.append(f"- [ ] {qty_str} {ing['unit']} {ing['name']}") # Additional items @@ -2419,7 +2419,7 @@ def cmd_shopping_export(args): name = ing.get("ingredientNotation", "") qty = ing.get("quantity", {}).get("value", 0) unit = ing.get("unitNotation", "") - qty_str = str(int(qty)) if qty == int(qty) else f"{qty:.1f}" + qty_str = format_qty(qty) lines.append(f" {qty_str} {unit} {name}") lines.append("") else: @@ -2428,7 +2428,7 @@ def cmd_shopping_export(args): if ing["is_owned"]: continue qty = ing["quantity"] - qty_str = str(int(qty)) if qty == int(qty) else f"{qty:.1f}" + qty_str = format_qty(qty) lines.append(f"{qty_str} {ing['unit']} {ing['name']}") # Additional items @@ -2885,7 +2885,7 @@ def build_parser(): search_parser.add_argument("-t", "--time", type=int, help="Max. Zubereitungszeit in Minuten") search_parser.add_argument("-d", "--difficulty", choices=["easy", "medium", "advanced"], help="Schwierigkeitsgrad") search_parser.add_argument("--tm", choices=["TM5", "TM6", "TM7"], help="Thermomix-Version") - search_parser.add_argument("-c", "--category", choices=list(CATEGORIES.keys()), help="Kategorie") + search_parser.add_argument("-c", "--category", choices=list(get_categories().keys()), help="Kategorie") search_parser.set_defaults(func=cmd_search) # recipe command with subcommands