feat: Added templates for macro, list, and view for recipe

- feat: Added list and view commands for recipe
This commit is contained in:
Aerex 2019-08-04 23:59:28 -05:00
parent 6af9bef334
commit 05089108a9
11 changed files with 282 additions and 118 deletions

View File

@ -1,12 +0,0 @@
1. Preheat oven to 425 degrees F (220 degrees C). Lightly oil a large roasting pan.
2. Place chicken pieces in large bowl. Season with salt, oregano, pepper, rosemary, and cayenne pepper. Add fresh lemon juice, olive oil, and garlic. Place potatoes in bowl with the chicken; stir together until chicken and potatoes are evenly coated with marinade.
3. Transfer chicken pieces, skin side up, to prepared roasting pan, reserving marinade. Distribute potato pieces among chicken thighs. Drizzle with 2/3 cup chicken broth. Spoon remainder of marinade over chicken and potatoes.
4. Place in preheated oven. Bake in the preheated oven for 20 minutes. Toss chicken and potatoes, keeping chicken skin side up; continue baking until chicken is browned and cooked through, about 25 minutes more. An instant-read thermometer inserted near the bone should read 165 degrees F (74 degrees C). Transfer chicken to serving platter and keep warm.
5. Set oven to broil or highest heat setting. Toss potatoes once again in pan juices. Place pan under broiler and broil until potatoes are caramelized, about 3 minutes. Transfer potatoes to serving platter with chicken.
6. Place roasting pan on stove over medium heat. Add a splash of broth and stir up browned bits from the bottom of the pan. Strain; spoon juices over chicken and potatoes. Top with chopped oregano.
[](/"https://www.allrecipes.com/recipe/242352/greek-lemon-chicken-and-
potatoes//")

View File

@ -23,6 +23,7 @@ CONFIG_DIR = click.get_app_dir(APP_NAME)
PROJ_DIR = path.join(path.dirname(path.realpath(__file__)))
TEMPLATE_LOADER = Environment(loader=FileSystemLoader('templates'), trim_blocks=True, lstrip_blocks=True)
class GrocyGroup(click.Group):
def parse_args(self, ctx, args):
is_subcommand = True if args[0] in self.commands else False
@ -97,6 +98,7 @@ def view(ctx, product_id):
logger.error(e)
raise e
@product.command()
@click.option('--name', '-n', 'name')
@click.argument('product_id', required=False)
@ -124,7 +126,7 @@ def edit(product_id, name):
if edited_products is None:
return
parsed_edited_products = Util.load_yaml(edited_products)
parsed_edited_products = util.load_yaml(edited_products)
schema = entity.schema
for index, edited_product in enumerate(parsed_edited_products):
@ -154,6 +156,8 @@ def list(name, template):
# Convert name args to a single string
string_name_arg = ' '.join(name) if type(name) == list else name
products = product_entity.find({'name': string_name_arg})
if len(products) == 0:
return
else:
products = product_entity.get()
@ -193,16 +197,23 @@ def add(template):
logger = logging.getLogger('cli.product.add')
meta = Meta()
# Get product_groups
meta.add(type='entities', name='product_groups')
entity = Entity(name='product_groups')
product_groups = entity.get()
meta.add(type='entities', name='product_groups', valid_values=product_groups)
# Get locations
meta.add(type='entities', name='locations')
entity = Entity(name='locations')
locations = entity.get()
meta.add(type='entities', name='locations', valid_values=locations)
# Get quantity_units
meta.add(type='entities', name='quantity_units')
entity = Entity(name='quantity_units')
quantity_units = entity.get()
meta.add(type='entities', name='quantity_units', valid_values=quantity_units)
data = { 'meta': meta.generate() }
if template:
loaded_template = cfg.templates(template)
else:
loaded_template = cfg.templates('product/add')
new_product = click.edit(loaded_template.render(grocy=meta.generate()), extension='.yml')
new_product = click.edit(loaded_template.render(grocy=data), extension='.yml')
if not new_product:
return
@ -222,6 +233,7 @@ def add(template):
def recipe(ctx):
pass
@recipe.command()
@click.pass_context
@click.argument('recipe_id', required=False)
@ -234,7 +246,10 @@ def view(ctx, recipe_id, template):
data = {'fields': {}}
if recipe_id:
entity = Entity(name='recipes')
meta = Meta()
data['fields'] = entity.get(id=recipe_id)
recipe = Recipe(id=recipe_id)
data['fields']['fulfillment'] = recipe.fulfillment
# Change html markup to plain text
html_markup_description = data['fields']['description']
@ -242,35 +257,147 @@ def view(ctx, recipe_id, template):
data['fields']['description'] = plain_text_description
recipe = Recipe(id=recipe_id)
data['fields']['fulfillment'] = recipe.get_fulfillments()
meta.add(type='recipes', name='ingredients', valid_values=recipe.ingredients)
data['meta'] = meta.generate()
if template:
loaded_template = cfg.templates(template)
else:
loaded_template = cfg.templates('recipe/view')
entity = Entity(name='recipes')
click.echo(loaded_template.render(grocy=data))
else:
click.echo(ctx.get_help())
except Exception as e:
logger.error(e)
raise e
#@recipe.command()
#def list():
# logger = logging.getLogger('cli.recipe')
# try:
# entity = Entity(name='recipes')
# recipes = entity.get()
# recipe = Recipe(
# ingredient_reqs = recipe.get_ingredient_requirements()
# table = Table(recipes=recipes, recipes_reqs=recipes_reqs, ingredient_reqs=ingredient_reqs)
# click.echo(table.recipe)
# except Exception as e:
# logger.error(e)
# raise e
##
#
@recipe.command()
@click.option('--name', '-n', 'name')
@click.option('-t', 'template')
def list(name, template):
logger = logging.getLogger('cli.recipe.list')
cfg = Configuration()
cfg.load()
try:
entity = Entity(name='recipes')
if name:
# Convert name args to a single string
string_name_arg = ' '.join(name) if type(name) == list else name
recipe_entities = entity.find({'name': string_name_arg})
if len(recipe_entities) == 0:
return 0
else:
recipe_entities = entity.get()
data = {'recipes': []}
for recipe_entity in recipe_entities:
entry = {'fields': recipe_entity, 'meta': []}
meta = Meta(include_fa_icons=False)
recipe = Recipe(id=recipe_entity['id'])
entry['fields']['fulfillment'] = recipe.fulfillment
entry['fields']['description'] = recipe.generate_plain_text_description(recipe_entity['description'])
meta.add(type='recipes', name='ingredients', valid_values=recipe.ingredients)
entry['meta'].append(meta.generate())
data['recipes'].append(entry)
meta = Meta()
data['meta'] = meta.generate()
if template:
loaded_template = cfg.templates(template)
else:
loaded_template = cfg.templates('recipe/list')
click.echo(loaded_template.render(grocy=data))
except Exception as e:
logger.error(e)
raise e
@recipe.command()
@click.option('--fullscreen', '-f', 'fullscreen', is_flag=True)
@click.argument('recipe_id')
def browse(fullscreen, recipe_id):
logger = logging.getLogger('cli.recipe.browse')
try:
cfg = Configuration()
cfg.load()
url = '{domain}/recipes?recipe={recipe_id}'.format(domain=cfg.domain, recipe_id=recipe_id)
if fullscreen:
url += '#fullscreen'
click.launch(url, wait=False)
except Exception as e:
logger.error(e)
@recipe.command()
@click.option('--name', '-n', 'name')
@click.argument('recipe_id', required=False)
@click.option('-t', 'template')
def edit(recipe_id, name, template):
logger = logging.getLogger('cli.recipe.edit')
try:
cfg = Configuration()
util = Util(cfg=cfg)
cfg.load()
loaded_template = cfg.templates('recipe/edit')
if template:
loaded_template = cfg.templates(template)
else:
loaded_template = cfg.templates('recipe/list')
entity = Entity(name='recipes')
if recipe_id:
recipe_entity = entity.get(id=recipe_id)
recipe = Recipe(id=recipe_id)
entry = {'fields': recipe_entity}
meta = Meta()
entry['fields']['fulfillment'] = recipe.fulfillment
entry['fields']['description'] = recipe.generate_plain_text_description(entity['description'])
entry['meta'] = meta.generate()
edited_recipe = click.edit(loaded_template.render(grocy=entry))
edited_recipe.update()
return
elif name:
# Convert name args to a single string
string_name_arg = ' '.join(name) if type(name) == list else name
recipe_entities = entity.find({'name': string_name_arg})
if len(recipe_entities) == 0:
click.echo('Could not find recipe')
return
data = {'recipes': []}
for recipe_entity in recipe_entities:
entry = {'fields': recipe_entity, 'meta': []}
meta = Meta(include_fa_icons=False)
recipe = Recipe(id=recipe_entity['id'])
entry['fields']['fulfillment'] = recipe.fulfillment
entry['fields']['description'] = recipe.generate_plain_text_description(recipe_entity['description'])
meta.add(type='recipes', name='ingredients', valid_values=recipe.ingredients)
entry['meta'].append(meta.generate())
data['recipes'].append(entry)
meta = Meta()
data['meta'] = meta.generate()
edited_recipes = click.edit(loaded_template.render(grocy=data))
if edited_recipes is None:
return
parsed_edited_recipes = util.load_yaml(edited_recipes)
for index, edited_recipe in enumerate(parsed_edited_recipes):
edited_recipe['id'] = recipe_entities[index]['id']
# Util.verify_integrity(edited_recipe, schema)
entity.update(edited_recipe, id=recipe[index]['id'])
else:
raise click.BadParameter('Missing RECIPE_ID or QUERY')
except Exception as e:
logger.error(e)
raise e
#@main.command()
#def shopping():
# logger = logging.getLogger('cli.shopping')
@ -337,27 +464,6 @@ def view(ctx, recipe_id, template):
# raise e
#
#
#@recipe.command('edit')
#@click.argument('recipe_id')
#def edit(recipe_id):
# logger = logging.getLogger('cli.recipe.edit')
# try:
# if recipe_id:
# entity = Entity(name='recipes')
# recipe = entity.get(id=recipe_id)
# loaded_template = TEMPLATE_LOADER.get_template('recipe_edit.yml')
# edited_recipe = click.edit(loaded_template.render(recipe))
# if edited_recipe is not None:
# parsed_edited_recipe = yaml.safe_load(edited_recipe)
# parsed_edited_recipe['description'] = markdown(parsed_edited_recipe['description'])
# recipe.__dict__.update(parsed_edited_recipe)
# recipe.update()
# except Exception as e:
# logger.error(e)
# logger.error('Could not edit recipe {}'.format(recipe_id))
# raise e
#
#
#@recipe.command('create')
#@click.pass_context
#def create(ctx):

View File

@ -99,7 +99,7 @@ class Configuration(object):
def templates(self, name):
try:
TEMPLATE_LOADER = Environment(loader=FileSystemLoader([self.USER_TEMPLATE_DIR, self.PROJ_TEMPLATE_DIR]),
trim_blocks=False, lstrip_blocks=True)
trim_blocks=False, lstrip_blocks=True, keep_trailing_newline=False)
return TEMPLATE_LOADER.get_template('{}.{}'.format(name, self.TEMPLATE_EXT))
except Exception as e:

