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__))) PROJ_DIR = path.join(path.dirname(path.realpath(__file__)))
TEMPLATE_LOADER = Environment(loader=FileSystemLoader('templates'), trim_blocks=True, lstrip_blocks=True) TEMPLATE_LOADER = Environment(loader=FileSystemLoader('templates'), trim_blocks=True, lstrip_blocks=True)
class GrocyGroup(click.Group): class GrocyGroup(click.Group):
def parse_args(self, ctx, args): def parse_args(self, ctx, args):
is_subcommand = True if args[0] in self.commands else False is_subcommand = True if args[0] in self.commands else False
@ -97,6 +98,7 @@ def view(ctx, product_id):
logger.error(e) logger.error(e)
raise e raise e
@product.command() @product.command()
@click.option('--name', '-n', 'name') @click.option('--name', '-n', 'name')
@click.argument('product_id', required=False) @click.argument('product_id', required=False)
@ -124,7 +126,7 @@ def edit(product_id, name):
if edited_products is None: if edited_products is None:
return return
parsed_edited_products = Util.load_yaml(edited_products) parsed_edited_products = util.load_yaml(edited_products)
schema = entity.schema schema = entity.schema
for index, edited_product in enumerate(parsed_edited_products): for index, edited_product in enumerate(parsed_edited_products):
@ -154,6 +156,8 @@ def list(name, template):
# Convert name args to a single string # Convert name args to a single string
string_name_arg = ' '.join(name) if type(name) == list else name string_name_arg = ' '.join(name) if type(name) == list else name
products = product_entity.find({'name': string_name_arg}) products = product_entity.find({'name': string_name_arg})
if len(products) == 0:
return
else: else:
products = product_entity.get() products = product_entity.get()
@ -193,16 +197,23 @@ def add(template):
logger = logging.getLogger('cli.product.add') logger = logging.getLogger('cli.product.add')
meta = Meta() meta = Meta()
# Get product_groups # 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 # 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 # 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: if template:
loaded_template = cfg.templates(template) loaded_template = cfg.templates(template)
else: else:
loaded_template = cfg.templates('product/add') 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: if not new_product:
return return
@ -222,6 +233,7 @@ def add(template):
def recipe(ctx): def recipe(ctx):
pass pass
@recipe.command() @recipe.command()
@click.pass_context @click.pass_context
@click.argument('recipe_id', required=False) @click.argument('recipe_id', required=False)
@ -234,7 +246,10 @@ def view(ctx, recipe_id, template):
data = {'fields': {}} data = {'fields': {}}
if recipe_id: if recipe_id:
entity = Entity(name='recipes') entity = Entity(name='recipes')
meta = Meta()
data['fields'] = entity.get(id=recipe_id) data['fields'] = entity.get(id=recipe_id)
recipe = Recipe(id=recipe_id)
data['fields']['fulfillment'] = recipe.fulfillment
# Change html markup to plain text # Change html markup to plain text
html_markup_description = data['fields']['description'] html_markup_description = data['fields']['description']
@ -242,35 +257,147 @@ def view(ctx, recipe_id, template):
data['fields']['description'] = plain_text_description data['fields']['description'] = plain_text_description
recipe = Recipe(id=recipe_id) 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: if template:
loaded_template = cfg.templates(template) loaded_template = cfg.templates(template)
else: else:
loaded_template = cfg.templates('recipe/view') loaded_template = cfg.templates('recipe/view')
entity = Entity(name='recipes')
click.echo(loaded_template.render(grocy=data)) click.echo(loaded_template.render(grocy=data))
else: else:
click.echo(ctx.get_help()) click.echo(ctx.get_help())
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
raise e raise e
#@recipe.command()
#def list(): @recipe.command()
# logger = logging.getLogger('cli.recipe') @click.option('--name', '-n', 'name')
# try: @click.option('-t', 'template')
# entity = Entity(name='recipes') def list(name, template):
# recipes = entity.get() logger = logging.getLogger('cli.recipe.list')
# recipe = Recipe( cfg = Configuration()
# ingredient_reqs = recipe.get_ingredient_requirements() cfg.load()
# table = Table(recipes=recipes, recipes_reqs=recipes_reqs, ingredient_reqs=ingredient_reqs) try:
# click.echo(table.recipe) entity = Entity(name='recipes')
# except Exception as e: if name:
# logger.error(e) # Convert name args to a single string
# raise e 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() #@main.command()
#def shopping(): #def shopping():
# logger = logging.getLogger('cli.shopping') # logger = logging.getLogger('cli.shopping')
@ -337,27 +464,6 @@ def view(ctx, recipe_id, template):
# raise e # 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') #@recipe.command('create')
#@click.pass_context #@click.pass_context
#def create(ctx): #def create(ctx):

