grocy-cli/grocy/cli.py

789 lines
25 KiB
Python
Raw Normal View History

import logging
import click
2019-04-28 16:12:05 -05:00
from markdown import markdown
from html2text import html2text
2019-04-01 00:16:32 -05:00
from jinja2 import Environment, FileSystemLoader
from grocy.meta import Meta
2019-06-16 23:54:10 -05:00
from grocy.conf import Configuration
from grocy.util import Util
2019-06-16 23:54:10 -05:00
from grocy.recipe import Recipe
from grocy.table import Table
from grocy.entity import Entity
from grocy.chore import Chore
2019-06-16 23:54:10 -05:00
from grocy.stock import Stock
from grocy.schema import get_schema
from grocy.shoppinglist import ShoppingList
import yaml
from sys import exit
2019-06-16 23:54:10 -05:00
from os import path
APP_NAME = 'grocy-cli'
SAMPLE_CONFIG_FILE = 'sample.config.yml'
2019-04-01 00:16:32 -05:00
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__)))
2019-06-16 23:54:10 -05:00
TEMPLATE_LOADER = Environment(loader=FileSystemLoader('templates'), trim_blocks=True, lstrip_blocks=True)
class GrocyGroup(click.Group):
def parse_args(self, ctx, args):
2019-10-30 00:33:59 -05:00
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()
2019-06-16 23:54:10 -05:00
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)
2019-06-16 23:54:10 -05:00
user_cfg_options = {}
if create_config_app_dir:
2019-06-16 23:54:10 -05:00
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)
2019-06-16 23:54:10 -05:00
else:
cfg.load()
2019-03-09 19:02:42 -06:00
2019-06-16 23:54:10 -05:00
logging.basicConfig(level=cfg.logger_level, filename=cfg.logger_file_location)
@main.command()
@click.pass_context
2019-03-09 19:02:42 -06:00
def stock(ctx):
2019-06-16 23:54:10 -05:00
logger = logging.getLogger('cli.stock')
if ctx.invoked_subcommand is None:
try:
stock = Stock()
table = Table(stocks=stock.products)
2019-06-16 23:54:10 -05:00
click.echo(table.stock)
except Exception as e:
logger.error(e)
raise e
2019-03-09 19:02:42 -06:00
@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')
2019-06-16 23:54:10 -05:00
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')
2019-06-16 23:54:10 -05:00
except Exception as e:
logger.error(e)
raise e
2019-03-09 19:50:42 -06:00
@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')
2019-04-01 00:16:32 -05:00
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)
2019-04-01 00:16:32 -05:00
except Exception as e:
logger.error(e)
2019-06-16 23:54:10 -05:00
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 create(template):
logger = logging.getLogger('cli.product.create')
try:
cfg = Configuration()
cfg.load()
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/create')
created_product = click.edit(loaded_template.render(grocy=data), extension='.yml')
if not created_product:
return
parsed_created_product = yaml.safe_load(created_product)
if template == 'debug':
click.echo(parsed_created_product)
return
entity = Entity(name='products')
entity.create(parsed_created_product)
except Exception as e:
logger.error(e)
raise e
2019-10-30 00:33:59 -05:00
@main.group()
@click.pass_context
def recipe(ctx):
pass
@recipe.command()
def create(template):
logger = logging.getLogger('cli.recipe.create')
try:
cfg = Configuration()
recipe = Entity(name='recipes')
if template:
loaded_template = cfg.templates(template)
else:
loaded_template = cfg.templates('recipe/create')
created_recipe = click.edit(loaded_template.render(),
extension='.yml')
if created_recipe is None:
return
parsed_created_recipe = util.load_yaml(created_recipe)[0]
if template == 'debug':
click.echo(parsed_created_recipe)
return
shopping_lists.create(parsed_created_recipe)
except Exception as e:
raise e
@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
2019-10-30 00:33:59 -05:00
@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
2019-10-30 00:33:59 -05:00
@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.group()
@click.pass_context
def shopping(ctx):
pass
@shopping.command()
@click.pass_context
@click.argument('shopping_id', required=False)
@click.option('-t', 'template')
def view(ctx, shopping_id, template):
logger = logging.getLogger('cli.shopping.view')
try:
cfg = Configuration()
cfg.load()
data = {'fields': {'shopping_list_items': []}}
if shopping_id:
entity = Entity(name='shopping_list')
all_shopping_list_items = entity.get()
shopping_list_items = [s for s in all_shopping_list_items if s['shopping_list_id'] == shopping_id]
if shopping_list_items is None:
return
entity = Entity(name='products')
products = entity.get()
products_map = {product['id']: product for product in products}
for shopping_list_item in shopping_list_items:
shopping_product_item = products_map[shopping_list_item['product_id']]
shopping_list_item['product'] = shopping_product_item
data['fields']['shopping_list_items'].append(shopping_list_item)
if template:
loaded_template = cfg.templates(template)
else:
loaded_template = cfg.templates('shopping/view')
click.echo(loaded_template.render(grocy=data))
else:
click.echo(ctx.get_help())
except Exception as e:
logger.error(e)
raise e
@shopping.command()
@click.option('--name', '-n', 'name')
@click.option('-t', 'template')
def list(name, template):
logger = logging.getLogger('cli.shopping.list')
cfg = Configuration()
cfg.load()
try:
entity = Entity(name='shopping_lists')
if name:
# Convert name args to a single string
string_name_arg = ' '.join(name) if type(name) == list else name
shopping_list_entities = entity.find({'name': string_name_arg})
if len(shopping_list_entities) == 0:
return
else:
shopping_list_entities = entity.get()
data = {'fields': {'shopping_lists': shopping_list_entities}}
if template:
loaded_template = cfg.templates(template)
else:
loaded_template = cfg.templates('shopping/list')
click.echo(loaded_template.render(grocy=data))
except Exception as e:
logger.error(e)
raise e
@shopping.command()
@click.argument('shopping_id')
def browse(shopping_id):
logger = logging.getLogger('cli.shopping.browse')
try:
cfg = Configuration()
cfg.load()
url = '{domain}/shoppinglist?list={shopping_id}'.format(domain=cfg.domain, shopping_id=shopping_id)
click.launch(url, wait=False)
except Exception as e:
logger.error(e)
@shopping.command()
@click.option('-t', 'template')
def create(template):
logger = logging.getLogger('cli.shopping.create')
try:
cfg = Configuration()
cfg.load()
util = Util(cfg=cfg)
shopping_lists = Entity(name='shopping_lists')
if template:
loaded_template = cfg.templates(template)
else:
loaded_template = cfg.templates('shopping/create')
created_shopping_list = click.edit(loaded_template.render(),
extension='.yml')
if created_shopping_list is None:
return
parsed_created_shopping_list = util.load_yaml(created_shopping_list)[0]
if template == 'debug':
click.echo(parsed_created_shopping_list)
return
shopping_lists.create(parsed_created_shopping_list)
except Exception as e:
raise e
@shopping.command()
@click.argument('shopping_id')
def delete(shopping_id):
logger = logging.getLogger('cli.shopping.delete')
try:
cfg = Configuration()
cfg.load()
util = Util(cfg=cfg)
shopping_lists = Entity(name='shopping_lists')
shopping_lists.delete(shopping_id)
except Exception as e:
raise e
@shopping.command()
@click.argument('shopping_id')
def clear(shopping_id):
logger = logging.getLogger('cli.shopping.clear')
try:
cfg = Configuration()
cfg.load()
util = Util(cfg=cfg)
# Validate that shopping list exists
entity = Entity(name='shopping_lists')
found_shopping_list = entity.get(id=shopping_id)
if found_shopping_list is None:
raise Exception('Shopping list does not exist')
shopping_list = ShoppingList(id=shopping_id)
shopping_list.clear()
except Exception as e:
raise e
@shopping.command()
@click.argument('shopping_id')
@click.option('-t', 'template')
def add_product(shopping_id, template):
logger = logging.getLogger('cli.shopping.add_product')
try:
cfg = Configuration()
cfg.load()
util = Util(cfg=cfg)
# Validate that shopping list exists
entity = Entity(name='shopping_lists')
found_shopping_list = entity.get(id=shopping_id)
if found_shopping_list is None:
raise Exception('Shopping list does not exist')
shopping_list = ShoppingList(id=shopping_id)
if template:
loaded_template = cfg.templates(template)
else:
loaded_template = cfg.templates('shopping/add_product')
added_product = click.edit(loaded_template.render(), extension='.yml')
if added_product is None:
return
parsed_added_product = util.load_yaml(added_product)[0]
if template == 'debug':
# TODO: Reserve for meta
return
shopping_list.add_product(parsed_added_product)
except Exception as e:
raise e
@shopping.command()
@click.argument('shopping_id')
@click.option('-t', 'template')
def remove_product(shopping_id, template):
logger = logging.getLogger('cli.shopping.remove_product')
try:
cfg = Configuration()
cfg.load()
util = Util(cfg=cfg)
shopping_list = ShoppingList(id=shopping_id)
if template:
loaded_template = cfg.templates(template)
else:
loaded_template = cfg.templates('shopping/remove_product')
remove_product = click.edit(loaded_template.render(), extension='.yml')
if remove_product is None:
return
parsed_removed_product = util.load_yaml(remove_product)[0]
if template == 'debug':
# TODO: Reserve for meta
return
shopping_list.remove_product(parsed_removed_product)
except Exception as e:
raise e
# TODO: Figure out why this command is not working for new shopping lists
#@shopping.command()
#@click.argument('shopping_id')
#@click.option('-t', 'template')
#@click.option('recipe_id', 'r')
#def add_missing_products(shopping_id, recipe_id, template):
# logger = logging.getLogger('cli.shopping.add_missing_products')
# try:
# cfg = Configuration()
# cfg.load()
# util = Util(cfg=cfg)
#
# # Validate that shopping list exists
# entity = Entity(name='shopping_lists')
# found_shopping_list = entity.get(id=shopping_id)
# if found_shopping_list is None:
# raise Exception('Shopping list does not exist')
#
# shopping_list = ShoppingList(id=shopping_id)
# except Exception as e:
# raise e
@main.group()
def chore():
pass
@chore.command()
@click.option('--name', '-n', 'name')
@click.option('-t', 'template')
def list(name, template):
logger = logging.getLogger('cli.shopping.list')
cfg = Configuration()
cfg.load()
try:
entity = Entity(name='chores')
if name:
chores = entity.find_by_name(name)
if len(chores) == 0:
return
else:
chores = entity.get()
data = {'chores': []}
for chore in chores:
chore_details = Chore(chore['id'])
chore.update(chore_details.execution_times)
data['chores'].append({'fields': chore})
if template:
loaded_template = cfg.templates(template)
else:
loaded_template = cfg.templates('chore/list')
click.echo(loaded_template.render(grocy=data))
except Exception as e:
logger.error(e)
raise e
@chore.command()
@click.option('-t', 'template')
def create(template):
logger = logging.getLogger('cli.chore.create')
try:
cfg = Configuration()
cfg.load()
entity = Entity(name='chores')
if template:
loaded_template = cfg.templates(template)
else:
loaded_template = cfg.templates('chore/create')
created_chore = click.edit(loaded_template.render(), extension='.yml')
if not created_chore:
return
parsed_created_chore = yaml.safe_load(created_chore)
if template == 'debug':
click.echo(parsed_created_chore)
return
entity.create(parsed_created_chore)
except Exception as e:
logger.error(e)
raise e