View File

@ -2,7 +2,6 @@ from grocy.request import Request
import re
from copy import deepcopy
from grocy.conf import Configuration
import logging
@ -51,7 +50,7 @@ class Entity(object):
found_entities.append(entity)
if len(found_entities) == 0:
return None
return []
return found_entities
except Exception as e:

View File

@ -5,23 +5,21 @@ from fontawesome import icons as fa_icons
class Meta(object):
def __init__(self):
self.meta = {'meta': {'entities': {}, 'fa_icons': fa_icons}}
def __init__(self, include_fa_icons=True):
self.meta = {}
if include_fa_icons:
self.meta['fa_icons'] = fa_icons
def add(self, type, name):
if type == 'entities':
entity = Entity(name=name)
resources = entity.get()
schema = get_schema(name)
elif type == 'recipes':
recipe = Recipe()
if name == 'fulfillments':
resources = recipe.get_fulfillments()
schema = get_schema(name='recipe_fulfilments')
self.meta['meta'][type][name] = {}
self.meta['meta'][type][name]['properties'] = schema['properties']
self.meta['meta'][type][name]['valid_values'] = resources
def add(self, type, name=None, ids=[], valid_values=None):
if type not in self.meta:
self.meta[type] = {}
if name and name not in self.meta[type]:
self.meta[type][name] = {}
#if name:
# schema = get_schema(name)
#if type == 'entities':
#self.meta[type][name]['properties'] = schema['properties']
self.meta[type][name]['valid_values'] = valid_values
def generate(self):
return self.meta

