feat: added edit for recipe

This commit is contained in:
Aerex 2019-04-28 16:12:05 -05:00
parent 3a85ad936d
commit 48896dda23
18 changed files with 243 additions and 125 deletions

View File

@ -1,30 +1,46 @@
import requests import requests
from dataclasses import asdict
import requests_cache import requests_cache
import json import json
requests_cache.install_cache('grocy', allowable_methods=('GET',), expire_after=180) #requests_cache.install_cache('grocy', allowable_methods=('GET',), expire_after=180)
class RestService(object): class RestService(object):
API_KEY_HEADER = 'GROCY-API-KEY' API_KEY_HEADER = 'GROCY-API-KEY'
RESOURCE_URL_TEMPLATE = '{api_url}/objects/{entity}/{objectId}'
COLLECTION_URL_TEMPLATE = '{api_url}/objects/{entity}'
def __init__(self, api_url, json=False): def __init__(self, api_url, json=False):
self.api_url = api_url self.api_url = api_url
self.headers = {} self.headers = {}
self.json = json self.json = json
def get(self, path, id=None): def put(self, entity_name, entity, entity_id):
if type(entity) is not dict:
# TODO: change this to a single pattern assume a pattern then stick with it json_payload = entity.toJSON()
if self.api_url.endswith('/'):
url = '{0}{1}'.format(self.api_url, path[1:])
else: else:
url = '{0}{1}'.format(self.api_url, path) json_payload = entity
if id: url = RestService.RESOURCE_URL_TEMPLATE.format(api_url=self.api_url, entity=entity_name, objectId=entity_id)
url = '{0}/{1}'.format(url, id) r = requests.put(url, json=json_payload, headers=self.headers)
if r.raise_for_status():
logger.error(r.raise_for_status())
raise r.raise_for_status()
# if self.json:
# return r.json()
#
return r.content
def get(self, entity_name, id=None):
if not id:
url = RestService.COLLECTION_URL_TEMPLATE.format(api_url=self.api_url, entity=entity_name)
else:
url = RestService.RESOURCE_URL_TEMPLATE.format(api_url=self.api_url, entity=entity_name, objectId=id)
r = requests.get(url, headers=self.headers) r = requests.get(url, headers=self.headers)
if r.raise_for_status(): if r.raise_for_status():
logger.error(r.raise_for_status())
raise r.raise_for_status() raise r.raise_for_status()
if self.json: if self.json:
@ -41,7 +57,6 @@ class RestService(object):
url = '{0}/{1}'.format(api_url, path) url = '{0}/{1}'.format(api_url, path)
r = requests.get(url, headers=self.headers) r = requests.get(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()
@ -62,7 +77,6 @@ class RestService(object):
else: else:
url = '{0}/{1}'.format(api_url, path) url = '{0}/{1}'.format(api_url, path)
print('{}'.format(url))
r = requests.post(url, data=json.dumps(payload), headers=self.headers) r = requests.post(url, data=json.dumps(payload), headers=self.headers)
#if r.raise_for_status(): #if r.raise_for_status():

View File

@ -1,8 +1,11 @@
import click import click
from markdown import markdown
from dataclasses import asdict, replace
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from uuid import uuid4 from uuid import uuid4
from grocy import RestService from grocy import RestService
from grocy.commands import * from grocy.models import (Stock,
Battery, Shopping, Recipe)
from pkg_resources import iter_entry_points from pkg_resources import iter_entry_points
import yaml import yaml
from sys import exit from sys import exit
@ -115,10 +118,12 @@ def edit(ctx, recipe_id):
recipe = Recipe(id=recipe_id, **cfg) recipe = Recipe(id=recipe_id, **cfg)
recipe.get(include_products=True) recipe.get(include_products=True)
loaded_template = TEMPLATE_LOADER.get_template('recipe.yml') loaded_template = TEMPLATE_LOADER.get_template('recipe.yml')
edited_recipe = click.edit(loaded_template.render(dict(recipe=recipe))) edited_recipe = click.edit(loaded_template.render(recipe.toJSON()))
if edited_recipe is not None: if edited_recipe is not None:
updated_recipe = Recipe(id=receipe_id, **edited_recipe) parsed_edited_recipe = yaml.safe_load(edited_recipe)
updated_recipe.update() parsed_edited_recipe['description'] = markdown(parsed_edited_recipe['description'])
recipe.__dict__.update(parsed_edited_recipe)
recipe.update()
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))

