diff --git a/file.txt b/file.txt deleted file mode 100644 index 06c46ea..0000000 --- a/file.txt +++ /dev/null @@ -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//") - - - diff --git a/grocy/cli.py b/grocy/cli.py index 712981e..65ec49e 100644 --- a/grocy/cli.py +++ b/grocy/cli.py @@ -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 @@ -62,7 +63,7 @@ def main(): @main.command() -@click.pass_context +@click.pass_context def stock(ctx): logger = logging.getLogger('cli.stock') if ctx.invoked_subcommand is None: @@ -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 @@ -221,7 +232,8 @@ def add(template): @click.pass_context def recipe(ctx): pass - + + @recipe.command() @click.pass_context @click.argument('recipe_id', required=False) @@ -234,43 +246,158 @@ 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'] + html_markup_description = data['fields']['description'] plain_text_description = html2text(html_markup_description) - data['fields']['description'] = plain_text_description + 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): diff --git a/grocy/conf.py b/grocy/conf.py index 891d919..510c3a4 100644 --- a/grocy/conf.py +++ b/grocy/conf.py @@ -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: diff --git a/grocy/entity.py b/grocy/entity.py index 7b98b13..c49baa8 100644 --- a/grocy/entity.py +++ b/grocy/entity.py @@ -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: diff --git a/grocy/meta.py b/grocy/meta.py index c236475..02ab6a7 100644 --- a/grocy/meta.py +++ b/grocy/meta.py @@ -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 diff --git a/grocy/recipe.py b/grocy/recipe.py index e65e5b4..ddd764b 100644 --- a/grocy/recipe.py +++ b/grocy/recipe.py @@ -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 - - - - - - - - - - - - diff --git a/grocy/schema.py b/grocy/schema.py index c7771d3..42ea355 100644 --- a/grocy/schema.py +++ b/grocy/schema.py @@ -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' } diff --git a/grocy/util.py b/grocy/util.py index a3a7884..6b19c2a 100644 --- a/grocy/util.py +++ b/grocy/util.py @@ -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 diff --git a/templates/recipe/list.yml b/templates/recipe/list.yml new file mode 100644 index 0000000..c1182a5 --- /dev/null +++ b/templates/recipe/list.yml @@ -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 %} diff --git a/templates/recipe/macro.yml b/templates/recipe/macro.yml new file mode 100644 index 0000000..b515254 --- /dev/null +++ b/templates/recipe/macro.yml @@ -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 %} diff --git a/templates/recipe/view.yml b/templates/recipe/view.yml index ed1d1be..abbb2fa 100644 --- a/templates/recipe/view.yml +++ b/templates/recipe/view.yml @@ -1,17 +1,10 @@ +{% from 'recipe/macro.yml' import ingredient_label, render_stock_fulfilment %} name: {{ grocy.fields.name }} -servings: {{ grocy.fields.base_servings }} +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 }}