View File

@ -1,5 +1,8 @@
from grocy.request import Request
from html2text import html2text
from grocy.stock import Stock
from grocy.conf import Configuration
from copy import deepcopy
from grocy.entity import Entity
import logging
@ -7,17 +10,87 @@ import logging
class Recipe(object):
GET_RECIPES_FULFILLMENT_URL_TEMPLATE = '{domain}/api/recipes/fulfillment'
GET_RECIPE_FULFILLMENT_URL_TEMPLATE = '{domain}/api/recipes/{recipeId}/fulfillment'
GET_RECIPES_POS_FULFILLMENT_URL_TEMPLATE = '{domain}/api/recipes/pos/fulfillment'
GET_RECIPE_POS_FULFILLMENT_URL_TEMPLATE = '{domain}/api/recipes/{recipeId}/pos/{recipeId}/fulfillment'
GET_RECIPES_POS_FULFILLMENT_URL_TEMPLATE = '{domain}/api/recipes/{recipeId}/pos/fulfillment'
GET_RECIPE_POS_FULFILLMENT_URL_TEMPLATE = '{domain}/api/recipes/{recipeId}/pos/{recipePosId}/fulfillment'
def __init__(self, id=None):
self.conf = Configuration()
self.conf.load()
self.id = id
def get_ingredient_requirements(self, recipe_id=None):
logger = logging.getLogger('recipe.get_ingredient_requirements')
@property
def ingredients(self):
logger = logging.getLogger('recipes.ingredients')
stock = Stock()
ingredients_detail = []
products = Entity(name='products').get()
locations = Entity(name='locations').get()
quantity_units = Entity(name='quantity_units').get()
for recipe_pos in self.pos_fulfillment:
if recipe_pos['recipe_id'] == self.id:
data = {}
product = next((p for p in products if p['id'] == recipe_pos['product_id']), None)
product['quantity_unit'] = next((q for q in quantity_units if q['id'] == recipe_pos['qu_id']), None)
product['location'] = next((l for l in locations if l['id'] == product['location_id']), None)
product['group'] = recipe_pos['ingredient_group']
# Remove extraneous properties
recipe_pos.pop('recipe_id', None)
recipe_pos.pop('ingredient_group', None)
recipe_pos.pop('id', None)
recipe_pos.pop('product_id', None)
recipe_pos.pop('recipe_pos_id', None)
recipe_pos.pop('qu_id', None)
product.pop('qu_id_purchase', None)
product.pop('qu_id_stock', None)
product.pop('location_id', None)
product['location'].pop('row_created_timestamp', None)
product['quantity_unit'].pop('row_created_timestamp', None)
product.pop('row_created_timestamp', None)
# Merge recipe pos and products
product.update(recipe_pos)
data.update(product)
ingredients_detail.append(data)
return ingredients_detail
@property
def pos_fulfillment(self):
logger = logging.getLogger('recipe.pos_fulfillment')
try:
if self.id is None:
raise Exception('recipe id is required')
url = self.GET_RECIPES_POS_FULFILLMENT_URL_TEMPLATE.format(domain=self.conf.domain, recipeId=self.id)
request = Request('get', url)
return request.send()
except Exception as e:
logger.error(e)
raise e
def generate_plain_text_description(self, description):
if not description:
raise Exception('Missing description')
logger = logging.getLogger('recipe.generate_plain_text_description')
try:
plain_text_description = html2text(description)
return plain_text_description
except Exception as e:
logger.error(e)
raise e
@property
def fulfillment(self):
logger = logging.getLogger('recipe.fulfillment')
url = self.GET_RECIPE_FULFILLMENT_URL_TEMPLATE.format(domain=self.conf.domain, recipeId=self.id)
request = Request('get', url)
try:
return request.send()
except Exception as e:
logger.error(e)
raise e
def get_fulfillments(self):
logger = logging.getLogger('recipe.get_requirements')
@ -31,24 +104,3 @@ class Recipe(object):
except Exception as e:
logger.error(e)
raise e
def get(self):
# Get list of available ingredients
if self.id:
entity = Entity(name='recipes')
recipe = entity.get(id=self.id)
if type(recipe) is list:
pass