View File

@ -99,7 +99,7 @@ class Configuration(object):
def templates(self, name): def templates(self, name):
try: try:
TEMPLATE_LOADER = Environment(loader=FileSystemLoader([self.USER_TEMPLATE_DIR, self.PROJ_TEMPLATE_DIR]), 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)) return TEMPLATE_LOADER.get_template('{}.{}'.format(name, self.TEMPLATE_EXT))
except Exception as e: except Exception as e:

View File

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

View File

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

View File

@ -1,5 +1,8 @@
from grocy.request import Request from grocy.request import Request
from html2text import html2text
from grocy.stock import Stock
from grocy.conf import Configuration from grocy.conf import Configuration
from copy import deepcopy
from grocy.entity import Entity from grocy.entity import Entity
import logging import logging
@ -7,17 +10,87 @@ import logging
class Recipe(object): class Recipe(object):
GET_RECIPES_FULFILLMENT_URL_TEMPLATE = '{domain}/api/recipes/fulfillment' GET_RECIPES_FULFILLMENT_URL_TEMPLATE = '{domain}/api/recipes/fulfillment'
GET_RECIPE_FULFILLMENT_URL_TEMPLATE = '{domain}/api/recipes/{recipeId}/fulfillment' GET_RECIPE_FULFILLMENT_URL_TEMPLATE = '{domain}/api/recipes/{recipeId}/fulfillment'
GET_RECIPES_POS_FULFILLMENT_URL_TEMPLATE = '{domain}/api/recipes/pos/fulfillment' GET_RECIPES_POS_FULFILLMENT_URL_TEMPLATE = '{domain}/api/recipes/{recipeId}/pos/fulfillment'
GET_RECIPE_POS_FULFILLMENT_URL_TEMPLATE = '{domain}/api/recipes/{recipeId}/pos/{recipeId}/fulfillment' GET_RECIPE_POS_FULFILLMENT_URL_TEMPLATE = '{domain}/api/recipes/{recipeId}/pos/{recipePosId}/fulfillment'
def __init__(self, id=None): def __init__(self, id=None):
self.conf = Configuration() self.conf = Configuration()
self.conf.load() self.conf.load()
self.id = id self.id = id
def get_ingredient_requirements(self, recipe_id=None): @property
logger = logging.getLogger('recipe.get_ingredient_requirements') 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): def get_fulfillments(self):
logger = logging.getLogger('recipe.get_requirements') logger = logging.getLogger('recipe.get_requirements')
@ -31,24 +104,3 @@ class Recipe(object):
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
raise 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_URL_TEMPLATE = '{domain}/api/openapi/specification'
SCHEMA_MODEL_MAP = { SCHEMA_MODEL_MAP = {
'products': 'Product', 'products': 'Product',
'product_details': 'ProductDetailsResponse',
'stock': 'StockEntry', 'stock': 'StockEntry',
'product_groups': 'ProductGroup', 'product_groups': 'ProductGroup',
'locations': 'Location', 'locations': 'Location',
'quantity_units': 'QuantityUnit', 'quantity_units': 'QuantityUnit',
'recipe_requirements': 'RecipeFulfilmentResponse' 'recipe_fulfillment': 'RecipeFulfillmentResponse'
} }

View File

@ -11,8 +11,7 @@ class Util(object):
self.cfg = cfg self.cfg = cfg
yaml.SafeLoader.add_constructor("tag:yaml.org,2002:python/unicode", _yaml_constructor) yaml.SafeLoader.add_constructor("tag:yaml.org,2002:python/unicode", _yaml_constructor)
def load_yaml(self, data):
def load_yaml(data):
generator = yaml.safe_load_all(data) generator = yaml.safe_load_all(data)
data_list = list(generator) data_list = list(generator)
return data_list 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 }} name: {{ grocy.fields.name }}
servings: {{ grocy.fields.base_servings }} servings: {{ grocy.fields.base_servings }}
costs: {{ grocy.fields.fulfillment.costs }} costs: {{ grocy.fields.fulfillment.costs }}
ingredients: {% for ingredient in grocy.fields.ingredients %} ingredients: {% for ingredient in grocy.meta.recipes.ingredients.valid_values %}
- product_id: {{ ingredient.id }} {{ ingredient_label(ingredient) }} {{ render_stock_fulfilment(ingredient, grocy.meta.fa_icons) }}
name: {{ ingredient.name}} note: {{ ingredient.note }} {% endfor %}
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 %}
description: |- description: |-
{{ grocy.fields.description }} {{ grocy.fields.description }}