View File

@ -1,7 +0,0 @@
from grocy.commands.stock import Stock
from grocy.commands.product import Product
from grocy.commands.recipe import Recipe
from grocy.commands.chore import Chore
from grocy.commands.task import Task
from grocy.commands.shopping import Shopping
from grocy.commands.battery import Battery

View File

@ -1,3 +0,0 @@
class Product(object):
def __init__(self, **entries):
self.__dict__.update(entries)

View File

@ -1,78 +0,0 @@
from grocy import RestService
import html2text
from grocy.commands import product
from tabulate import tabulate
from os import path
class Recipe(object):
GET_RECIPES = '/objects/recipes'
GET_RECIPE = '/objects/recipes/{0}'
GET_PRODUCT = '/objects/products/{0}'
GET_RECIPES_POS = '/objects/recipes_pos'
def __init__(self, id, **entries):
self.id = id
self.__dict__.update(entries)
self._init_rest_service()
self.products = []
#self._set_default_table_formats()
#if not hasattr('tablefmt', self):
# self.tablefmt = None
#if not hasattr('colalign', self):
# self.colalign = None
def get_list(self):
try:
recipes = self.rest_service.get(Recipe.GET_RECIPES)
table_headers = ['#', 'Name']
table_entries = []
for recipe in recipes:
table_entry = [recipe.get('id'), recipe.get('name')]
table_entries.append(table_entry)
except Exception as e:
raise e
# Generate stock overview table
return tabulate(table_entries, headers=table_headers)
def _get_products_by_recipe_id(self):
recipe_products = self.rest_service.get(Recipe.GET_RECIPES_POS)
products_for_recipe = []
for recipe_product in recipe_products:
if recipe_product.get('recipe_id') == self.id:
## TODO: need to find a better way to run a batch call to get only products for recipe
product = self.rest_service.get(Recipe.GET_PRODUCT.format(recipe_product.get('product_id')))
## combined dict into single dict
product_recipe_info = {k: v for combined_dict in [product, recipe_product] for k, v in combined_dict.items()}
self.products.append(product_recipe_info)
def _set_default_table_formats(self):
if not hasattr('formats', self):
self.tablefmt = None
self.colalign = None
elif not hasattr('table', self.formats):
self.tableformat = None
elif not hasattr('col', self.formats):
self.colalign = None
def _init_rest_service(self):
if self.api.startswith == '/':
self.api = self.api[1:]
if self.api.endswith == '/':
self.api = self.api[1:-1]
self.rest_service = RestService(self.api, json=True)
self.rest_service.addHeader('Content-Type', 'application/json')
self.rest_service.addToken(self.token)
def get(self, include_products=False):
try:
recipe = self.rest_service.get(Recipe.GET_RECIPE.format(self.id))
if 'description' in recipe:
recipe['description_txt'] = html2text.html2text(recipe['description'].strip())
self.__dict__.update(recipe)
if include_products:
self._get_products_by_recipe_id()
except Exception as e:
raise e

9
grocy/models/__init__.py Normal file
View File

@ -0,0 +1,9 @@
from grocy.models.recipe_pos import RecipePos
from grocy.models.stock import Stock
from grocy.models.product import Product
from grocy.models.recipe import Recipe
from grocy.models.chore import Chore
#
from grocy.models.task import Task
from grocy.models.shopping import Shopping
from grocy.models.battery import Battery

View File

@ -17,7 +17,6 @@ class Chore(object):
# self.colalign = None # self.colalign = None
def _set_default_table_formats(self): def _set_default_table_formats(self):
if not hasattr('formats', self): if not hasattr('formats', self):
self.tablefmt = None self.tablefmt = None

