Beispiel #1
0
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')
Beispiel #2
0
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'
Beispiel #3
0
 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'
Beispiel #4
0
 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
Beispiel #5
0
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)
Beispiel #6
0
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 []
Beispiel #7
0
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)
Beispiel #8
0
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'),
                                            ]),
                        ]),
                ],
            ),
        ])
Beispiel #9
0
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))
Beispiel #10
0
def test_amount():
    with pytest.raises(TypeError) as excinfo:
        Amount()

    assert excinfo.value.args[0] == "Factor and unit may not both be None"
Beispiel #11
0
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'))
Beispiel #12
0
    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)