563 lines
19 KiB
Python
563 lines
19 KiB
Python
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)
|