def test_multiply_recipe(): recipe = Recipe(title="Test", yields=[ Amount(factor=Decimal('5'), unit="servings"), Amount(unit="unitless yield") ], ingredients=[ Ingredient(amount=Amount(factor=Decimal('5')), name='Eggs'), Ingredient(amount=Amount(factor=Decimal('200'), unit='g'), name='Butter'), Ingredient(name='Salt') ], ingredient_groups=[ IngredientGroup(title='Group', ingredients=[ Ingredient(amount=Amount( factor=Decimal('2'), unit='cloves'), name='Garlic'), ]), ]) result = multiply_recipe(recipe, Decimal(2)) assert result.yields[0].factor == Decimal('10') assert result.ingredients[0].amount.factor == Decimal('10') assert result.ingredients[1].amount.factor == Decimal('400') assert result.ingredients[2].amount is None assert result.ingredient_groups[0].ingredients[0].amount.factor == Decimal( '4')
def test_ingredient_list_get_leaf_ingredients(): recipe = Recipe(title="Test", ingredients=[ Ingredient(amount=Amount(factor=Decimal('5')), name='Eggs'), Ingredient(amount=Amount(factor=Decimal('200'), unit='g'), name='Butter'), Ingredient(name='Salt') ], ingredient_groups=[ IngredientGroup( title='Group', ingredients=[ Ingredient(amount=Amount(factor=Decimal('2'), unit='cloves'), name='Garlic'), ], ingredient_groups=[ IngredientGroup(title='Subgroup', ingredients=[ Ingredient(name='Onions'), ]), ]), ]) pprint(recipe) leaf_ingredients = list(recipe.leaf_ingredients) assert len(leaf_ingredients) == 5 assert leaf_ingredients[0].name == 'Eggs' assert leaf_ingredients[1].name == 'Butter' assert leaf_ingredients[2].name == 'Salt' assert leaf_ingredients[3].name == 'Garlic' assert leaf_ingredients[4].name == 'Onions'
def test_serialize_amount(self, serializer): assert serializer._serialize_amount( Amount(factor=Decimal('5.000'))) == '5' assert serializer._serialize_amount(Amount(factor=Decimal('1') / Decimal('3')), rounding=2) == '0.33' assert serializer._serialize_amount(Amount(factor=Decimal('1') / Decimal('3')), rounding=4) == '0.3333'
def test_parse_amount(self, parser): assert parser.parse_amount("2") == Amount(factor=Decimal('2')) assert parser.parse_amount("5 g") == Amount(factor=Decimal('5'), unit='g') assert parser.parse_amount("5 1/4 ml") == Amount(factor=Decimal('5.25'), unit='ml') assert parser.parse_amount("1/4 l") == Amount(factor=Decimal('0.25'), unit='l') assert parser.parse_amount("-5") == Amount(factor=Decimal('-5')) assert parser.parse_amount("3.2") == Amount(factor=Decimal('3.2')) assert parser.parse_amount("3,2") == Amount(factor=Decimal('3.2')) assert parser.parse_amount("1 ½ cloves") == Amount(factor=Decimal('1.5'), unit='cloves') assert parser.parse_amount("½ pieces") == Amount(factor=Decimal('.5'), unit='pieces') assert parser.parse_amount('') is None
def extract(url, soup): if not 'seriouseats.com' in url: return # title title = soup.find('h1', attrs={'class': 'title recipe-title'}).text.strip() # summary summary = '' summaryPars = soup.find('div', attrs={ 'class': 'recipe-introduction-body' }).find_all('p') for par in summaryPars: if not 'caption' in par.attrs.get('class', []): summary = summary + par.text + '\n\n' summary = summary.strip() # servings yields = [] servings = soup.find('span', attrs={'class': 'info yield'}).text servings_factor = re.compile("\d+").findall(servings) if servings_factor: yields.append(Amount(Decimal(servings_factor[0]), 'servings')) # tags tags = [] for tag in soup.find_all('a', attrs={'class': 'tag'}): tags.append(tag.text) # ingredients ingredients = [] for ingred in soup.find_all('li', attrs={'class': 'ingredient'}): ingredients.append(Ingredient(name=ingred.text)) # instructions instructions = '' for step in soup.find_all('li', attrs={'class': 'recipe-procedure'}): stepNumber = step.find('div', attrs={ 'class': 'recipe-procedure-number' }).text.strip() stepInstr = step.find('div', attrs={ 'class': 'recipe-procedure-text' }).text.strip() instructions = instructions + stepNumber + ' ' + stepInstr + '\n' instructions = instructions.strip() return Recipe(title=title, ingredients=ingredients, instructions=instructions, description=summary, tags=tags, yields=yields)
def _yield_completer(prefix, action, parser, parsed_args): try: src = parsed_args.file.read() r = RecipeParser().parse(src) parsed_yield = RecipeParser.parse_amount(prefix) if parsed_yield is None or parsed_yield.factor is None: return [RecipeSerializer._serialize_amount(a) for a in r.yields] return [RecipeSerializer._serialize_amount(Amount(parsed_yield.factor, a.unit)) for a in r.yields if parsed_yield.unit is None or (a.unit is not None and a.unit.startswith(parsed_yield.unit))] except Exception as e: print(e) return []
def extract(url, soup): if not 'chefkoch.de' in url: return # title title = soup.find('h1', attrs={'class': 'page-title'}).text if title == 'Fehler: Seite nicht gefunden' or title == 'Fehler: Rezept nicht gefunden': raise ValueError('No recipe found, check URL') # summary summaryTag = soup.find('div', attrs={'class': 'summary'}) summary = summaryTag.text if summaryTag else None # servings servings = soup.find('input', attrs={'id': 'divisor'}).attrs['value'] yields = [ Amount(Decimal(servings), f'Portion{"en" if int(servings) > 1 else ""}') ] # tags tags = [] tagcloud = soup.find('ul', attrs={'class': 'tagcloud'}) for tag in tagcloud.find_all('a'): tags.append(tag.text) # ingredients table = soup.find('table', attrs={'class': 'incredients'}) rows = table.find_all('tr') ingreds = [] for row in rows: cols = row.find_all('td') cols = [s.text.strip() for s in cols] amount = RecipeParser.parse_amount(cols[0]) ingreds.append(Ingredient(name=cols[1], amount=amount)) # instructions instruct = soup.find('div', attrs={ 'id': 'rezept-zubereitung' }).text # only get text instruct = instruct.strip() # remove leadin and ending whitespace # write to file return Recipe(title=title, ingredients=ingreds, instructions=instruct, description=summary, tags=tags, yields=yields)
def recipe() -> Recipe: return Recipe( title="Test", tags=["vegetarian", "flavorful", "tag with spaces"], yields=[ Amount(factor=Decimal("1"), unit="serving"), Amount(factor=Decimal(0.4), unit="kg") ], ingredients=[ Ingredient(amount=Amount(factor=Decimal('5')), name='Eggs'), Ingredient(amount=Amount(factor=Decimal('200'), unit='g'), name='Butter'), Ingredient(name='Salt') ], ingredient_groups=[ IngredientGroup( title='Group', ingredients=[ Ingredient(amount=Amount(factor=Decimal('2'), unit='cloves'), name='Garlic'), ], ingredient_groups=[ IngredientGroup( title='Group', ingredients=[ Ingredient(amount=Amount(factor=Decimal('2'), unit='cloves'), name='Garlic'), ], ingredient_groups=[ IngredientGroup(title='Subgroup', ingredients=[ Ingredient(name='Onions'), ]), ]), ], ), ])
def main(): parser = argparse.ArgumentParser( description='Read and process recipemd recipes') parser.add_argument('file', type=open, help='A recipemd file') display_parser = parser.add_mutually_exclusive_group() display_parser.add_argument('-t', '--title', action='store_true', help='Display recipe title') display_parser.add_argument('-i', '--ingredients', action='store_true', help='Display recipe ingredients') scale_parser = parser.add_mutually_exclusive_group() scale_parser.add_argument('-m', '--multiply', type=str, help='Multiply recipe by N', metavar='N') scale_parser.add_argument('-y', '--yield', type=str, help='Scale the recipe for yield Y', metavar='Y', dest='required_yield') args = parser.parse_args() src = args.file.read() rp = RecipeParser() r = rp.parse(src) if args.required_yield is not None: required_yield = RecipeParser.parse_amount(args.required_yield) if required_yield is None or required_yield.factor is None: print(f'Given yield is not valid', file=sys.stderr) exit(1) matching_recipe_yield = next( (y for y in r.yields if y.unit == required_yield.unit), None) if matching_recipe_yield is None: if required_yield.unit is None: matching_recipe_yield = Amount(Decimal(1)) else: print( f'Recipe "{r.title}" does not specify a yield in the unit "{required_yield.unit}". The ' f'following units can be used: ' + ", ".join(f'"{y.unit}"' for y in r.yields), file=sys.stderr) exit(1) r = multiply_recipe( r, required_yield.factor / matching_recipe_yield.factor) elif args.multiply is not None: multiply = RecipeParser.parse_amount(args.multiply) if multiply is None or multiply.factor is None: print(f'Given multiplier is not valid', file=sys.stderr) exit(1) if multiply.unit is not None: print(f'A recipe can only be multiplied with a unitless amount', file=sys.stderr) exit(1) r = multiply_recipe(r, multiply.factor) if args.title: print(r.title) elif args.ingredients: for ingr in r.leaf_ingredients: print(_ingredient_to_string(ingr)) else: rs = RecipeSerializer() print(rs.serialize(r))
def test_amount(): with pytest.raises(TypeError) as excinfo: Amount() assert excinfo.value.args[0] == "Factor and unit may not both be None"
def test_get_recipe_with_yield(): recipe = Recipe( title="Test", yields=[Amount(factor=Decimal('2'), unit="servings")], ingredients=[ Ingredient(amount=Amount(factor=Decimal('5')), name='Eggs'), ], ) result = get_recipe_with_yield( recipe, Amount(factor=Decimal('4'), unit='servings')) assert result.yields[0] == Amount(factor=Decimal('4'), unit='servings') assert result.ingredients[0].amount == Amount(factor=Decimal('10')) # interpreted as "4 recipes", that is multiply by 4 result_unitless = get_recipe_with_yield(recipe, Amount(factor=Decimal('4'))) assert result_unitless.yields[0] == Amount(factor=Decimal('8'), unit='servings') assert result_unitless.ingredients[0].amount == Amount( factor=Decimal('20')) # if recipe has unitless yield, it is preferred to the above interpretation recipe_with_unitless_yield = replace(recipe, yields=[Amount(factor=Decimal('4'))]) result_unitless_from_unitless_yield = get_recipe_with_yield( recipe_with_unitless_yield, Amount(factor=Decimal('4'))) assert result_unitless_from_unitless_yield.yields[0] == Amount( factor=Decimal('4')) assert result_unitless_from_unitless_yield.ingredients[0].amount == Amount( factor=Decimal('5')) # try with unit not in recipe yields with pytest.raises(StopIteration): get_recipe_with_yield(recipe, Amount(factor=Decimal('500'), unit='ml')) # try with factorless required yield with pytest.raises(RuntimeError): get_recipe_with_yield(recipe, Amount(unit='ml')) # try with factorless yield in recipe recipe_with_factorless_yield = replace(recipe, yields=[Amount(unit='Foos')]) with pytest.raises(RuntimeError): get_recipe_with_yield(recipe_with_factorless_yield, Amount(factor=Decimal('500'), unit='Foos'))
def download_file(relative_path=''): absolute_path = os.path.join(base_folder_path, relative_path) if os.path.isdir(absolute_path): if not absolute_path.endswith('/'): return redirect(f'/{relative_path}/', code=302) child_paths = [(ch, os.path.isdir(os.path.join(absolute_path, ch))) for ch in os.listdir(absolute_path)] child_paths = [ (ch, is_dir) for ch, is_dir in child_paths if not ch.startswith('.') and (is_dir or ch.endswith('.md')) ] child_paths = [ f'{ch}/' if not ch.endswith('/') and is_dir else ch for ch, is_dir in child_paths ] child_paths = sorted(child_paths) return render_template("folder.html", child_paths=child_paths, path=relative_path) if not absolute_path.endswith('.md'): return send_from_directory(base_folder_path, relative_path) with open(absolute_path, 'r', encoding='UTF-8') as f: required_yield_str = request.args.get('yield', '1') required_yield = recipe_parser.parse_amount(required_yield_str) if required_yield is None: required_yield = Amount(factor=Decimal(1)) src = f.read() try: recipe = recipe_parser.parse(src) except Exception as e: return render_template("markdown.html", markdown=src, path=relative_path, errors=[e.args[0]]) errors = [] try: recipe = get_recipe_with_yield(recipe, required_yield) except StopIteration: errors.append( f'The recipe does not specify a yield in the unit "{required_yield.unit}". ' f'The following units can be used: ' + ", ".join(f'"{y.unit}"' for y in recipe.yields)) except Exception as e: errors.append(str(e)) return render_template( "recipe.html", recipe=recipe, yields=recipe_serializer._serialize_yields(recipe.yields, rounding=2), tags=recipe_serializer._serialize_tags(recipe.tags), units=list(set(y.unit for y in recipe.yields)), default_yield=recipe_serializer._serialize_amount( recipe.yields[0]) if recipe.yields else "1", path=relative_path, errors=errors)