30
grocy/models/product.py Normal file
View File

@ -0,0 +1,30 @@
from grocy.models.schema import Schema
class Product(Schema):
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)
#self._init_rest_service()
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
#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

113
grocy/models/recipe.py Normal file
View File

@ -0,0 +1,113 @@
from grocy import RestService
import html2text
from grocy.models import Product, RecipePos
from grocy.models.schema import Schema
from tabulate import tabulate
from os import path
class Recipe(Schema):
def __init__(self, **entries):
self.fields = [
'name',
'picture_file_name', 'description',
'base_servings', 'desired_servings',
'not_check_shoppinglist', 'recipes_pos', 'products'
]
self.__dict__.update(entries)
self.recipes_pos = []
self.products = []
self._init_rest_service()
#self._set_default_table_formats()
#if not hasattr('tablefmt', self):
# self.tablefmt = None
#if not hasattr('colalign', self):
# self.colalign = None
def get_list(self):
try:
recipes = self.rest_service.get('recipe')
table_headers = ['#', 'Name']
table_entries = []
for recipe in recipes:
table_entry = [recipe.get('id'), recipe.get('name')]
table_entries.append(table_entry)
except Exception as e:
raise e
# Generate stock overview table
return tabulate(table_entries, headers=table_headers)
def _get_products_by_recipe_id(self):
recipe_products_info = self.rest_service.get('recipes_pos')
for recipe_product_info in recipe_products_info:
if recipe_product_info.get('recipe_id') == self.id:
## TODO: need to find a better way to run a batch call to get only products for recipe
product_info = self.rest_service.get('products', recipe_product_info.get('product_id'))
product = Product(**product_info)
product.id = recipe_product_info.get('product_id')
self.products.append(product)
recipe_pos = RecipePos(**recipe_product_info)
recipe_pos.id = recipe_product_info.get('id')
self.recipes_pos.append(recipe_pos)
def _set_default_table_formats(self):
if not hasattr('formats', self):
self.tablefmt = None
self.colalign = None
elif not hasattr('table', self.formats):
self.tableformat = None
elif not hasattr('col', self.formats):
self.colalign = None
# def _init_rest_service(self):
# if self.api.startswith == '/':
# self.api = self.api[1:]
# if self.api.endswith == '/':
# self.api = self.api[1:-1]
# self.rest_service = RestService(self.api, json=True)
# self.rest_service.addHeader('Content-Type', 'application/json')
# self.rest_service.addToken(self.token)
def toJSON(self):
obj = {}
for attr, value in self.__dict__.items():
isEmptyList = True if type(value) == list and len(value) == 0 else False
if attr in self.fields and not isEmptyList:
obj[attr] = value
return obj
def update(self):
try:
for item in self.products:
product = Product(**item)
self.rest_service.put('products', product, product.id)
#for item in self.recipes_pos:
# self.rest_service.put('recipes_pos', item)
updated_recipe = {
'description': self.description,
'name': self.name,
'base_servings': self.base_servings,
'desired_servings': self.desired_servings,
'not_check_shoppinglist': self.not_check_shoppinglist
}
self.rest_service.put('recipes', updated_recipe, self.id)
except Exception as e:
raise e
def get(self, include_products=False):
try:
recipe = self.rest_service.get('recipes', id=self.id)
if 'description' in recipe:
recipe['description'] = html2text.html2text(recipe['description'].strip())
self.__dict__.update(recipe)
if include_products:
self._get_products_by_recipe_id()
except Exception as e:
raise e

View File

@ -0,0 +1,19 @@
from grocy.models.schema import Schema
class RecipePos(Schema):
def __init__(self, **entries):
self.fields = [
'id', 'recipe_id',
'product_id', 'amount',
'note', 'qu_id',
'only_check_single_unit_in_stock',
'not_check_stock_fulfillment', 'ingredient_group'
]
self.__dict__.update(entries)
def toJSON(self):
obj = {}
for attr, value in self.__dict__.items():
if attr in self.fields:
obj[attr] = value
return obj

