Repository: SebRut/pygrocy
Pull request #24 (2019-09-04 07:23:47)

Shopping list API
## Description

Final (I hope)
**Related issue (if applicable):** fixes #<issue number goes here>

## Checklist
  - [x] The code change is tested and works locally.
  - [x] tests pass. **Your PR won't be merged unless tests pass**
  - [x] There is no commented out code in this PR



Patch:
diff --git a/pygrocy/grocy.py b/pygrocy/grocy.py
index 9457f09..ef02bea 100644
--- a/pygrocy/grocy.py
+++ b/pygrocy/grocy.py
@@ -3,6 +3,7 @@ from typing import List
 
 from .grocy_api_client import (ChoreDetailsResponse, CurrentChoreResponse,
                                CurrentStockResponse,
+                               ShoppingListItem,
                                CurrentVolatilStockResponse, GrocyApiClient,
                                ProductData, ProductDetailsResponse,
                                TransactionType, UserDto)
@@ -44,7 +45,39 @@ class Product(object):
     def barcodes(self) -> List[str]:
         return self._barcodes
 
-
+class ShoppingListProduct(object):
+    def __init__(self, raw_shopping_list: ShoppingListItem):
+        self._id = raw_shopping_list.id
+        self._product_id = raw_shopping_list.product_id
+        self._note = raw_shopping_list.note
+        self._amount = raw_shopping_list.amount
+        self._product = None
+        
+    def get_details(self, api_client: GrocyApiClient):
+        self._product = api_client.get_product(self._product_id).product
+        
+    @property
+    def id(self) -> int:
+        return self._id
+        
+    @property
+    def product_id(self) -> int:
+        return self._product_id
+        
+    @property
+    def amount(self) -> int:
+        return self._amount
+        
+    @property
+    def note(self) -> str:
+        return self._note
+        
+    @property
+    def product(self) -> Product:
+        if self._product_id is None:
+            self.get_details()
+        return self._product
+    
 class Chore(object):
     def __init__(self, raw_chore: CurrentChoreResponse):
         self._chore_id = raw_chore.chore_id
@@ -133,3 +166,24 @@ class Grocy(object):
     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()
+        if raw_shoppinglist is None:
+            return
+        shopping_list = [ShoppingListProduct(resp) for resp in raw_shoppinglist]
+
+        if get_details:
+            for item in shopping_list:
+                item.get_details(self._api_client)
+        return shopping_list
+        
+    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 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, shopping_list_product_id: int):
+        return self._api_client.remove_product_in_sl(shopping_list_product_id)
+        
\ No newline at end of file
diff --git a/pygrocy/grocy_api_client.py b/pygrocy/grocy_api_client.py
index 6bb9f13..faa88d9 100644
--- a/pygrocy/grocy_api_client.py
+++ b/pygrocy/grocy_api_client.py
@@ -7,6 +7,32 @@ import requests
 
 from pygrocy.utils import parse_date, parse_float, parse_int
 
+class ShoppingListItem(object):
+    def __init__(self, parsed_json):
+        self._id = parse_int(parsed_json.get('id'))
+        self._product_id = parse_int(parsed_json.get('product_id'))
+        self._note = parsed_json.get('note',None)
+        self._amount = parse_float(parsed_json.get('amount'),0)
+        self._row_created_timestamp = parse_date(parsed_json.get('row_created_timestamp', None))
+        self._shopping_list_id = parse_int(parsed_json.get('shopping_list_id'))
+        self._done = parse_int(parsed_json.get('done'))
+
+
+    @property
+    def id(self) -> int:
+        return self._id
+    
+    @property
+    def product_id(self) -> int:
+        return self._product_id
+
+    @property
+    def note(self) -> str:
+        return self._note
+
+    @property
+    def amount(self) -> float:
+        return self._amount
 
 class QuantityUnitData(object):
     def __init__(self, parsed_json):
@@ -315,3 +341,37 @@ class GrocyApiClient(object):
 
         req_url = urljoin(urljoin(urljoin(self._base_url, "stock/products/"), str(product_id) + "/"), "consume")
         requests.post(req_url, verify=self._verify_ssl, headers=self._headers, data=data)
+
+        
+    def get_shopping_list(self) -> List[ShoppingListItem]:
+        req_url = urljoin(self._base_url, "objects/shopping_list")
+        resp = requests.get(req_url, verify=self._verify_ssl, headers=self._headers)
+        if resp.status_code != 200:
+            return
+        parsed_json = resp.json()
+        return [ShoppingListItem(response) for response in parsed_json]
+
+    def add_missing_product_to_shopping_list(self, shopping_list_id: int = 1):
+        data = {
+            "list_id": shopping_list_id
+        }
+
+        req_url = urljoin(self._base_url, "stock/shoppinglist/add-missing-products")
+        resp = requests.post(req_url, verify=self._verify_ssl, headers=self._headers, data=data)
+        return resp
+            
+    def clear_shopping_list(self, shopping_list_id: int = 1):
+        data = {
+            "list_id": shopping_list_id
+        }
+
+        req_url = urljoin(self._base_url, "stock/shoppinglist/clear")
+        resp = requests.post(req_url, verify=self._verify_ssl, headers=self._headers, data=data)
+        return resp
+            
+    def remove_product_in_sl(self, sl_product_id: int):
+        
+        req_url = urljoin(urljoin(self._base_url, "objects/shopping_list/"), str(sl_product_id))
+        resp = requests.delete(req_url, verify=self._verify_ssl, headers=self._headers)
+        return resp
+        