View File

@ -5,11 +5,12 @@ import logging
SCHEMA_URL_TEMPLATE = '{domain}/api/openapi/specification'
SCHEMA_MODEL_MAP = {
'products': 'Product',
'product_details': 'ProductDetailsResponse',
'stock': 'StockEntry',
'product_groups': 'ProductGroup',
'locations': 'Location',
'quantity_units': 'QuantityUnit',
'recipe_requirements': 'RecipeFulfilmentResponse'
'recipe_fulfillment': 'RecipeFulfillmentResponse'
}

View File

@ -11,8 +11,7 @@ class Util(object):
self.cfg = cfg
yaml.SafeLoader.add_constructor("tag:yaml.org,2002:python/unicode", _yaml_constructor)
def load_yaml(data):
def load_yaml(self, data):
generator = yaml.safe_load_all(data)
data_list = list(generator)
return data_list

11
templates/recipe/list.yml Normal file
View File

@ -0,0 +1,11 @@
{% from 'recipe/macro.yml' import render_recipe_fulfilment %}
{% for recipe in grocy.recipes %}
name: {{ recipe.fields.name }}
servings: {{ recipe.fields.base_servings }}
required_fulfilled: {{ render_recipe_fulfilment(recipe.fields.fulfillment, grocy.meta.fa_icons) }}
description: |-
{{ recipe.fields.description }}
{% if loop.nextitem %}
---
{%endif%}
{%endfor %}

