feat: Added ability to edit multiple products

- refactor: Changed templates to use yml extension
This commit is contained in:
Aerex 2019-06-25 00:52:17 -05:00
parent 1f7a061600
commit d479b4f621
12 changed files with 365 additions and 148 deletions

View File

@ -2,6 +2,7 @@ import click
from markdown import markdown
from jinja2 import Environment, FileSystemLoader
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
@ -20,6 +21,13 @@ 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():
@ -65,16 +73,20 @@ def stock(ctx):
raise e
@main.group(invoke_without_command=True)
@main.group()
def product():
pass
@product.command()
@click.pass_context
@click.argument('product_id', required=False)
def product(ctx, product_id):
def view(ctx, product_id):
logger = logging.getLogger('cli.product')
try:
if product_id:
stock = Stock()
product = stock.get_product(product_id)
# Need to get quantity_unit
table = Table(entry=product)
click.echo(table.product)
else:
@ -83,148 +95,203 @@ def product(ctx, product_id):
logger.error(e)
raise e
#@product.command('list')
#@click.argument('query', required=False)
#def view(query, product_id):
# logger = logging.getLogger('cli.product.view')
@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')
def list(name):
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})
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(table.products)
except Exception as e:
logger.error(e)
raise e
#@main.command()
#def shopping():
# logger = logging.getLogger('cli.shopping')
# try:
# if product_id:
# entity = Entity(name='product')
# product = entity.get(id=product_id)
# table = Table(product=product)
# click.echo(table.product)
# else:
# 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()
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.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
@main.group()
@click.pass_context
def recipe(ctx):
pass
@recipe.command('ls')
def ls():
logger = logging.getLogger('cli.recipe')
try:
entity = Entity(name='recipes')
recipes = entity.get()
recipe = Recipe()
recipes_reqs = recipe.get_requirements()
table = Table(recipes=recipes, recipes_reqs=recipes_reqs)
click.echo(table.recipe)
except Exception as e:
logger.error(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')
@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()
#@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
#
#
#@main.group()
#@click.pass_context
#def recipe(ctx):
# pass
#
#
#@recipe.command('ls')
#def ls():
# logger = logging.getLogger('cli.recipe')
# try:
# entity = Entity(name='recipes')
# recipes = entity.get()
# recipe = Recipe()
# recipes_reqs = recipe.get_requirements()
# table = Table(recipes=recipes, recipes_reqs=recipes_reqs)
# click.echo(table.recipe)
# except Exception as e:
# logger.error(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')
#@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
@ -243,3 +310,4 @@ def create(ctx):
# task = Task(**cfg)
# tasks = task.get_list()
# click.echo(tasks)
# click.echo(tasks)

View File

@ -1,4 +1,5 @@
from os import path, chmod, makedirs
from os import path, chmod, makedirs, pardir
from pathlib import Path
from shutil import copy
from yaml import safe_load, dump
from jinja2 import Environment, FileSystemLoader
@ -10,7 +11,8 @@ class Configuration(object):
CONFIG_DIR = path.expanduser('~/.config/grocy')
USER_TEMPLATE_DIR = path.expanduser('~/.config/grocy/templates')
CONFIG_FILE = CONFIG_DIR + '/config.yml'
PROJ_DIR = path.join(path.dirname(path.realpath(__file__)))
TEMPLATE_EXT = 'yml'
PROJ_DIR = Path(__file__).resolve().parent.parent
PROJ_TEMPLATE_DIR = '{}/templates'.format(PROJ_DIR)
API_KEY_HEADER = 'GROCY-API-KEY'
DEFAULT_CFG = {
@ -98,7 +100,8 @@ class Configuration(object):
try:
TEMPLATE_LOADER = Environment(loader=FileSystemLoader([self.USER_TEMPLATE_DIR, self.PROJ_TEMPLATE_DIR]),
trim_blocks=True, lstrip_blocks=True)
return TEMPLATE_LOADER.get_template(name)
return TEMPLATE_LOADER.get_template('{}.{}'.format(name, self.TEMPLATE_EXT))
except Exception as e:
raise e

View File

@ -1,4 +1,5 @@
from grocy.request import Request
import re
from grocy.conf import Configuration
import logging
@ -6,6 +7,11 @@ import logging
class Entity(object):
RESOURCE_URL_TEMPLATE = '{api}/objects/{entity}/{objectId}'
COLLECTION_URL_TEMPLATE = '{api}/objects/{entity}'
SCHEMA_URL_TEMPLATE = '{api}/openapi/specification'
SCHEMA_MODEL_MAP = {
'products': 'Product',
'stock': 'StockEntry'
}
def __init__(self, name, **props):
self.conf = Configuration()
@ -27,9 +33,28 @@ class Entity(object):
logger.error(e)
raise e
def find(self, query):
logger = logging.getLogger('entity.find')
found_entities = []
try:
entities = self.get()
for entity in entities:
for prop, value in query.items():
regex = re.compile(r'{}'.format(value))
if regex.search(entity[prop]):
found_entities.append(entity)
if len(found_entities) == 0:
return None
return found_entities
except Exception as e:
logger.error(e)
raise e
def create(self, entity):
logger = logging.getLogger('entity.add')
url = self.RESOURCE_URL_TEMPLATE
url = self.COLLECTION_URL_TEMPLATE.format(api=self.conf.api, entity=self.name)
request = Request('post', url, entity)
try:
@ -37,3 +62,29 @@ class Entity(object):
except Exception as e:
logger.error(e)
raise e
def update(self, entity, id=None):
if id is None:
raise Exception('id property is required to update entity')
logger = logging.getLogger('entity.update')
url = self.RESOURCE_URL_TEMPLATE.format(api=self.conf.api, entity=self.name, objectId=id)
request = Request('put', url, resource=entity)
try:
return request.send()
except Exception as e:
logger.error(e)
raise e
@property
def schema(self):
logger = logging.getLogger('entity.schema')
try:
url = self.SCHEMA_URL_TEMPLATE.format(api=self.conf.api)
request = Request('get', url)
response = request.send()
schema_name = self.SCHEMA_MODEL_MAP[self.name]
return response['components']['schemas'][schema_name]
except Exception as e:
logger.error(e)
raise e

View File

@ -43,7 +43,7 @@ class Stock(object):
product_ids = [entry['product_id'] for entry in get_current_stock]
table_entries = []
try:
for index in range(len(product_ids)):
for index in range(0, len(product_ids)):
product_id = product_ids[index]
path = Stock.GET_PRODUCT_BY_ID.format(product_id)
product = self.rest_service.get(path)

View File

@ -1,13 +1,16 @@
from grocy.conf import Configuration
import json
from requests import request
import requests
import logging
class Request(object):
def __init__(self, method, url):
def __init__(self, method, url, resource=None):
self.conf = Configuration()
self.conf.load()
self.url = url
self.resource = resource
self.method = method
self.headers = {
'Content-Type': 'application/json',
@ -18,8 +21,14 @@ class Request(object):
def send(self):
logger = logging.getLogger('request.send')
r = request(method=self.method, url=self.url, headers=self.headers)
if self.resource:
r = request(method=self.method, url=self.url, headers=self.headers, json=self.resource)
else:
r = request(method=self.method, url=self.url, headers=self.headers)
if r.raise_for_status():
logger.error(r.raise_for_status())
raise r.raise_for_status()
return r.json()
if r.status_code != 204:
return r.json()

View File

@ -23,6 +23,40 @@ class Table(object):
loaded_template = self.conf.templates('single_product')
return loaded_template.render(self.entry)
@property
def products(self):
if not self.entries:
raise Exception('Missing entries')
product_groups_map = {product_group['id']: product_group['name']
for product_group in self.entries['product_groups']}
location_map = {location['id']: location['name']
for location in self.entries['locations']}
quantity_unit_map = {quantity_unit['id']: quantity_unit['name']
for quantity_unit in self.entries['quantity_units']}
table_entries = []
try:
for entry in self.entries['products']:
print('{}'.format(entry))
table_entry = []
product_group_name = '' if entry['product_group_id'] == '' else product_groups_map[entry['product_group_id']]
location_name = location_map[entry['location_id']]
quantity_unit_purchase_name = quantity_unit_map[entry['qu_id_purchase']]
quantity_unit_stock_name = quantity_unit_map[entry['qu_id_stock']]
table_entry.append(entry['id'])
table_entry.append(location_name)
table_entry.append(quantity_unit_purchase_name)
table_entry.append(quantity_unit_stock_name)
table_entry.append(product_group_name)
table_entry.append(entry['mini_stock_amount'])
table_entry.append(entry['qu_factor_purchase_to_stock'])
table_entries.append(table_entry)
table_headers = ['ID', 'Name', 'Location', 'Min Stock Amount',
'QU Purchase', 'QU Stock', 'QU Factor', 'Product Group']
return tabulate(table_entries, headers=table_headers)
except Exception as e:
raise e
@property
def stock(self):
logger = logging.getLogger('table.stock')

31
grocy/util.py Normal file
View File

@ -0,0 +1,31 @@
import yaml
import logging
def _yaml_constructor(loader, node):
return node.value
class Util(object):
def __init__(self, cfg):
self.cfg = cfg
yaml.SafeLoader.add_constructor("tag:yaml.org,2002:python/unicode", _yaml_constructor)
def load_yaml(data):
generator = yaml.safe_load_all(data)
data_list = list(generator)
return data_list
def verify_integrity(new_data, schema):
logger = logging.getLogger('util.verify_integrity')
try:
# Verify that updated fields exist
schema_keys = schema['properties'].keys()
for prop in new_data.keys():
if prop not in schema_keys:
raise Exception('{} is not a valid field'.format(prop))
except Exception as e:
logger.error(e)
raise e

View File

@ -0,0 +1,21 @@
{% for product in products %}
name: {{ product.name }}
description: |
{{ product.description }}
location_id: {{ product.location_id }}
qu_id_purchase: {{ product.qu_id_purchase }}
qu_id_stock: {{ product.qu_id_stock }}
qu_factor_purchase_to_stock: {{ product.qu_factor_purchase_to_stock }}
barcode: {{ product.barcode | default("") }}
min_stock_amount: {{ product.min_stock_amount | default("0") }}
default_best_before_days: {{ product.default_best_before_days | default("0") }}
product_group_id: {{ product.product_group_id }}
default_best_before_days_after_open: {{ product.default_best_before_days_after_open | default("0") }}
allow_partial_units_in_stock: {{ product.allow_partial_units_in_stock | default("0") }}
enable_tare_weight_handling: {{ product.enable_tare_weight_handling | default("0") }}
tare_weight: {{ product.tare_weight | default("0.0") }}
not_check_stock_fulfillment_for_recipes: {{ product.not_check_stock_fulfillment_for_recipes | default("0") }}
{% if loop.nextitem %}
---
{%endif%}
{%endfor %}