Test patch:
diff --git a/test/test_ShoppingListItem.py b/test/test_ShoppingListItem.py
new file mode 100644
index 0000000..60e1650
--- /dev/null
+++ b/test/test_ShoppingListItem.py
@@ -0,0 +1,25 @@
+import json
+from unittest import TestCase
+
+from pygrocy.grocy_api_client import ShoppingListItem
+
+
+class TestShoppingListItem(TestCase):
+    def test_parse(self):
+        input_json = """{
+            "id": "1",
+            "product_id": "6",
+            "note": "string",
+            "amount": "2",
+            "row_created_timestamp": "2019-04-17 10:30:00",
+            "shopping_list_id": "1",
+            "done": "0"
+        }"""
+
+        response = ShoppingListItem(json.loads(input_json))
+
+        assert response.product_id == 6
+        assert response.id == 1
+        assert response.note == "string"
+        assert response.amount == 2
+        
\ No newline at end of file
diff --git a/test/test_grocy.py b/test/test_grocy.py
index 21a15eb..0636b2a 100644
--- a/test/test_grocy.py
+++ b/test/test_grocy.py
@@ -3,6 +3,7 @@ from unittest import TestCase
 import responses
 from pygrocy import Grocy
 from pygrocy.grocy import Product
+from pygrocy.grocy import ShoppingListProduct
 from pygrocy.grocy_api_client import CurrentStockResponse, GrocyApiClient
 
 
@@ -121,4 +122,71 @@ class TestGrocy(TestCase):
             }
         ]
         responses.add(responses.GET, "https://example.com/api/stock", json=resp, status=200)
+        
+    @responses.activate
+    def test_get_shopping_list_valid(self):
+        resp = [
+            {
+                "id": 1,
+                "product_id": 6,
+                "note": "string",
+                "amount": 2,
+                "row_created_timestamp": "2019-04-17 10:30:00",
+                "shopping_list_id": 1,
+                "done": 0
+            }
+        ]
+        responses.add(responses.GET, "https://example.com/api/objects/shopping_list", json=resp, status=200)
+
+        shopping_list = self.grocy.shopping_list()
+        
+        assert isinstance(shopping_list, list)
+        assert len(shopping_list) == 1
+        for item in shopping_list:
+            assert isinstance(item, ShoppingListProduct)
+            
+    @responses.activate
+    def test_get_shopping_list_invalid_no_data(self):
+        responses.add(responses.GET, "https://example.com/api/objects/shopping_list", status=400)
+        assert self.grocy.shopping_list() is None
+        
+    @responses.activate
+    def test_get_shopping_list_invalid_missing_data(self):
+        resp = [
+            {
+            }
+        ]
+        responses.add(responses.GET, "https://example.com/api/objects/shopping_list", json=resp, status=200)
+        
+    @responses.activate
+    def test_add_missing_product_to_shopping_list_valid(self):
+        responses.add(responses.POST, "https://example.com/api/stock/shoppinglist/add-missing-products", status=204)
+        assert self.grocy.add_missing_product_to_shopping_list().status_code == 204
+        
+    @responses.activate
+    def test_add_missing_product_to_shopping_list_error(self):
+        responses.add(responses.POST, "https://example.com/api/stock/shoppinglist/add-missing-products", status=400)
+        assert self.grocy.add_missing_product_to_shopping_list().status_code != 204
+        
+    @responses.activate
+    def test_clear_shopping_list_valid(self):
+        responses.add(responses.POST, "https://example.com/api/stock/shoppinglist/clear", status=204)
+        assert self.grocy.clear_shopping_list().status_code == 204
+        
+    @responses.activate
+    def test_clear_shopping_list_error(self):
+        responses.add(responses.POST, "https://example.com/api/stock/shoppinglist/clear", status=400)
+        assert self.grocy.clear_shopping_list().status_code != 204
+        
+    @responses.activate
+    def test_remove_product_in_shopping_list_valid(self):
+        responses.add(responses.DELETE, "https://example.com/api/objects/shopping_list/1", status=204)
+        assert self.grocy.remove_product_in_shopping_list(1).status_code == 204
+        
+    @responses.activate
+    def test_remove_product_in_shopping_list_error(self):
+        responses.add(responses.DELETE, "https://example.com/api/objects/shopping_list/1", status=400)
+        assert self.grocy.remove_product_in_shopping_list(1).status_code != 204
+        
+        
         
\ No newline at end of file