15
grocy/models/schema.py Normal file
View File

@ -0,0 +1,15 @@
from grocy import RestService
import logging
class Schema(object):
def _init_rest_service(self):
if hasattr(self, 'api'):
if self.api.startswith == '/':
self.api = self.api[1:]
if self.api.endswith == '/':
self.api = self.api[1:-1]
self.rest_service = RestService(self.api, json=True)
self.rest_service.addHeader('Content-Type', 'application/json')
self.rest_service.addHeader('Accept', 'application/json')
self.rest_service.addToken(self.token)

View File

@ -17,8 +17,6 @@ class Task(object):
#if not hasattr('colalign', self): #if not hasattr('colalign', self):
# self.colalign = None # self.colalign = None
def _set_default_table_formats(self): def _set_default_table_formats(self):
if not hasattr('formats', self): if not hasattr('formats', self):
self.tablefmt = None self.tablefmt = None

View File

@ -1,4 +1,5 @@
click click
jinja2
markdown markdown
html2text html2text
colorama colorama

View File

@ -17,12 +17,12 @@ setup(
author='Aerex', author='Aerex',
author_email='aerex@aerex.me', author_email='aerex@aerex.me',
description=('A plugin to create, delete, and modify tasks across various services'), description=('A plugin to create, delete, and modify tasks across various services'),
keywords='taskwarrior, grocy', keywords='grocy, cli',
url='http://packages.python.org/an_example_pypi_project', url='http://packages.python.org/an_example_pypi_project',
packages=find_packages(), packages=find_packages(),
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,
install_requires=['Click', 'pyyaml', 'requests', 'taskw', 'lockfile', 'tox', 'html2text'], install_requires=['Click', 'pyyaml', 'requests', 'taskw', 'lockfile', 'tox', 'html2text', 'markdown'],
long_description=read('README.rst'), long_description=read('README.rst'),
tests_require=[ tests_require=[
"pytest_mock", "pytest_mock",

View File

@ -1,21 +1,24 @@
name: {{ recipe.name }} name: {{ name }}
description: >- description: |
{{ recipe.description_txt }}
recipe_id: {{ recipe.id }} {{ description }}
picture_file_name: {% if recipe.picture_file_name is not none %}
{{recipe.picture_file_name}} picture_file_name: {% if picture_file_name is not none %}
{{picture_file_name}}
{% else %} {% else %}
{% endif %} {% endif %}
base_servings: {{ recipe.base_servings | default("1") }} base_servings: {{ base_servings | default("1") }}
desired_servings: {{ recipe.desired_servings | default("1") }} desired_servings: {{ desired_servings | default("1") }}
not_checking_shopping_list: {{ recipe.not_checking_shopping_list | default("1") }} not_check_shoppinglist: {{ not_check_shoppinglist | default("1") }}
products: {% for product in recipe.products %} products: {% for product in products %}
- id: {{ product.id }} - id: {{ product.id }}
amount: {{ product.amount }} name: {{ product.name }}
note: {{ product.note }} description: {{ product.description | default(null) }}
qu_id: '' note: {{ recipes_pos[loop.index0].note }}
only_check_single_unit_in_stock: {{ product.only_check_single_unit_in_stock }} amount: {{ recipes_pos[loop.index0].amount }}
ingredient_group: {{ product.ingredient_group | default(null) }} qu_id: {{ recipes_pos[loop.index0].qu_id }}
not_checking_shopping_list: {{ product.not_checking_shopping_list }}{% endfor %} only_check_single_unit_in_stock: {{ recipes_pos[loop.index0].only_check_single_unit_in_stock }}
ingredient_group: {{ recipes_pos[loop.index0].ingredient_group | default(null) }}
not_check_stock_fulfillment: {{ recipes_pos[loop.index0].not_check_stock_fulfillment }}{% endfor %}