diff --git a/grocy/__init__.py b/grocy/__init__.py index f7fd106..cc96a1b 100644 --- a/grocy/__init__.py +++ b/grocy/__init__.py @@ -1,30 +1,46 @@ import requests +from dataclasses import asdict import requests_cache 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): 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): self.api_url = api_url self.headers = {} self.json = json - def get(self, path, id=None): - - # TODO: change this to a single pattern assume a pattern then stick with it - if self.api_url.endswith('/'): - url = '{0}{1}'.format(self.api_url, path[1:]) + def put(self, entity_name, entity, entity_id): + if type(entity) is not dict: + json_payload = entity.toJSON() else: - url = '{0}{1}'.format(self.api_url, path) + json_payload = entity - if id: - url = '{0}/{1}'.format(url, id) + url = RestService.RESOURCE_URL_TEMPLATE.format(api_url=self.api_url, entity=entity_name, objectId=entity_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) if r.raise_for_status(): - logger.error(r.raise_for_status()) raise r.raise_for_status() if self.json: @@ -41,7 +57,6 @@ class RestService(object): url = '{0}/{1}'.format(api_url, path) r = requests.get(url, headers=self.headers) - if r.raise_for_status(): logger.error(r.raise_for_status()) raise r.raise_for_status() @@ -62,7 +77,6 @@ class RestService(object): else: url = '{0}/{1}'.format(api_url, path) - print('{}'.format(url)) r = requests.post(url, data=json.dumps(payload), headers=self.headers) #if r.raise_for_status(): diff --git a/grocy/cli.py b/grocy/cli.py index 8d77c67..b00a3c2 100644 --- a/grocy/cli.py +++ b/grocy/cli.py @@ -1,8 +1,11 @@ import click +from markdown import markdown +from dataclasses import asdict, replace from jinja2 import Environment, FileSystemLoader from uuid import uuid4 from grocy import RestService -from grocy.commands import * +from grocy.models import (Stock, +Battery, Shopping, Recipe) from pkg_resources import iter_entry_points import yaml from sys import exit @@ -115,10 +118,12 @@ def edit(ctx, recipe_id): recipe = Recipe(id=recipe_id, **cfg) recipe.get(include_products=True) 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: - updated_recipe = Recipe(id=receipe_id, **edited_recipe) - updated_recipe.update() + 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)) diff --git a/grocy/commands/__init__.py b/grocy/commands/__init__.py deleted file mode 100644 index 26556e8..0000000 --- a/grocy/commands/__init__.py +++ /dev/null @@ -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 diff --git a/grocy/commands/product.py b/grocy/commands/product.py deleted file mode 100644 index 4390983..0000000 --- a/grocy/commands/product.py +++ /dev/null @@ -1,3 +0,0 @@ -class Product(object): - def __init__(self, **entries): - self.__dict__.update(entries) diff --git a/grocy/commands/recipe.py b/grocy/commands/recipe.py deleted file mode 100644 index 2992a8c..0000000 --- a/grocy/commands/recipe.py +++ /dev/null @@ -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 - diff --git a/grocy/models/__init__.py b/grocy/models/__init__.py new file mode 100644 index 0000000..6e3c9b1 --- /dev/null +++ b/grocy/models/__init__.py @@ -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 diff --git a/grocy/commands/battery.py b/grocy/models/battery.py similarity index 100% rename from grocy/commands/battery.py rename to grocy/models/battery.py diff --git a/grocy/commands/chore.py b/grocy/models/chore.py similarity index 99% rename from grocy/commands/chore.py rename to grocy/models/chore.py index 8b5548c..927a3e5 100644 --- a/grocy/commands/chore.py +++ b/grocy/models/chore.py @@ -16,7 +16,6 @@ class Chore(object): #if not hasattr('colalign', self): # self.colalign = None - def _set_default_table_formats(self): if not hasattr('formats', self): diff --git a/grocy/models/product.py b/grocy/models/product.py new file mode 100644 index 0000000..ed618c8 --- /dev/null +++ b/grocy/models/product.py @@ -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 diff --git a/grocy/models/recipe.py b/grocy/models/recipe.py new file mode 100644 index 0000000..418c535 --- /dev/null +++ b/grocy/models/recipe.py @@ -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 diff --git a/grocy/models/recipe_pos.py b/grocy/models/recipe_pos.py new file mode 100644 index 0000000..4028830 --- /dev/null +++ b/grocy/models/recipe_pos.py @@ -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 diff --git a/grocy/models/schema.py b/grocy/models/schema.py new file mode 100644 index 0000000..e41fa25 --- /dev/null +++ b/grocy/models/schema.py @@ -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) diff --git a/grocy/commands/shopping.py b/grocy/models/shopping.py similarity index 100% rename from grocy/commands/shopping.py rename to grocy/models/shopping.py diff --git a/grocy/commands/stock.py b/grocy/models/stock.py similarity index 100% rename from grocy/commands/stock.py rename to grocy/models/stock.py diff --git a/grocy/commands/task.py b/grocy/models/task.py similarity index 99% rename from grocy/commands/task.py rename to grocy/models/task.py index 60d5385..4fd76f4 100644 --- a/grocy/commands/task.py +++ b/grocy/models/task.py @@ -17,8 +17,6 @@ class Task(object): #if not hasattr('colalign', self): # self.colalign = None - - def _set_default_table_formats(self): if not hasattr('formats', self): self.tablefmt = None diff --git a/requirements.txt b/requirements.txt index 749c48b..b2bedee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ click +jinja2 markdown html2text colorama diff --git a/setup.py b/setup.py index fd69b8d..b6708c4 100644 --- a/setup.py +++ b/setup.py @@ -17,12 +17,12 @@ setup( author='Aerex', author_email='aerex@aerex.me', 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', packages=find_packages(), include_package_data=True, 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'), tests_require=[ "pytest_mock", diff --git a/templates/recipe.yml b/templates/recipe.yml index 2857007..4ddb8e5 100644 --- a/templates/recipe.yml +++ b/templates/recipe.yml @@ -1,21 +1,24 @@ -name: {{ recipe.name }} -description: >- -{{ recipe.description_txt }} -recipe_id: {{ recipe.id }} -picture_file_name: {% if recipe.picture_file_name is not none %} -{{recipe.picture_file_name}} +name: {{ name }} +description: | + +{{ description }} + +picture_file_name: {% if picture_file_name is not none %} +{{picture_file_name}} {% else %} {% endif %} -base_servings: {{ recipe.base_servings | default("1") }} -desired_servings: {{ recipe.desired_servings | default("1") }} -not_checking_shopping_list: {{ recipe.not_checking_shopping_list | default("1") }} -products: {% for product in recipe.products %} +base_servings: {{ base_servings | default("1") }} +desired_servings: {{ desired_servings | default("1") }} +not_check_shoppinglist: {{ not_check_shoppinglist | default("1") }} +products: {% for product in products %} - id: {{ product.id }} - amount: {{ product.amount }} - note: {{ product.note }} - qu_id: '' - only_check_single_unit_in_stock: {{ product.only_check_single_unit_in_stock }} - ingredient_group: {{ product.ingredient_group | default(null) }} - not_checking_shopping_list: {{ product.not_checking_shopping_list }}{% endfor %} + name: {{ product.name }} + description: {{ product.description | default(null) }} + note: {{ recipes_pos[loop.index0].note }} + amount: {{ recipes_pos[loop.index0].amount }} + qu_id: {{ recipes_pos[loop.index0].qu_id }} + 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 %}