diff --git a/pygrocy/grocy.py b/pygrocy/grocy.py index 1b344f6..6964abe 100644 --- a/pygrocy/grocy.py +++ b/pygrocy/grocy.py @@ -4,14 +4,22 @@ from typing import List, Dict from .base import DataModel -from .grocy_api_client import (DEFAULT_PORT_NUMBER, ChoreDetailsResponse, - CurrentChoreResponse, CurrentStockResponse, - GrocyApiClient, - LocationData, MissingProductResponse, - ProductDetailsResponse, - MealPlanResponse, - RecipeDetailsResponse, - ShoppingListItem, TransactionType, UserDto, TaskResponse) +from .grocy_api_client import ( + DEFAULT_PORT_NUMBER, + ChoreDetailsResponse, + CurrentChoreResponse, + CurrentStockResponse, + GrocyApiClient, + LocationData, + MissingProductResponse, + ProductDetailsResponse, + MealPlanResponse, + RecipeDetailsResponse, + ShoppingListItem, + TransactionType, + UserDto, + TaskResponse, +) class Product(DataModel): @@ -172,20 +180,22 @@ def display_name(self) -> str: return self._display_name -class Chore(DataModel): - class PeriodType(str, Enum): - MANUALLY = 'manually' - DYNAMIC_REGULAR = 'dynamic-regular' - DAILY = 'daily' - WEEKLY = 'weekly' - MONTHLY = 'monthly' - - class AssignmentType(str, Enum): - NO_ASSIGNMENT = 'no-assignment' - WHO_DID_LEAST_DID_FIRST = 'who-did-least-did-first' - RANDOM = 'random' - IN_ALPHABETICAL_ORDER = 'in-alphabetical-order' +class PeriodType(str, Enum): + MANUALLY = "manually" + DYNAMIC_REGULAR = "dynamic-regular" + DAILY = "daily" + WEEKLY = "weekly" + MONTHLY = "monthly" + + +class AssignmentType(str, Enum): + NO_ASSIGNMENT = "no-assignment" + WHO_LEAST_DID_FIRST = "who-least-did-first" + RANDOM = "random" + IN_ALPHABETICAL_ORDER = "in-alphabetical-order" + +class Chore(DataModel): def __init__(self, response): if isinstance(response, CurrentChoreResponse): self._init_from_CurrentChoreResponse(response) @@ -208,7 +218,7 @@ def _init_from_ChoreDetailsResponse(self, response: ChoreDetailsResponse): self._description = chore_data.description if chore_data.period_type is not None: - self._period_type = Chore.PeriodType(chore_data.period_type) + self._period_type = PeriodType(chore_data.period_type) else: self._period_type = None @@ -218,12 +228,14 @@ def _init_from_ChoreDetailsResponse(self, response: ChoreDetailsResponse): self._rollover = chore_data.rollover if chore_data.assignment_type is not None: - self._assignment_type = Chore.AssignmentType(chore_data.assignment_type) + self._assignment_type = AssignmentType(chore_data.assignment_type) else: self._assignment_type = None self._assignment_config = chore_data.assignment_config - self._next_execution_assigned_to_user_id = chore_data.next_execution_assigned_to_user_id + self._next_execution_assigned_to_user_id = ( + chore_data.next_execution_assigned_to_user_id + ) self._userfields = chore_data.userfields self._last_tracked_time = response.last_tracked @@ -234,7 +246,9 @@ def _init_from_ChoreDetailsResponse(self, response: ChoreDetailsResponse): self._last_done_by = None self._track_count = response.track_count if response.next_execution_assigned_user is not None: - self._next_execution_assigned_user = User(response.next_execution_assigned_user) + self._next_execution_assigned_user = User( + response.next_execution_assigned_user + ) else: self._next_execution_assigned_user = None @@ -285,9 +299,9 @@ def assignment_config(self) -> str: @property def next_execution_assigned_to_user_id(self) -> int: return self._next_execution_assigned_to_user_id - + @property - def userfields(self) -> Dict[str,str]: + def userfields(self) -> Dict[str, str]: return self._userfields @property @@ -367,7 +381,7 @@ def picture_file_name(self) -> str: def get_picture_url_path(self, width: int = 400): if self.picture_file_name: - b64name = base64.b64encode(self.picture_file_name.encode('ascii')) + b64name = base64.b64encode(self.picture_file_name.encode("ascii")) path = "files/recipepictures/" + str(b64name, "utf-8") return f"{path}?force_serve_as=picture&best_fit_width={width}" @@ -412,7 +426,9 @@ def get_details(self, api_client: GrocyApiClient): class Grocy(object): - def __init__(self, base_url, api_key, port: int = DEFAULT_PORT_NUMBER, verify_ssl=True): + def __init__( + self, base_url, api_key, port: int = DEFAULT_PORT_NUMBER, verify_ssl=True + ): self._api_client = GrocyApiClient(base_url, api_key, port, verify_ssl) def stock(self) -> List[Product]: @@ -461,20 +477,40 @@ def chores(self, get_details: bool = False) -> List[Chore]: chore.get_details(self._api_client) return chores - def execute_chore(self, chore_id: int, done_by: int = None, tracked_time: datetime = datetime.now()): + def execute_chore( + self, + chore_id: int, + done_by: int = None, + tracked_time: datetime = datetime.now(), + ): return self._api_client.execute_chore(chore_id, done_by, tracked_time) def chore(self, chore_id: int) -> Chore: resp = self._api_client.get_chore(chore_id) return Chore(resp) - def add_product(self, product_id, amount: float, price: float, best_before_date: datetime = None, - transaction_type: TransactionType = TransactionType.PURCHASE): - return self._api_client.add_product(product_id, amount, price, best_before_date, transaction_type) - - def consume_product(self, product_id: int, amount: float = 1, spoiled: bool = False, - transaction_type: TransactionType = TransactionType.CONSUME): - return self._api_client.consume_product(product_id, amount, spoiled, transaction_type) + def add_product( + self, + product_id, + amount: float, + price: float, + best_before_date: datetime = None, + transaction_type: TransactionType = TransactionType.PURCHASE, + ): + return self._api_client.add_product( + product_id, amount, price, best_before_date, transaction_type + ) + + def consume_product( + self, + product_id: int, + amount: float = 1, + spoiled: bool = False, + transaction_type: TransactionType = TransactionType.CONSUME, + ): + return self._api_client.consume_product( + product_id, amount, spoiled, transaction_type + ) def shopping_list(self, get_details: bool = False) -> List[ShoppingListProduct]: raw_shoppinglist = self._api_client.get_shopping_list() @@ -488,14 +524,22 @@ def shopping_list(self, get_details: bool = False) -> List[ShoppingListProduct]: def add_missing_product_to_shopping_list(self, shopping_list_id: int = 1): return self._api_client.add_missing_product_to_shopping_list(shopping_list_id) - def add_product_to_shopping_list(self, product_id: int, shopping_list_id: int = None, amount: int = None): - return self._api_client.add_product_to_shopping_list(product_id, shopping_list_id, amount) + def add_product_to_shopping_list( + self, product_id: int, shopping_list_id: int = None, amount: int = None + ): + return self._api_client.add_product_to_shopping_list( + product_id, shopping_list_id, amount + ) def clear_shopping_list(self, shopping_list_id: int = 1): return self._api_client.clear_shopping_list(shopping_list_id) - def remove_product_in_shopping_list(self, product_id: int, shopping_list_id: int = 1, amount: int = 1): - return self._api_client.remove_product_in_shopping_list(product_id, shopping_list_id, amount) + def remove_product_in_shopping_list( + self, product_id: int, shopping_list_id: int = 1, amount: int = 1 + ): + return self._api_client.remove_product_in_shopping_list( + product_id, shopping_list_id, amount + ) def product_groups(self) -> List[Group]: raw_groups = self._api_client.get_product_groups() diff --git a/test/test_grocy.py b/test/test_grocy.py index e2a0a4b..2a742cc 100644 --- a/test/test_grocy.py +++ b/test/test_grocy.py @@ -8,19 +8,19 @@ from requests.exceptions import HTTPError from pygrocy import Grocy -from pygrocy.grocy import Chore, Product, Group, ShoppingListProduct -from pygrocy.grocy_api_client import GrocyApiClient, UserDto, \ - ProductData +from pygrocy.grocy import Chore, Product, Group, ShoppingListProduct, AssignmentType +from pygrocy.grocy_api_client import GrocyApiClient, UserDto, ProductData from test.test_const import CONST_BASE_URL, CONST_PORT, CONST_SSL class TestGrocy(TestCase): - def setUp(self): self.grocy_regular = Grocy(CONST_BASE_URL, "api_key") - self.grocy = Grocy(CONST_BASE_URL, "demo_mode", verify_ssl=CONST_SSL, port=CONST_PORT) + self.grocy = Grocy( + CONST_BASE_URL, "demo_mode", verify_ssl=CONST_SSL, port=CONST_PORT + ) self.base_url = f"{CONST_BASE_URL}:{CONST_PORT}/api" - self.date_test = datetime.strptime("2019-05-04 11:31:04", '%Y-%m-%d %H:%M:%S') + self.date_test = datetime.strptime("2019-05-04 11:31:04", "%Y-%m-%d %H:%M:%S") def test_init(self): self.assertIsInstance(self.grocy, Grocy) @@ -31,7 +31,7 @@ def test_get_tasks_valid(self): assert len(tasks) == 6 assert tasks[0].id == 1 - assert tasks[0].name == 'Repair the garage door' + assert tasks[0].name == "Repair the garage door" @unittest.skip("no chores_current table in current demo data") def test_get_chores_valid(self): @@ -60,7 +60,7 @@ def test_get_chore_details_valid(self): "period_config": null, "track_date_only": "0", "rollover": "0", - "assignment_type": null, + "assignment_type": "who-least-did-first", "assignment_config": null, "next_execution_assigned_to_user_id": null, "consume_product_on_execution": "0", @@ -73,9 +73,14 @@ def test_get_chore_details_valid(self): "next_execution_assigned_user": null }""" details_json = json.loads(details_json) - responses.add(responses.GET, f"{self.base_url}/chores/1", json=details_json, status=200) + responses.add( + responses.GET, f"{self.base_url}/chores/1", json=details_json, status=200 + ) chore_details = self.grocy.chore(1) self.assertIsInstance(chore_details, Chore) + self.assertEqual( + chore_details.assignment_type, AssignmentType.WHO_LEAST_DID_FIRST + ) @unittest.skip("no stock_current table in current demo data") def test_product_get_details_valid(self): @@ -83,7 +88,9 @@ def test_product_get_details_valid(self): product = stock[0] - api_client = GrocyApiClient(CONST_BASE_URL, "demo_mode", port=CONST_PORT, verify_ssl=CONST_SSL) + api_client = GrocyApiClient( + CONST_BASE_URL, "demo_mode", port=CONST_PORT, verify_ssl=CONST_SSL + ) product.get_details(api_client) self.assertIsInstance(product.name, str) @@ -134,13 +141,20 @@ def test_get_shopping_list_valid(self): @responses.activate def test_get_shopping_list_invalid_no_data(self): - responses.add(responses.GET, f"{self.base_url}/objects/shopping_list", status=400) + responses.add( + responses.GET, f"{self.base_url}/objects/shopping_list", status=400 + ) self.assertRaises(HTTPError, self.grocy.shopping_list) @responses.activate def test_get_shopping_list_invalid_missing_data(self): resp = [] - responses.add(responses.GET, f"{self.base_url}/objects/shopping_list", json=resp, status=200) + responses.add( + responses.GET, + f"{self.base_url}/objects/shopping_list", + json=resp, + status=200, + ) self.assertEqual(len(self.grocy.shopping_list()), 0) @unittest.skip("no shopping list existing in current demo data") @@ -149,7 +163,11 @@ def test_add_missing_product_to_shopping_list_valid(self): @responses.activate def test_add_missing_product_to_shopping_list_error(self): - responses.add(responses.POST, f"{self.base_url}/stock/shoppinglist/add-missing-products", status=400) + responses.add( + responses.POST, + f"{self.base_url}/stock/shoppinglist/add-missing-products", + status=400, + ) self.assertRaises(HTTPError, self.grocy.add_missing_product_to_shopping_list) @unittest.skip("no shopping list existing in current demo data") @@ -162,22 +180,34 @@ def test_add_product_to_shopping_list_error(self): @responses.activate def test_clear_shopping_list_valid(self): - responses.add(responses.POST, f"{self.base_url}/stock/shoppinglist/clear", status=204) + responses.add( + responses.POST, f"{self.base_url}/stock/shoppinglist/clear", status=204 + ) self.grocy.clear_shopping_list() @responses.activate def test_clear_shopping_list_error(self): - responses.add(responses.POST, f"{self.base_url}/stock/shoppinglist/clear", status=400) + responses.add( + responses.POST, f"{self.base_url}/stock/shoppinglist/clear", status=400 + ) self.assertRaises(HTTPError, self.grocy.clear_shopping_list) @responses.activate def test_remove_product_in_shopping_list_valid(self): - responses.add(responses.POST, f"{self.base_url}/stock/shoppinglist/remove-product", status=204) + responses.add( + responses.POST, + f"{self.base_url}/stock/shoppinglist/remove-product", + status=204, + ) self.grocy.remove_product_in_shopping_list(1) @responses.activate def test_remove_product_in_shopping_list_error(self): - responses.add(responses.POST, f"{self.base_url}/stock/shoppinglist/remove-product", status=400) + responses.add( + responses.POST, + f"{self.base_url}/stock/shoppinglist/remove-product", + status=400, + ) self.assertRaises(HTTPError, self.grocy.remove_product_in_shopping_list, 1) @unittest.skip("no userentities table in current demo data") @@ -195,22 +225,35 @@ def test_get_product_groups_valid(self): @responses.activate def test_get_product_groups_invalid_no_data(self): - responses.add(responses.GET, f"{self.base_url}/objects/product_groups", status=400) + responses.add( + responses.GET, f"{self.base_url}/objects/product_groups", status=400 + ) self.assertRaises(HTTPError, self.grocy.product_groups) @responses.activate def test_get_product_groups_invalid_missing_data(self): resp = [] - responses.add(responses.GET, f"{self.base_url}/objects/product_groups", json=resp, status=200) + responses.add( + responses.GET, + f"{self.base_url}/objects/product_groups", + json=resp, + status=200, + ) self.assertEqual(len(self.grocy.product_groups()), 0) @responses.activate def test_add_product_pic_valid(self): with patch("os.path.exists") as m_exist: - with patch("builtins.open", mock_open()) as m_open: + with patch("builtins.open", mock_open()): m_exist.return_value = True - responses.add(responses.PUT, f"{self.base_url}/files/productpictures/MS5qcGc=", status=204) - responses.add(responses.PUT, f"{self.base_url}/objects/products/1", status=204) + responses.add( + responses.PUT, + f"{self.base_url}/files/productpictures/MS5qcGc=", + status=204, + ) + responses.add( + responses.PUT, f"{self.base_url}/objects/products/1", status=204 + ) resp = self.grocy.add_product_pic(1, "/somepath/pic.jpg") self.assertIsNone(resp) @@ -218,20 +261,32 @@ def test_add_product_pic_valid(self): def test_add_product_pic_invalid_missing_data(self): with patch("os.path.exists") as m_exist: m_exist.return_value = False - self.assertRaises(FileNotFoundError, self.grocy.add_product_pic, 1, "/somepath/pic.jpg") + self.assertRaises( + FileNotFoundError, self.grocy.add_product_pic, 1, "/somepath/pic.jpg" + ) @responses.activate def test_upload_product_picture_error(self): with patch("os.path.exists") as m_exist: - with patch("builtins.open", mock_open()) as m_open: + with patch("builtins.open", mock_open()): m_exist.return_value = True - api_client = GrocyApiClient(CONST_BASE_URL, "demo_mode", port=CONST_PORT, verify_ssl=CONST_SSL) - responses.add(responses.PUT, f"{self.base_url}/files/productpictures/MS5qcGc=", status=400) - self.assertRaises(HTTPError, api_client.upload_product_picture, 1, "/somepath/pic.jpg") + api_client = GrocyApiClient( + CONST_BASE_URL, "demo_mode", port=CONST_PORT, verify_ssl=CONST_SSL + ) + responses.add( + responses.PUT, + f"{self.base_url}/files/productpictures/MS5qcGc=", + status=400, + ) + self.assertRaises( + HTTPError, api_client.upload_product_picture, 1, "/somepath/pic.jpg" + ) @responses.activate def test_update_product_pic_error(self): - api_client = GrocyApiClient(CONST_BASE_URL, "demo_mode", port=CONST_PORT, verify_ssl=CONST_SSL) + api_client = GrocyApiClient( + CONST_BASE_URL, "demo_mode", port=CONST_PORT, verify_ssl=CONST_SSL + ) responses.add(responses.PUT, f"{self.base_url}/objects/products/1", status=400) self.assertRaises(HTTPError, api_client.update_product_pic, 1) @@ -247,19 +302,19 @@ def test_get_expiring_products_valid(self): @responses.activate def test_get_expiring_invalid_no_data(self): - resp = { - "expiring_products": [], - "expired_products": [], - "missing_products": [] - } - responses.add(responses.GET, f"{self.base_url}/stock/volatile", json=resp, status=200) + resp = {"expiring_products": [], "expired_products": [], "missing_products": []} + responses.add( + responses.GET, f"{self.base_url}/stock/volatile", json=resp, status=200 + ) self.grocy.expiring_products(True) @responses.activate def test_get_expiring_invalid_missing_data(self): resp = {} - responses.add(responses.GET, f"{self.base_url}/stock/volatile", json=resp, status=200) + responses.add( + responses.GET, f"{self.base_url}/stock/volatile", json=resp, status=200 + ) @unittest.skip("no stock_current table in current demo data") def test_get_expired_products_valid(self): @@ -273,19 +328,19 @@ def test_get_expired_products_valid(self): @responses.activate def test_get_expired_invalid_no_data(self): - resp = { - "expiring_products": [], - "expired_products": [], - "missing_products": [] - } - responses.add(responses.GET, f"{self.base_url}/stock/volatile", json=resp, status=200) + resp = {"expiring_products": [], "expired_products": [], "missing_products": []} + responses.add( + responses.GET, f"{self.base_url}/stock/volatile", json=resp, status=200 + ) self.grocy.expired_products(True) @responses.activate def test_get_expired_invalid_missing_data(self): resp = {} - responses.add(responses.GET, f"{self.base_url}/stock/volatile", json=resp, status=200) + responses.add( + responses.GET, f"{self.base_url}/stock/volatile", json=resp, status=200 + ) @unittest.skip("no stock_current table in current demo data") def test_get_missing_products_valid(self): @@ -301,32 +356,31 @@ def test_get_missing_products_valid(self): @responses.activate def test_get_missing_invalid_no_data(self): - resp = { - "expiring_products": [], - "expired_products": [], - "missing_products": [] - } - responses.add(responses.GET, f"{self.base_url}/stock/volatile", json=resp, status=200) + resp = {"expiring_products": [], "expired_products": [], "missing_products": []} + responses.add( + responses.GET, f"{self.base_url}/stock/volatile", json=resp, status=200 + ) self.grocy.missing_products(True) @responses.activate def test_get_missing_invalid_missing_data(self): resp = {} - responses.add(responses.GET, f"{self.base_url}/stock/volatile", json=resp, status=200) + responses.add( + responses.GET, f"{self.base_url}/stock/volatile", json=resp, status=200 + ) @responses.activate def test_get_userfields_valid(self): - resp = { - "uf1": 0, - "uf2": "string" - } + resp = {"uf1": 0, "uf2": "string"} - responses.add(responses.GET, f"{self.base_url}/userfields/chores/1", json=resp, status=200) + responses.add( + responses.GET, f"{self.base_url}/userfields/chores/1", json=resp, status=200 + ) a_chore_uf = self.grocy.get_userfields("chores", 1) - self.assertEqual(a_chore_uf['uf1'], 0) + self.assertEqual(a_chore_uf["uf1"], 0) @unittest.skip("no userentities table in current demo data") def test_get_userfields_invalid_no_data(self): @@ -340,7 +394,9 @@ def test_set_userfields_valid(self): @responses.activate def test_set_userfields_error(self): responses.add(responses.PUT, f"{self.base_url}/userfields/chores/1", status=400) - self.assertRaises(HTTPError, self.grocy.set_userfields, "chores", 1, "auserfield", "value") + self.assertRaises( + HTTPError, self.grocy.set_userfields, "chores", 1, "auserfield", "value" + ) def test_get_last_db_changed_valid(self): @@ -351,28 +407,43 @@ def test_get_last_db_changed_valid(self): @responses.activate def test_get_last_db_changed_invalid_no_data(self): resp = {} - responses.add(responses.GET, f"{self.base_url}/system/db-changed-time", json=resp, status=200) + responses.add( + responses.GET, + f"{self.base_url}/system/db-changed-time", + json=resp, + status=200, + ) self.assertIsNone(self.grocy.get_last_db_changed()) @responses.activate def test_add_product_valid(self): - responses.add(responses.POST, f"{self.base_url}/stock/products/1/add", status=200) + responses.add( + responses.POST, f"{self.base_url}/stock/products/1/add", status=200 + ) self.assertIsNone(self.grocy.add_product(1, 1.3, 2.44, self.date_test)) @responses.activate def test_add_product_error(self): - responses.add(responses.POST, f"{self.base_url}/stock/products/1/add", status=400) - self.assertRaises(HTTPError, self.grocy.add_product, 1, 1.3, 2.44, self.date_test) + responses.add( + responses.POST, f"{self.base_url}/stock/products/1/add", status=400 + ) + self.assertRaises( + HTTPError, self.grocy.add_product, 1, 1.3, 2.44, self.date_test + ) @responses.activate def test_consume_product_valid(self): - responses.add(responses.POST, f"{self.base_url}/stock/products/1/consume", status=200) + responses.add( + responses.POST, f"{self.base_url}/stock/products/1/consume", status=200 + ) self.assertIsNone(self.grocy.consume_product(1, 1.3, self.date_test)) @responses.activate def test_consume_product_error(self): - responses.add(responses.POST, f"{self.base_url}/stock/products/1/consume", status=400) + responses.add( + responses.POST, f"{self.base_url}/stock/products/1/consume", status=400 + ) self.assertRaises(HTTPError, self.grocy.consume_product, 1, 1.3, self.date_test) @responses.activate @@ -387,7 +458,8 @@ def test_execute_chore_error(self): @responses.activate def test_get_meal_plan(self): - resp_json = json.loads("""[ + resp_json = json.loads( + """[ { "id": "1", "day": "2020-08-10", @@ -401,8 +473,14 @@ def test_get_meal_plan(self): "row_created_timestamp": "2020-08-12 19:59:30", "userfields": null } - ]""") - responses.add(responses.GET, f"{self.base_url}/objects/meal_plan", json=resp_json, status=200) + ]""" + ) + responses.add( + responses.GET, + f"{self.base_url}/objects/meal_plan", + json=resp_json, + status=200, + ) meal_plan = self.grocy.meal_plan() self.assertEqual(len(meal_plan), 1) self.assertEqual(meal_plan[0].id, 1) @@ -410,7 +488,8 @@ def test_get_meal_plan(self): @responses.activate def test_get_recipe(self): - resp_json = json.loads("""{ + resp_json = json.loads( + """{ "id": "1", "name": "Pizza", "description": "

Mix everything

", @@ -422,8 +501,14 @@ def test_get_recipe(self): "type": "normal", "product_id": "", "userfields": null - }""") - responses.add(responses.GET, f"{self.base_url}/objects/recipes/1", json=resp_json, status=200) + }""" + ) + responses.add( + responses.GET, + f"{self.base_url}/objects/recipes/1", + json=resp_json, + status=200, + ) recipe = self.grocy.recipe(1) self.assertEqual(recipe.id, 1) self.assertEqual(recipe.name, "Pizza")