View File

@ -0,0 +1,17 @@
{% macro render_stock_fulfilment(ingredient, fa_icons) -%}
{% if ingredient.need_fulfilled == "1" %} {{ 'Enough in stock' }} {{ fa_icons['check'] }}
{% elif ingredient.need_fulfilled_with_shopping_list == "1" %} {{ 'Not enough in stock, %s missing %s already in stock' | format(ingredient.missing_amount, ingredient.amount_on_shopping_list | int) }} {{ fa_icons['exclamation'] }}
{% else %} {{ 'Not enough in stock, %s missing' | format(ingredient.missing_amount) }} {{ fa_icons['times'] }}
{% endif %}
{%- endmacro %}
{% macro render_recipe_fulfilment(recipe, fa_icons) -%}
{% if recipe.need_fulfilled == "1" %} {{ 'Enough in stock' }} {{ fa_icons['check'] }}
{% else %} {{ 'Not enough in stock, %s ingredients missing' | format(recipe.missing_products_count) }} {{ fa_icons['times'] }}
{% endif %}
{%- endmacro %}
{% macro ingredient_label(ingredient) -%}
{{ ingredient.recipe_amount }} {% if ingredient.recipe_amount | int > 1 %} {{ ingredient.quantity_unit.name_plural }} {% else %} {{ ingredient.quantity_unit.name }} {% endif %} {{ ingredient.name }}
{%- endmacro %}

View File

@ -1,17 +1,10 @@
{% from 'recipe/macro.yml' import ingredient_label, render_stock_fulfilment %}
name: {{ grocy.fields.name }}
servings: {{ grocy.fields.base_servings }}
costs: {{ grocy.fields.fulfillment.costs }}
ingredients: {% for ingredient in grocy.fields.ingredients %}
- product_id: {{ ingredient.id }}
name: {{ ingredient.name}}
description: {{ ingredient.description | default(null) }}
note: {{ ingredient.note }}
amount: {{ ingredient.amount }}
qu_id: {{ ingredient.qu_id }}
only_check_single_unit_in_stock: {{ ingredient.only_check_single_unit_in_stock }}
ingredient_group: {{ ingredient.ingredient_group | default(null) }}
not_check_stock_fulfillment: {{ ingredient.not_check_stock_fulfillment }}{% endfor %}
ingredients: {% for ingredient in grocy.meta.recipes.ingredients.valid_values %}
{{ ingredient_label(ingredient) }} {{ render_stock_fulfilment(ingredient, grocy.meta.fa_icons) }}
note: {{ ingredient.note }} {% endfor %}
description: |-
{{ grocy.fields.description }}