feat: Added recipe list command
This commit is contained in:
parent
a1c593b118
commit
0a1ecddf3d
@ -76,10 +76,6 @@ class RestService(object):
|
|||||||
|
|
||||||
r = requests.post(url, json=json.dumps(json_payload), headers=self.headers)
|
r = requests.post(url, json=json.dumps(json_payload), headers=self.headers)
|
||||||
|
|
||||||
#if r.raise_for_status():
|
|
||||||
# logger.error(r.raise_for_status())
|
|
||||||
# raise r.raise_for_status()
|
|
||||||
|
|
||||||
if self.json:
|
if self.json:
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
@ -89,4 +85,4 @@ class RestService(object):
|
|||||||
self.headers[type] = value
|
self.headers[type] = value
|
||||||
|
|
||||||
def addToken(self, value):
|
def addToken(self, value):
|
||||||
self.headers[RestService.API_KEY_HEADER] = value;
|
self.headers[RestService.API_KEY_HEADER] = value
|
||||||
|
185
grocy/cli.py
185
grocy/cli.py
@ -1,20 +1,16 @@
|
|||||||
import click
|
import click
|
||||||
from markdown import markdown
|
from markdown import markdown
|
||||||
from dataclasses import asdict, replace
|
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
from uuid import uuid4
|
from grocy.conf import Configuration
|
||||||
from grocy import RestService
|
from grocy.recipe import Recipe
|
||||||
from grocy.models import (Stock,
|
from grocy.table import Table
|
||||||
Battery, Shopping, Recipe)
|
from grocy.entity import Entity
|
||||||
from pkg_resources import iter_entry_points
|
from grocy.stock import Stock
|
||||||
import yaml
|
import yaml
|
||||||
from sys import exit
|
from sys import exit
|
||||||
from shutil import copy
|
from os import path
|
||||||
from os import path, chmod, makedirs
|
|
||||||
from taskw import TaskWarriorShellout
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
APP_NAME = 'grocy-cli'
|
APP_NAME = 'grocy-cli'
|
||||||
SAMPLE_CONFIG_FILE = 'sample.config.yml'
|
SAMPLE_CONFIG_FILE = 'sample.config.yml'
|
||||||
@ -22,103 +18,136 @@ TMP_DIR = '/tmp/grocy'
|
|||||||
CONFIG_FILE = 'config.yml'
|
CONFIG_FILE = 'config.yml'
|
||||||
CONFIG_DIR = click.get_app_dir(APP_NAME)
|
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)
|
||||||
|
|
||||||
|
|
||||||
def __validate_token(cfg):
|
def __validate_token(cfg):
|
||||||
# Validate token
|
# Validate token
|
||||||
if hasattr(cfg, 'token') or cfg['token'] is None:
|
if hasattr(cfg, 'token') or cfg['token'] is None:
|
||||||
click.echo('No token was found. Please add your token to the config file', err=True)
|
click.echo('No token was found. Please add your token to the config file', err=True)
|
||||||
logger.error('No token was found. Please add your token')
|
#logger.error('No token was found. Please add your token')
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
def __create_config_file():
|
|
||||||
user_cfg_file = path.join(CONFIG_DIR, CONFIG_FILE)
|
|
||||||
sample_cfg_file = path.join(PROJ_DIR, '..', SAMPLE_CONFIG_FILE)
|
|
||||||
if not path.exists(CONFIG_DIR):
|
|
||||||
click.echo('Config {} director does not exist, create...'.format(CONFIG_DIR))
|
|
||||||
makedirs(CONFIG_DIR)
|
|
||||||
|
|
||||||
copy(sample_cfg_file, user_cfg_file)
|
|
||||||
click.echo('Copying sample config to {}'.format(sample_cfg_file, user_cfg_file))
|
|
||||||
chmod(user_cfg_file, 0o664)
|
|
||||||
return user_cfg_file
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
@click.pass_context
|
def main():
|
||||||
def main(ctx):
|
cfg = Configuration()
|
||||||
cfg = get_config_file()
|
if not cfg.exists:
|
||||||
# Get logger
|
no_config_msg = 'A config file was not found. A sample configuration file'
|
||||||
if 'logger' in cfg:
|
no_config_msg += ' will be created under {}. Is that Ok?'.format(cfg.CONFIG_DIR)
|
||||||
log_cfg = cfg['logger']
|
|
||||||
else:
|
|
||||||
log_cfg = path.join(click.get_app_dir(APP_NAME), 'grocy.log')
|
|
||||||
log_level = 'DEBUG' if 'level' not in log_cfg else log_cfg['level']
|
|
||||||
log_filename = 'log' if 'file_location' not in log_cfg else log_cfg['file_location']
|
|
||||||
logging.basicConfig(level=log_level, filename=log_filename)
|
|
||||||
cfg['logger'] = logger
|
|
||||||
|
|
||||||
__validate_token(cfg)
|
|
||||||
ctx.ensure_object(dict)
|
|
||||||
ctx.obj['cfg'] = cfg
|
|
||||||
|
|
||||||
|
|
||||||
def get_config_file():
|
|
||||||
no_config_msg = 'A config file was not found'
|
|
||||||
no_config_msg+= ' and will be created under {}. Is that Ok?'
|
|
||||||
no_config_msg = no_config_msg.format(click.get_app_dir(APP_NAME))
|
|
||||||
cfg_file = path.join(click.get_app_dir(APP_NAME), 'config.yml')
|
|
||||||
if not path.exists(cfg_file):
|
|
||||||
create_config_app_dir = click.confirm(no_config_msg)
|
create_config_app_dir = click.confirm(no_config_msg)
|
||||||
|
user_cfg_options = {}
|
||||||
if create_config_app_dir:
|
if create_config_app_dir:
|
||||||
cfg_file = __create_config_file()
|
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)
|
exit(0)
|
||||||
fd = open(cfg_file)
|
else:
|
||||||
parse_cfg_file = yaml.safe_load(fd)
|
cfg.load()
|
||||||
|
|
||||||
return parse_cfg_file
|
logging.basicConfig(level=cfg.logger_level, filename=cfg.logger_file_location)
|
||||||
|
|
||||||
|
|
||||||
@main.command()
|
@main.command()
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def stock(ctx):
|
def stock(ctx):
|
||||||
cfg = ctx.obj['cfg']
|
logger = logging.getLogger('cli.stock')
|
||||||
|
if ctx.invoked_subcommand is None:
|
||||||
|
try:
|
||||||
|
entity = Stock()
|
||||||
|
stocks = entity.get()
|
||||||
|
table = Table(stocks=stocks)
|
||||||
|
click.echo(table.stock)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
raise e
|
||||||
|
|
||||||
stock = Stock(**cfg)
|
|
||||||
stock_entries = stock.get_entries()
|
|
||||||
click.echo(stock_entries)
|
|
||||||
|
|
||||||
@main.command()
|
@main.command()
|
||||||
@click.pass_context
|
def shopping():
|
||||||
def shopping(ctx):
|
logger = logging.getLogger('cli.shopping')
|
||||||
cfg = ctx.obj['cfg']
|
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(ctx):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ingredient.command('add')
|
||||||
|
@click.option('-r', '--recipe_id')
|
||||||
|
def add(ctx, recipe_id):
|
||||||
|
logger = logging.getLogger('cli.ingredient.add')
|
||||||
|
|
||||||
|
try:
|
||||||
|
loaded_template = TEMPLATE_LOADER.get_template('ingredient_add.yml')
|
||||||
|
|
||||||
|
if recipe_id:
|
||||||
|
entity = Entity(name='recipes')
|
||||||
|
recipe = entity.get(id=recipe_id)
|
||||||
|
new_ingredient = click.edit(loaded_template.render(recipe))
|
||||||
|
parsed_new_ingredient = yaml.safe_load(new_ingredient)
|
||||||
|
else:
|
||||||
|
new_ingredient = click.edit(loaded_template.render())
|
||||||
|
parsed_new_ingredient = yaml.safe_load(new_ingredient)
|
||||||
|
if parsed_new_ingredient['recipe_id']:
|
||||||
|
raise Exception('Recipe id is not defined')
|
||||||
|
|
||||||
|
if new_ingredient:
|
||||||
|
entity = Entity(name='ingredients', **parsed_new_ingredient)
|
||||||
|
entity.create()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
|
||||||
shopping = Shopping(**cfg)
|
|
||||||
shopping_list = shopping.get_list()
|
|
||||||
click.echo(shopping_list)
|
|
||||||
|
|
||||||
@main.group()
|
@main.group()
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def recipe(ctx):
|
def recipe(ctx):
|
||||||
if ctx.invoked_subcommand is None:
|
pass
|
||||||
cfg = ctx.obj['cfg']
|
|
||||||
|
|
||||||
receipe = Recipe(id=None, **cfg)
|
|
||||||
recipes = receipe.get_list()
|
@recipe.command('ls')
|
||||||
click.echo(recipes)
|
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')
|
@recipe.command('edit')
|
||||||
@click.argument('recipe_id')
|
@click.argument('recipe_id')
|
||||||
@click.pass_context
|
def edit(recipe_id):
|
||||||
def edit(ctx, recipe_id):
|
logger = logging.getLogger('cli.recipe.edit')
|
||||||
cfg = ctx.obj['cfg']
|
|
||||||
logger = cfg['logger']
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if recipe_id:
|
if recipe_id:
|
||||||
recipe = Recipe(id=recipe_id, **cfg)
|
entity = Entity(name='recipes')
|
||||||
recipe.get(include_products=True)
|
recipe = entity.get(id=recipe_id)
|
||||||
loaded_template = TEMPLATE_LOADER.get_template('recipe_edit.yml')
|
loaded_template = TEMPLATE_LOADER.get_template('recipe_edit.yml')
|
||||||
edited_recipe = click.edit(loaded_template.render(recipe.toJSON()))
|
edited_recipe = click.edit(loaded_template.render(recipe))
|
||||||
if edited_recipe is not None:
|
if edited_recipe is not None:
|
||||||
parsed_edited_recipe = yaml.safe_load(edited_recipe)
|
parsed_edited_recipe = yaml.safe_load(edited_recipe)
|
||||||
parsed_edited_recipe['description'] = markdown(parsed_edited_recipe['description'])
|
parsed_edited_recipe['description'] = markdown(parsed_edited_recipe['description'])
|
||||||
@ -127,16 +156,16 @@ def edit(ctx, recipe_id):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
logger.error('Could not edit recipe {}'.format(recipe_id))
|
logger.error('Could not edit recipe {}'.format(recipe_id))
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
@recipe.command('create')
|
@recipe.command('create')
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def create(ctx):
|
def create(ctx):
|
||||||
cfg = ctx.obj['cfg']
|
logger = logging.getLogger('cli.recipe.create')
|
||||||
logger = cfg['logger']
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
recipe = Recipe(**cfg)
|
recipe = Entity(name='recipes')
|
||||||
loaded_template = TEMPLATE_LOADER.get_template('recipe_add.yml')
|
loaded_template = TEMPLATE_LOADER.get_template('recipe_add.yml')
|
||||||
new_recipe = click.edit(loaded_template.render())
|
new_recipe = click.edit(loaded_template.render())
|
||||||
if new_recipe is not None:
|
if new_recipe is not None:
|
||||||
@ -146,6 +175,8 @@ def create(ctx):
|
|||||||
recipe.create()
|
recipe.create()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
#@main.command()
|
#@main.command()
|
||||||
#@click.pass_context
|
#@click.pass_context
|
||||||
|
85
grocy/conf.py
Normal file
85
grocy/conf.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
from os import path, chmod, makedirs
|
||||||
|
from yaml import safe_load, dump
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
|
||||||
|
class Configuration(object):
|
||||||
|
# TODO: Figure out how to handle windows config
|
||||||
|
CONFIG_DIR = path.expanduser('~/.config/grocy')
|
||||||
|
CONFIG_FILE = CONFIG_DIR + '/config.yml'
|
||||||
|
API_KEY_HEADER = 'GROCY-API-KEY'
|
||||||
|
DEFAULT_CFG = {
|
||||||
|
'logger': {
|
||||||
|
'level': 'DEBUG',
|
||||||
|
'file_location': None
|
||||||
|
},
|
||||||
|
'api': 'https://demo-en.grocy.info/api',
|
||||||
|
'token': None,
|
||||||
|
'formats': {
|
||||||
|
'col': 'center',
|
||||||
|
'table': 'simple'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def update(self, props):
|
||||||
|
# Set nested fields (e.g. logger, formats)
|
||||||
|
if props['logger']:
|
||||||
|
for prop in props['logger']:
|
||||||
|
prop_attr = 'logger_{prop}'.format(prop=prop)
|
||||||
|
prop_value = props['logger'][prop]
|
||||||
|
setattr(self, prop_attr, prop_value)
|
||||||
|
if props['formats']:
|
||||||
|
for prop in props['formats']:
|
||||||
|
prop_attr = '{prop}_format'.format(prop=prop)
|
||||||
|
prop_value = props['formats'][prop]
|
||||||
|
setattr(self, prop_attr, prop_value)
|
||||||
|
|
||||||
|
for prop, value in props.items():
|
||||||
|
if not prop == 'logger' and prop in props:
|
||||||
|
setattr(self, prop, value)
|
||||||
|
|
||||||
|
def create(self, user_cfg_options={}):
|
||||||
|
cfg_json = deepcopy(self.DEFAULT_CFG)
|
||||||
|
|
||||||
|
if user_cfg_options['logger_level']:
|
||||||
|
self.logger_level = user_cfg_options['logger_level']
|
||||||
|
cfg_json['logger']['level'] = self.logger_level
|
||||||
|
|
||||||
|
if user_cfg_options['logger_file_location']:
|
||||||
|
self.logger_file_location = user_cfg_options['logger_file_location']
|
||||||
|
cfg_json['logger']['file_location'] = self.logger_file_location
|
||||||
|
|
||||||
|
if user_cfg_options['api']:
|
||||||
|
self.api = user_cfg_options['api']
|
||||||
|
cfg_json['api'] = self.api
|
||||||
|
|
||||||
|
if user_cfg_options['token']:
|
||||||
|
self.token = user_cfg_options['token']
|
||||||
|
cfg_json['token'] = self.token
|
||||||
|
|
||||||
|
if user_cfg_options['col_format']:
|
||||||
|
self.col_format = user_cfg_options['col_format']
|
||||||
|
cfg_json['formats']['col'] = self.col_format
|
||||||
|
|
||||||
|
if user_cfg_options['table_format']:
|
||||||
|
self.table_format = user_cfg_options['table_format']
|
||||||
|
cfg_json['formats']['table'] = self.table_format
|
||||||
|
|
||||||
|
# Create new configuration
|
||||||
|
makedirs(self.CONFIG_DIR)
|
||||||
|
with open(self.CONFIG_FILE, 'x') as fd:
|
||||||
|
dump_cfg = dump(cfg_json, default_flow_style=False, allow_unicode=True, encoding=None)
|
||||||
|
fd.write(dump_cfg)
|
||||||
|
chmod(self.CONFIG_DIR, 0o755)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exists(self):
|
||||||
|
return path.exists(self.CONFIG_FILE)
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
if self.exists:
|
||||||
|
with open(self.CONFIG_FILE) as fd:
|
||||||
|
data = safe_load(fd)
|
||||||
|
self.update(data)
|
||||||
|
else:
|
||||||
|
self.create()
|
39
grocy/entity.py
Normal file
39
grocy/entity.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
from grocy.request import Request
|
||||||
|
from grocy.conf import Configuration
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class Entity(object):
|
||||||
|
RESOURCE_URL_TEMPLATE = '{api}/objects/{entity}/{objectId}'
|
||||||
|
COLLECTION_URL_TEMPLATE = '{api}/objects/{entity}'
|
||||||
|
|
||||||
|
def __init__(self, name, **props):
|
||||||
|
self.conf = Configuration()
|
||||||
|
self.conf.load()
|
||||||
|
self.name = name
|
||||||
|
self.__dict__.update(**props)
|
||||||
|
|
||||||
|
def get(self, id=None):
|
||||||
|
logger = logging.getLogger('entity.get')
|
||||||
|
if id:
|
||||||
|
url = self.RESOURCE_URL_TEMPLATE.format(api=self.conf.api, entity=self.name, objectId=id)
|
||||||
|
else:
|
||||||
|
url = self.COLLECTION_URL_TEMPLATE.format(api=self.conf.api, entity=self.name)
|
||||||
|
|
||||||
|
request = Request('get', url)
|
||||||
|
try:
|
||||||
|
return request.send()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def create(self, entity):
|
||||||
|
logger = logging.getLogger('entity.add')
|
||||||
|
url = self.RESOURCE_URL_TEMPLATE
|
||||||
|
|
||||||
|
request = Request('post', url)
|
||||||
|
try:
|
||||||
|
return request.send()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
raise e
|
23
grocy/models.py
Normal file
23
grocy/models.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
class Product(dict):
|
||||||
|
def __init__(self, **entries):
|
||||||
|
self.fields = [
|
||||||
|
'location_id',
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'qu_id_purchase',
|
||||||
|
'qu_id_stock',
|
||||||
|
'qu_factor_purchase_to_stock',
|
||||||
|
'barcode',
|
||||||
|
'min_stock_amount',
|
||||||
|
'default_best_before_days',
|
||||||
|
'default_best_before_days_after_open',
|
||||||
|
'tare_weight', 'enable_tare_weight_handling', 'picture_file_name','product_group_id',
|
||||||
|
'allow_partial_units_in_stock']
|
||||||
|
self.__dict__.update(entries)
|
||||||
|
def toJSON(self):
|
||||||
|
obj = {}
|
||||||
|
for attr, value in self.__dict__.items():
|
||||||
|
if attr in self.fields and value is not None:
|
||||||
|
obj[attr] = value
|
||||||
|
return obj
|
||||||
|
|
@ -17,14 +17,3 @@ class Product(Schema):
|
|||||||
obj[attr] = value
|
obj[attr] = value
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
#id:str = ''
|
|
||||||
#location_id:str = ''
|
|
||||||
#name:str = ''
|
|
||||||
#description:str = ''
|
|
||||||
#qu_id_purchase:str = ''
|
|
||||||
#qu_id_stock:str = ''
|
|
||||||
#qu_factor_purchase_to_stock:float = 0
|
|
||||||
#barcode:str = ''
|
|
||||||
#min_stock_amount:int = 0
|
|
||||||
#default_best_before_days:int = 0
|
|
||||||
#default_best_before_days_after_open:int = 0
|
|
||||||
|
@ -101,6 +101,17 @@ class Recipe(Schema):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
def addIngredient(self, ingredient):
|
||||||
|
ingredient['recipe_id'] = self.id
|
||||||
|
hasProduct = False
|
||||||
|
if 'product_id' in ingredient and ingredient['product_id'] is not None:
|
||||||
|
hasProduct = Product.exists(ingredient['product_id'])
|
||||||
|
if hasProduct:
|
||||||
|
self.rest_service.post('recipes_pos', ingredient)
|
||||||
|
else
|
||||||
|
raise Exception('No product was given')
|
||||||
|
|
||||||
|
|
||||||
def create(self):
|
def create(self):
|
||||||
created_recipe = {
|
created_recipe = {
|
||||||
'description': self.description,
|
'description': self.description,
|
||||||
@ -110,6 +121,7 @@ class Recipe(Schema):
|
|||||||
'not_check_shoppinglist': self.not_check_shoppinglist
|
'not_check_shoppinglist': self.not_check_shoppinglist
|
||||||
}
|
}
|
||||||
self.rest_service.post('recipes', created_recipe)
|
self.rest_service.post('recipes', created_recipe)
|
||||||
|
|
||||||
def get(self, include_products=False):
|
def get(self, include_products=False):
|
||||||
try:
|
try:
|
||||||
recipe = self.rest_service.get('recipes', id=self.id)
|
recipe = self.rest_service.get('recipes', id=self.id)
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
from grocy import RestService
|
from grocy import RestService
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
class Schema(object):
|
|
||||||
|
|
||||||
|
class Schema(object):
|
||||||
def _init_rest_service(self):
|
def _init_rest_service(self):
|
||||||
if hasattr(self, 'api'):
|
if hasattr(self, 'api'):
|
||||||
if self.api.startswith == '/':
|
if self.api.startswith == '/':
|
||||||
|
22
grocy/recipe.py
Normal file
22
grocy/recipe.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
from grocy.request import Request
|
||||||
|
from grocy.conf import Configuration
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class Recipe(object):
|
||||||
|
GET_RECIPE_REQUIRMENTS_URL_TEMPLATE = '{api}/recipes/requirements'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.conf = Configuration()
|
||||||
|
self.conf.load()
|
||||||
|
|
||||||
|
def get_requirements(self):
|
||||||
|
logger = logging.getLogger('recipe.get_requirements')
|
||||||
|
url = self.GET_RECIPE_REQUIRMENTS_URL_TEMPLATE.format(api=self.conf.api)
|
||||||
|
request = Request('get', url)
|
||||||
|
try:
|
||||||
|
return request.send()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
raise e
|
||||||
|
|
25
grocy/request.py
Normal file
25
grocy/request.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
from grocy.conf import Configuration
|
||||||
|
from requests import request
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class Request(object):
|
||||||
|
def __init__(self, method, url):
|
||||||
|
self.conf = Configuration()
|
||||||
|
self.conf.load()
|
||||||
|
self.url = url
|
||||||
|
self.method = method
|
||||||
|
self.headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
if self.conf.token:
|
||||||
|
self.headers[self.conf.API_KEY_HEADER] = self.conf.token
|
||||||
|
|
||||||
|
def send(self):
|
||||||
|
logger = logging.getLogger('request.send')
|
||||||
|
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()
|
22
grocy/stock.py
Normal file
22
grocy/stock.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
from grocy.request import Request
|
||||||
|
from grocy.conf import Configuration
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class Stock(object):
|
||||||
|
GET_STOCK_URL_TEMPLATE = '{api}/stock'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.conf = Configuration()
|
||||||
|
self.conf.load()
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
logger = logging.getLogger('stock.get')
|
||||||
|
url = self.GET_STOCK_URL_TEMPLATE.format(api=self.conf.api)
|
||||||
|
request = Request('get', url)
|
||||||
|
try:
|
||||||
|
return request.send()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
raise e
|
||||||
|
|
120
grocy/table.py
Normal file
120
grocy/table.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
from grocy.entity import Entity
|
||||||
|
import re
|
||||||
|
from grocy.conf import Configuration
|
||||||
|
import logging
|
||||||
|
from tabulate import tabulate
|
||||||
|
|
||||||
|
|
||||||
|
class Table(object):
|
||||||
|
NOT_ENOUGH_IN_STOCK_MSG = '{glyph} Not enough in stock, {missing_amount} ingredient{s} missing'
|
||||||
|
ENOUGH_IN_STOCK_MSG = '{glyph} Enough in stock'
|
||||||
|
NOT_ENOUGH_BUT_IN_SHOPPING_LIST_MSG = '{not_enough}, but already on list'.format(not_enough=NOT_ENOUGH_IN_STOCK_MSG)
|
||||||
|
|
||||||
|
def __init__(self, **entries):
|
||||||
|
self.__dict__.update(entries)
|
||||||
|
self.conf = Configuration()
|
||||||
|
self.conf.load()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def stock(self):
|
||||||
|
logger = logging.getLogger('table.stock')
|
||||||
|
entity = Entity(name='products')
|
||||||
|
products = entity.get()
|
||||||
|
products_map = {product['id']: product for product in products}
|
||||||
|
try:
|
||||||
|
# Get product names from ids and replace
|
||||||
|
table_entries = []
|
||||||
|
try:
|
||||||
|
for item in self.stocks:
|
||||||
|
product = products_map[item['product_id']]
|
||||||
|
item['product_id'] = product['name']
|
||||||
|
table_entry = list(dict.values(item))
|
||||||
|
table_entries.append(table_entry)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
# Generate stock overview table
|
||||||
|
table_headers = ['Product', 'Amount', 'Best Before Date', 'Amount Opened']
|
||||||
|
return tabulate(table_entries, headers=table_headers)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
@property
|
||||||
|
def recipe(self):
|
||||||
|
checkmark_glyph = ''
|
||||||
|
times_glyph = ''
|
||||||
|
exclamation_glyph = ''
|
||||||
|
logger = logging.getLogger('table.recipes')
|
||||||
|
normal_recipes = [recipe for recipe in self.recipes if re.search(r'^normal', recipe['type'])]
|
||||||
|
recipes_req_map = {recipe_reqs['recipe_id']: recipe_reqs for recipe_reqs in self.recipes_reqs}
|
||||||
|
|
||||||
|
try:
|
||||||
|
table_entries = []
|
||||||
|
try:
|
||||||
|
for item in normal_recipes:
|
||||||
|
table_entry = []
|
||||||
|
table_entry.append(item['id'])
|
||||||
|
table_entry.append(item['name'])
|
||||||
|
table_entry.append(item['base_servings'])
|
||||||
|
|
||||||
|
recipe_reqs = recipes_req_map[item['id']]
|
||||||
|
missing_amount = int(recipe_reqs['missing_products_count'])
|
||||||
|
number_of_ingredients = 's' if missing_amount > 1 else ''
|
||||||
|
if recipe_reqs['need_fulfilled'] == '1':
|
||||||
|
table_entry.append(self.ENOUGH_IN_STOCK_MSG.format(glyph=checkmark_glyph))
|
||||||
|
elif recipe_reqs['need_fulfilled_with_shopping_list'] == '1':
|
||||||
|
missing_amount = recipe_reqs['missing_products_count']
|
||||||
|
table_entry.append(self.NOT_ENOUGH_BUT_IN_SHOPPING_LIST_MSG.format(glyph=exclamation_glyph,
|
||||||
|
missing_amount=missing_amount, s=number_of_ingredients))
|
||||||
|
else:
|
||||||
|
table_entry.append(self.NOT_ENOUGH_IN_STOCK_MSG.format(glyph=times_glyph,
|
||||||
|
missing_amount=missing_amount, s=number_of_ingredients))
|
||||||
|
table_entries.append(table_entry)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
# Generate recipes overview table
|
||||||
|
table_headers = ['Id', 'Name', 'Servings', 'Requirements Fulfilled']
|
||||||
|
return tabulate(table_entries, headers=table_headers)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shopping_list(self):
|
||||||
|
logger = logging.getLogger('table.shopping_list')
|
||||||
|
try:
|
||||||
|
table_headers = ['Group', 'Product', 'Amount']
|
||||||
|
# Get product names and location from ids and replace
|
||||||
|
product_ids = [entry['product_id'] for entry in self.shopping_list]
|
||||||
|
products = []
|
||||||
|
location_ids = []
|
||||||
|
table_entries = []
|
||||||
|
for index in range(len(product_ids)):
|
||||||
|
product_id = product_ids[index]
|
||||||
|
entity = Entity(name='products')
|
||||||
|
product = entity.get(id=product_id)
|
||||||
|
|
||||||
|
entity = Entity(name='quantity_units')
|
||||||
|
quantity_unit = entity.get(product['qu_id_purchase'])
|
||||||
|
|
||||||
|
min_amount = '{} {}'.format(product['min_stock_amount'], quantity_unit['name'])
|
||||||
|
if product['product_group_id'] == '':
|
||||||
|
product_group_name = 'Uncategorized'
|
||||||
|
else:
|
||||||
|
entity = Entity(name='product_groups')
|
||||||
|
product_group = product.get(id=product['product_group_id'])
|
||||||
|
product_group_name = product_group['name']
|
||||||
|
shopping_item = [product_group_name, product['name'], min_amount]
|
||||||
|
table_entries.append(shopping_item)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
# Generate stock overview table
|
||||||
|
return tabulate(table_entries, headers=table_headers)
|
4
setup.cfg
Normal file
4
setup.cfg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[pycodestyle]
|
||||||
|
exclude = .git,*.egg-info
|
||||||
|
ignore = E241,E128,E226,E722,W504
|
||||||
|
max-line-length = 120
|
8
templates/ingredient_add.yml
Normal file
8
templates/ingredient_add.yml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
product_id:
|
||||||
|
amount:
|
||||||
|
note:
|
||||||
|
qu_id:
|
||||||
|
only_check_single_unit_in_stock:
|
||||||
|
ingredient_group:
|
||||||
|
not_check_stock_fulfillment:
|
||||||
|
variable_amount:
|
Loading…
Reference in New Issue
Block a user