import click from markdown import markdown from html2text import html2text from jinja2 import Environment, FileSystemLoader from grocy.meta import Meta from grocy.conf import Configuration from grocy.util import Util from grocy.recipe import Recipe from grocy.table import Table from grocy.entity import Entity from grocy.stock import Stock from grocy.schema import get_schema import yaml from sys import exit from os import path import logging APP_NAME = 'grocy-cli' SAMPLE_CONFIG_FILE = 'sample.config.yml' TMP_DIR = '/tmp/grocy' CONFIG_FILE = 'config.yml' 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 if is_subcommand: ctx.forward(self.commands[args[0]], args[1:]) super(GrocyGroup, self).parse_args(ctx, args) @click.group() def main(): cfg = Configuration() if not cfg.exists: no_config_msg = 'A config file was not found. A sample configuration file' no_config_msg += ' will be created under {}. Is that Ok?'.format(cfg.CONFIG_DIR) create_config_app_dir = click.confirm(no_config_msg) user_cfg_options = {} if create_config_app_dir: user_cfg_options['logger_level'] = click.prompt('Enter logger level', default='DEBUG') user_cfg_options['logger_file_location'] = click.prompt('Enter location for logger', default=path.expanduser('~/.config/grocy/log')) user_cfg_options['api'] = click.prompt('Enter the grocy api url', default='https://demo-en.grocy.info/api') user_cfg_options['token'] = click.prompt('Enter the grocy token ', default='') user_cfg_options['col_format'] = click.prompt('Enter the col position for rendering tables', default='col') user_cfg_options['table_format'] = click.prompt('Enter the table format', default='simple') cfg.create(user_cfg_options) else: exit(0) else: cfg.load() logging.basicConfig(level=cfg.logger_level, filename=cfg.logger_file_location) @main.command() @click.pass_context def stock(ctx): logger = logging.getLogger('cli.stock') if ctx.invoked_subcommand is None: try: stock = Stock() table = Table(stocks=stock.products) click.echo(table.stock) except Exception as e: logger.error(e) raise e @main.group() def product(): pass @product.command() @click.pass_context @click.argument('product_id', required=False) def view(ctx, product_id): logger = logging.getLogger('cli.product') try: if product_id: stock = Stock() product = stock.get_product(product_id) table = Table(entry=product) click.echo(table.product) else: click.echo(ctx.get_help()) except Exception as e: logger.error(e) raise e @product.command() @click.option('--name', '-n', 'name') @click.argument('product_id', required=False) def edit(product_id, name): logger = logging.getLogger('cli.product.edit') try: cfg = Configuration() util = Util(cfg=cfg) cfg.load() loaded_template = cfg.templates('product/edit') entity = Entity(name='products') if product_id: product = entity.get(id=product_id) edited_product = click.edit(loaded_template.render(product)) edited_product.update() elif name: # Convert name args to a single string string_name_arg = ' '.join(name) if type(name) == list else name products = entity.find({'name': string_name_arg}) if products is None: click.echo('Could not find product') return edited_products = click.edit(loaded_template.render(products=products), extension='.yml') if edited_products is None: return parsed_edited_products = util.load_yaml(edited_products) schema = entity.schema for index, edited_product in enumerate(parsed_edited_products): edited_product['id'] = products[index]['id'] util.verify_integrity(edited_product, schema) entity.update(edited_product, id=products[index]['id']) else: raise click.BadParameter('Missing PRODUCT_ID or QUERY') except Exception as e: logger.error(e) raise e @product.command() @click.option('--name', '-n', 'name') @click.option('-t', 'template') def list(name, template): logger = logging.getLogger('cli.product.list') cfg = Configuration() cfg.load() product_entity = Entity(name='products') qu_entity = Entity(name='quantity_units') location_entity = Entity(name='locations') product_group_entity = Entity(name='product_groups') try: if name: # 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() entries = {} entries['quantity_units'] = qu_entity.get() entries['product_groups'] = product_group_entity.get() entries['locations'] = location_entity.get() entries['products'] = products table = Table(entries=entries) click.echo_via_pager(table.products) except Exception as e: logger.error(e) raise e @product.command() @click.argument('product_id') def browse(product_id): logger = logging.getLogger('cli.product.browse') try: cfg = Configuration() cfg.load() url = '{domain}/product/{product_id}'.format(domain=cfg.domain, product_id=product_id) click.launch(url, wait=False) except Exception as e: logger.error(e) raise e @product.command() @click.option('-t', 'template') def add(template): logger = logging.getLogger('cli.product.add') try: cfg = Configuration() cfg.load() logger = logging.getLogger('cli.product.add') meta = Meta() # Get product_groups entity = Entity(name='product_groups') product_groups = entity.get() meta.add(type='entities', name='product_groups', valid_values=product_groups) # Get locations entity = Entity(name='locations') locations = entity.get() meta.add(type='entities', name='locations', valid_values=locations) # Get 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=data), extension='.yml') if not new_product: return parsed_new_product = yaml.safe_load(new_product) if template == 'debug': click.echo(parsed_new_product) return entity = Entity(name='products') entity.create(parsed_new_product) except Exception as e: logger.error(e) raise e @main.group() @click.pass_context def recipe(ctx): pass @recipe.command() @click.pass_context @click.argument('recipe_id', required=False) @click.option('-t', 'template') def view(ctx, recipe_id, template): logger = logging.getLogger('cli.recipes.view') try: cfg = Configuration() cfg.load() 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'] plain_text_description = html2text(html_markup_description) data['fields']['description'] = plain_text_description recipe = Recipe(id=recipe_id) 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') click.echo(loaded_template.render(grocy=data)) else: click.echo(ctx.get_help()) 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) # TODO: revist this command @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() if template: loaded_template = cfg.templates(template) else: loaded_template = cfg.templates('recipe/edit') 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 # Build hydrated recipe entity object for editing 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 schema = [] schema.append(Entity(name='products').schema) schema.append(Entity(name='locations').schema) schema.append(Entity(name='quantity_units').schema) schema.append(Entity(name='recipes').schema) parsed_edited_recipes = util.load_yaml(edited_recipes) for index, edited_recipe in enumerate(parsed_edited_recipes): schema = entity.schema edited_recipe['id'] = recipe_entities[index]['id'] util.verify_integrity(edited_recipe, schema) entity.update(edited_recipe, id=edited_recipe['id']) else: raise click.BadParameter('Missing RECIPE_ID or QUERY') except Exception as e: logger.error(e) raise e @recipe.command() @click.argument('recipe_id', required=True) @click.option('-t', 'template') def add_ingredient(recipe_id, template): logger = logging.getLogger('cli.recipe.add_ingredient') try: cfg = Configuration() util = Util(cfg=cfg) cfg.load() if template: loaded_template = cfg.templates(template) else: loaded_template = cfg.templates('recipe/add-ingredient') # Verify that recipe exist try: entity = Entity(name='recipes') entity.get(id=recipe_id) except Exception: raise Exception('Could not find recipe {}'.format(recipe_id)) # TODO: Meta has to be added to validate against know fields/properties # Add quantity_units to meta ingredient = click.edit(loaded_template.render(), extension='.yml') if ingredient is None: return parsed_new_ingredient = util.load_yaml(ingredient)[0] parsed_new_ingredient['recipe_id'] = recipe_id if template == 'debug': click.echo(parsed_new_ingredient) return entity = Entity(name='recipes_pos') entity.create(parsed_new_ingredient) except Exception as e: logger.error(e) raise e @recipe.command() @click.argument('recipe_id', required=True) @click.argument('ingredient_id', required=True) def remove_ingredient(recipe_id, ingredient_id): logger = logging.getLogger('cli.recipe.remove_ingredient') try: entity = Entity(name='recipes_pos') entity.delete(ingredient_id) except Exception as e: logger.error(e) raise e #@main.command() #def shopping(): # logger = logging.getLogger('cli.shopping') # try: # entity = Entity(name='shopping_list') # shopping_list = entity.get() # table = Table(shopping_list=shopping_list) # click.echo(table.shopping_list) # except Exception as e: # logger.error(e) # raise e # # #@main.command() #@main.group() #def ingredient(): # pass # # #@ingredient.command('add') #@click.argument('query') #@click.argument('recipe_id') #@click.option('--amount', '-a', 'amount', default=1, type=int) #@click.option('--group', '-g', type=str, default='') #@click.option('--variable-amount', '--va', 'variable_amount', default=None, type=float) #@click.option('--in-stock', '--is', 'in_stock', default=False) #@click.option('--disable-fulfillment', '--df', 'disable_fulfillment', default=False) #@click.option('--note', '-n', 'note', multiple=True, default='', type=str) #@click.option('--no-edit', '--ne', 'no_edit', default=False) #def add(query, recipe_id, amount, group, variable_amount, in_stock, disable_fulfillment, note, no_edit): # logger = logging.getLogger('cli.ingredient.add') # # try: # loaded_template = TEMPLATE_LOADER.get_template('ingredient_add.yml') # new_ingredient = {} # # entity = Entity(name='recipes') # recipe = entity.get(id=recipe_id) # # if not recipe: # raise click.BadParameter(message='recipe {id} does not exist', param='recipe_id', # param_hint='Use `grocy recipes ls` to get a list of recipes') # # entity = Entity(name='products') # product = entity.findOne(query) # new_ingredient['product_id'] = product['id'] # # new_ingredient['amount'] = amount # new_ingredient['group'] = group # new_ingredient['variable_amount'] = variable_amount # new_ingredient['only_check_single_unit_in_stock'] = "1" if in_stock else "0" # new_ingredient['not_check_stock_fulfillment'] = "1" if disable_fulfillment else "0" # new_ingredient['note'] = note # # if not no_edit: # new_ingredient = click.edit(loaded_template.render(new_ingredient)) # # parsed_new_ingredient = yaml.safe_load(new_ingredient) # entity = Entity(name='recipes_pos') # #entity.create(parsed_new_ingredient) # # except Exception as e: # logger.error(e) # raise e # # #@recipe.command('create') #@click.pass_context #def create(ctx): # logger = logging.getLogger('cli.recipe.create') # # try: # recipe = Entity(name='recipes') # loaded_template = TEMPLATE_LOADER.get_template('recipe_add.yml') # new_recipe = click.edit(loaded_template.render()) # if new_recipe is not None: # parsed_new_recipe = yaml.safe_load(new_recipe) # parsed_new_recipe['description'] = markdown(parsed_new_recipe['description']) # recipe.__dict__.update(parsed_new_recipe) # recipe.create() # except Exception as e: # logger.error(e) # raise e # #@main.command() #@click.pass_context #def chore(ctx): # cfg = ctx.obj['cfg'] # # chore = Chore(**cfg) # chores = chore.get_list() # click.echo(chores) # #@main.command() #@click.pass_context #def task(ctx): # cfg = ctx.obj['cfg'] # # task = Task(**cfg) # tasks = task.get_list() # click.echo(tasks) # click.echo(tasks)