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 markdown import markdown
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from grocy.conf import Configuration from grocy.conf import Configuration
from grocy.util import Util
from grocy.recipe import Recipe from grocy.recipe import Recipe
from grocy.table import Table from grocy.table import Table
from grocy.entity import Entity 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__))) PROJ_DIR = path.join(path.dirname(path.realpath(__file__)))
TEMPLATE_LOADER = Environment(loader=FileSystemLoader('templates'), trim_blocks=True, lstrip_blocks=True) 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() @click.group()
def main(): def main():
@ -65,16 +73,20 @@ def stock(ctx):
raise e raise e
@main.group(invoke_without_command=True) @main.group()
def product():
pass
@product.command()
@click.pass_context @click.pass_context
@click.argument('product_id', required=False) @click.argument('product_id', required=False)
def product(ctx, product_id): def view(ctx, product_id):
logger = logging.getLogger('cli.product') logger = logging.getLogger('cli.product')
try: try:
if product_id: if product_id:
stock = Stock() stock = Stock()
product = stock.get_product(product_id) product = stock.get_product(product_id)
# Need to get quantity_unit
table = Table(entry=product) table = Table(entry=product)
click.echo(table.product) click.echo(table.product)
else: else:
@ -83,148 +95,203 @@ def product(ctx, product_id):
logger.error(e) logger.error(e)
raise e raise e
#@product.command('list') @product.command()
#@click.argument('query', required=False) @click.option('--name', '-n', 'name')
#def view(query, product_id): @click.argument('product_id', required=False)
# logger = logging.getLogger('cli.product.view') 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: # try:
# if product_id: # entity = Entity(name='shopping_list')
# entity = Entity(name='product') # shopping_list = entity.get()
# product = entity.get(id=product_id) # table = Table(shopping_list=shopping_list)
# table = Table(product=product) # click.echo(table.shopping_list)
# click.echo(table.product)
# else:
# except Exception as e: # except Exception as e:
# logger.error(e) # logger.error(e)
# raise e # raise e
#
#
@main.command() #@main.command()
def shopping(): #@main.group()
logger = logging.getLogger('cli.shopping') #def ingredient():
try: # pass
entity = Entity(name='shopping_list') #
shopping_list = entity.get() #
table = Table(shopping_list=shopping_list) #@ingredient.command('add')
click.echo(table.shopping_list) #@click.argument('query')
except Exception as e: #@click.argument('recipe_id')
logger.error(e) #@click.option('--amount', '-a', 'amount', default=1, type=int)
raise e #@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)
@main.group() #@click.option('--disable-fulfillment', '--df', 'disable_fulfillment', default=False)
def ingredient(): #@click.option('--note', '-n', 'note', multiple=True, default='', type=str)
pass #@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')
@ingredient.command('add') #
@click.argument('query') # try:
@click.argument('recipe_id') # loaded_template = TEMPLATE_LOADER.get_template('ingredient_add.yml')
@click.option('--amount', '-a', 'amount', default=1, type=int) # new_ingredient = {}
@click.option('--group', '-g', type=str, default='') #
@click.option('--variable-amount', '--va', 'variable_amount', default=None, type=float) # entity = Entity(name='recipes')
@click.option('--in-stock', '--is', 'in_stock', default=False) # recipe = entity.get(id=recipe_id)
@click.option('--disable-fulfillment', '--df', 'disable_fulfillment', default=False) #
@click.option('--note', '-n', 'note', multiple=True, default='', type=str) # if not recipe:
@click.option('--no-edit', '--ne', 'no_edit', default=False) # raise click.BadParameter(message='recipe {id} does not exist', param='recipe_id',
def add(query, recipe_id, amount, group, variable_amount, in_stock, disable_fulfillment, note, no_edit): # param_hint='Use `grocy recipes ls` to get a list of recipes')
logger = logging.getLogger('cli.ingredient.add') #
# entity = Entity(name='products')
try: # product = entity.findOne(query)
loaded_template = TEMPLATE_LOADER.get_template('ingredient_add.yml') # new_ingredient['product_id'] = product['id']
new_ingredient = {} #
# new_ingredient['amount'] = amount
entity = Entity(name='recipes') # new_ingredient['group'] = group
recipe = entity.get(id=recipe_id) # new_ingredient['variable_amount'] = variable_amount
# new_ingredient['only_check_single_unit_in_stock'] = "1" if in_stock else "0"
if not recipe: # new_ingredient['not_check_stock_fulfillment'] = "1" if disable_fulfillment else "0"
raise click.BadParameter(message='recipe {id} does not exist', param='recipe_id', # new_ingredient['note'] = note
param_hint='Use `grocy recipes ls` to get a list of recipes') #
# if not no_edit:
entity = Entity(name='products') # new_ingredient = click.edit(loaded_template.render(new_ingredient))
product = entity.findOne(query) #
new_ingredient['product_id'] = product['id'] # parsed_new_ingredient = yaml.safe_load(new_ingredient)
# entity = Entity(name='recipes_pos')
new_ingredient['amount'] = amount # #entity.create(parsed_new_ingredient)
new_ingredient['group'] = group #
new_ingredient['variable_amount'] = variable_amount # except Exception as e:
new_ingredient['only_check_single_unit_in_stock'] = "1" if in_stock else "0" # logger.error(e)
new_ingredient['not_check_stock_fulfillment'] = "1" if disable_fulfillment else "0" # raise e
new_ingredient['note'] = note #
#
if not no_edit: #@main.group()
new_ingredient = click.edit(loaded_template.render(new_ingredient)) #@click.pass_context
#def recipe(ctx):
parsed_new_ingredient = yaml.safe_load(new_ingredient) # pass
entity = Entity(name='recipes_pos') #
#entity.create(parsed_new_ingredient) #
#@recipe.command('ls')
except Exception as e: #def ls():
logger.error(e) # logger = logging.getLogger('cli.recipe')
raise e # try:
# entity = Entity(name='recipes')
# recipes = entity.get()
@main.group() # recipe = Recipe()
@click.pass_context # recipes_reqs = recipe.get_requirements()
def recipe(ctx): # table = Table(recipes=recipes, recipes_reqs=recipes_reqs)
pass # click.echo(table.recipe)
# except Exception as e:
# logger.error(e)
@recipe.command('ls') # raise e
def ls(): #
logger = logging.getLogger('cli.recipe') #
try: #@recipe.command('edit')
entity = Entity(name='recipes') #@click.argument('recipe_id')
recipes = entity.get() #def edit(recipe_id):
recipe = Recipe() # logger = logging.getLogger('cli.recipe.edit')
recipes_reqs = recipe.get_requirements() # try:
table = Table(recipes=recipes, recipes_reqs=recipes_reqs) # if recipe_id:
click.echo(table.recipe) # entity = Entity(name='recipes')
except Exception as e: # recipe = entity.get(id=recipe_id)
logger.error(e) # loaded_template = TEMPLATE_LOADER.get_template('recipe_edit.yml')
raise e # edited_recipe = click.edit(loaded_template.render(recipe))
# if edited_recipe is not None:
# parsed_edited_recipe = yaml.safe_load(edited_recipe)
@recipe.command('edit') # parsed_edited_recipe['description'] = markdown(parsed_edited_recipe['description'])
@click.argument('recipe_id') # recipe.__dict__.update(parsed_edited_recipe)
def edit(recipe_id): # recipe.update()
logger = logging.getLogger('cli.recipe.edit') # except Exception as e:
try: # logger.error(e)
if recipe_id: # logger.error('Could not edit recipe {}'.format(recipe_id))
entity = Entity(name='recipes') # raise e
recipe = entity.get(id=recipe_id) #
loaded_template = TEMPLATE_LOADER.get_template('recipe_edit.yml') #
edited_recipe = click.edit(loaded_template.render(recipe)) #@recipe.command('create')
if edited_recipe is not None: #@click.pass_context
parsed_edited_recipe = yaml.safe_load(edited_recipe) #def create(ctx):
parsed_edited_recipe['description'] = markdown(parsed_edited_recipe['description']) # logger = logging.getLogger('cli.recipe.create')
recipe.__dict__.update(parsed_edited_recipe) #
recipe.update() # try:
except Exception as e: # recipe = Entity(name='recipes')
logger.error(e) # loaded_template = TEMPLATE_LOADER.get_template('recipe_add.yml')
logger.error('Could not edit recipe {}'.format(recipe_id)) # new_recipe = click.edit(loaded_template.render())
raise e # if new_recipe is not None:
# parsed_new_recipe = yaml.safe_load(new_recipe)
# parsed_new_recipe['description'] = markdown(parsed_new_recipe['description'])
@recipe.command('create') # recipe.__dict__.update(parsed_new_recipe)
@click.pass_context # recipe.create()
def create(ctx): # except Exception as e:
logger = logging.getLogger('cli.recipe.create') # logger.error(e)
# raise e
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.command()
#@click.pass_context #@click.pass_context
@ -243,3 +310,4 @@ def create(ctx):
# task = Task(**cfg) # task = Task(**cfg)
# tasks = task.get_list() # tasks = task.get_list()
# click.echo(tasks) # 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 shutil import copy
from yaml import safe_load, dump from yaml import safe_load, dump
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
@ -10,7 +11,8 @@ class Configuration(object):
CONFIG_DIR = path.expanduser('~/.config/grocy') CONFIG_DIR = path.expanduser('~/.config/grocy')
USER_TEMPLATE_DIR = path.expanduser('~/.config/grocy/templates') USER_TEMPLATE_DIR = path.expanduser('~/.config/grocy/templates')
CONFIG_FILE = CONFIG_DIR + '/config.yml' 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) PROJ_TEMPLATE_DIR = '{}/templates'.format(PROJ_DIR)
API_KEY_HEADER = 'GROCY-API-KEY' API_KEY_HEADER = 'GROCY-API-KEY'
DEFAULT_CFG = { DEFAULT_CFG = {
@ -98,7 +100,8 @@ class Configuration(object):
try: try:
TEMPLATE_LOADER = Environment(loader=FileSystemLoader([self.USER_TEMPLATE_DIR, self.PROJ_TEMPLATE_DIR]), TEMPLATE_LOADER = Environment(loader=FileSystemLoader([self.USER_TEMPLATE_DIR, self.PROJ_TEMPLATE_DIR]),
trim_blocks=True, lstrip_blocks=True) 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: except Exception as e:
raise e raise e

View File

@ -1,4 +1,5 @@
from grocy.request import Request from grocy.request import Request
import re
from grocy.conf import Configuration from grocy.conf import Configuration
import logging import logging
@ -6,6 +7,11 @@ import logging
class Entity(object): class Entity(object):
RESOURCE_URL_TEMPLATE = '{api}/objects/{entity}/{objectId}' RESOURCE_URL_TEMPLATE = '{api}/objects/{entity}/{objectId}'
COLLECTION_URL_TEMPLATE = '{api}/objects/{entity}' COLLECTION_URL_TEMPLATE = '{api}/objects/{entity}'
SCHEMA_URL_TEMPLATE = '{api}/openapi/specification'
SCHEMA_MODEL_MAP = {
'products': 'Product',
'stock': 'StockEntry'
}
def __init__(self, name, **props): def __init__(self, name, **props):
self.conf = Configuration() self.conf = Configuration()
@ -27,9 +33,28 @@ class Entity(object):
logger.error(e) logger.error(e)
raise 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): def create(self, entity):
logger = logging.getLogger('entity.add') 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) request = Request('post', url, entity)
try: try:
@ -37,3 +62,29 @@ class Entity(object):
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
raise 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] product_ids = [entry['product_id'] for entry in get_current_stock]
table_entries = [] table_entries = []
try: try:
for index in range(len(product_ids)): for index in range(0, len(product_ids)):
product_id = product_ids[index] product_id = product_ids[index]
path = Stock.GET_PRODUCT_BY_ID.format(product_id) path = Stock.GET_PRODUCT_BY_ID.format(product_id)
product = self.rest_service.get(path) product = self.rest_service.get(path)

View File

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

View File

@ -23,6 +23,40 @@ class Table(object):
loaded_template = self.conf.templates('single_product') loaded_template = self.conf.templates('single_product')
return loaded_template.render(self.entry) 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 @property
def stock(self): def stock(self):
logger = logging.getLogger('table.stock') 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 %}