Repository: SebRut/pygrocy
Pull request #242 (2022-07-06 19:49:49)

Add support for filter conditions
## Description

- Add an optional filter parameter for endpoints supporting this. Filter accepts an optional list of string filter conditions in the format `<field><condition><value>` (as specified in the Grocy REST API specifications).

- Add tests.

- In VCR configuration fixture, ensure base64 compressed responses are always decoded.

## 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 af07931..91fa624 100644
--- a/pygrocy/grocy.py
+++ b/pygrocy/grocy.py
@@ -109,8 +109,10 @@ class Grocy(object):
         product_datas = [ProductData(**product) for product in raw_products]
         return [Product(product) for product in product_datas]
 
-    def chores(self, get_details: bool = False) -> List[Chore]:
-        raw_chores = self._api_client.get_chores()
+    def chores(
+        self, get_details: bool = False, query_filters: List[str] = None
+    ) -> List[Chore]:
+        raw_chores = self._api_client.get_chores(query_filters)
         chores = [Chore(chore) for chore in raw_chores]
 
         if get_details:
@@ -238,8 +240,10 @@ class Grocy(object):
             product.get_details(self._api_client)
         return product
 
-    def shopping_list(self, get_details: bool = False) -> List[ShoppingListProduct]:
-        raw_shoppinglist = self._api_client.get_shopping_list()
+    def shopping_list(
+        self, get_details: bool = False, query_filters: List[str] = None
+    ) -> List[ShoppingListProduct]:
+        raw_shoppinglist = self._api_client.get_shopping_list(query_filters)
         shopping_list = [ShoppingListProduct(resp) for resp in raw_shoppinglist]
 
         if get_details:
@@ -271,8 +275,8 @@ class Grocy(object):
             product_id, shopping_list_id, amount
         )
 
-    def product_groups(self) -> List[Group]:
-        raw_groups = self._api_client.get_product_groups()
+    def product_groups(self, query_filters: List[str] = None) -> List[Group]:
+        raw_groups = self._api_client.get_product_groups(query_filters)
         return [Group(resp) for resp in raw_groups]
 
     def add_product_pic(self, product_id: int, pic_path: str):
@@ -288,8 +292,8 @@ class Grocy(object):
     def get_last_db_changed(self):
         return self._api_client.get_last_db_changed()
 
-    def tasks(self) -> List[Task]:
-        raw_tasks = self._api_client.get_tasks()
+    def tasks(self, query_filters: List[str] = None) -> List[Task]:
+        raw_tasks = self._api_client.get_tasks(query_filters)
         return [Task(task) for task in raw_tasks]
 
     def task(self, task_id: int) -> Task:
@@ -299,8 +303,10 @@ class Grocy(object):
     def complete_task(self, task_id, done_time: datetime = datetime.now()):
         return self._api_client.complete_task(task_id, done_time)
 
-    def meal_plan(self, get_details: bool = False) -> List[MealPlanItem]:
-        raw_meal_plan = self._api_client.get_meal_plan()
+    def meal_plan(
+        self, get_details: bool = False, query_filters: List[str] = None
+    ) -> List[MealPlanItem]:
+        raw_meal_plan = self._api_client.get_meal_plan(query_filters)
         meal_plan = [MealPlanItem(data) for data in raw_meal_plan]
 
         if get_details:
@@ -313,8 +319,8 @@ class Grocy(object):
         if recipe:
             return RecipeItem(recipe)
 
-    def batteries(self) -> List[Battery]:
-        raw_batteries = self._api_client.get_batteries()
+    def batteries(self, query_filters: List[str] = None) -> List[Battery]:
+        raw_batteries = self._api_client.get_batteries(query_filters)
         return [Battery(bat) for bat in raw_batteries]
 
     def battery(self, battery_id: int) -> Battery:
@@ -336,11 +342,17 @@ class Grocy(object):
     def delete_generic(self, entity_type: EntityType, object_id: int):
         return self._api_client.delete_generic(entity_type, object_id)
 
-    def get_generic_objects_for_type(self, entity_type: EntityType):
-        return self._api_client.get_generic_objects_for_type(entity_type.value)
+    def get_generic_objects_for_type(
+        self, entity_type: EntityType, query_filters: List[str] = None
+    ):
+        return self._api_client.get_generic_objects_for_type(
+            entity_type.value, query_filters
+        )
 
-    def meal_plan_sections(self) -> List[MealPlanSection]:
-        raw_sections = self._api_client.get_meal_plan_sections()
+    def meal_plan_sections(
+        self, query_filters: List[str] = None
+    ) -> List[MealPlanSection]:
+        raw_sections = self._api_client.get_meal_plan_sections(query_filters)
         return [MealPlanSection(section) for section in raw_sections]
 
     def meal_plan_section(self, meal_plan_section_id: int) -> MealPlanSection:
diff --git a/pygrocy/grocy_api_client.py b/pygrocy/grocy_api_client.py
index fbbb4d3..7587721 100644
--- a/pygrocy/grocy_api_client.py
+++ b/pygrocy/grocy_api_client.py
@@ -3,7 +3,7 @@ import json
 import logging
 from datetime import datetime
 from enum import Enum
-from typing import Dict, List, Optional, Any
+from typing import Any, Dict, List, Optional
 from urllib.parse import urljoin
 
 import requests
@@ -291,9 +291,14 @@ class GrocyApiClient(object):
         else:
             self._headers = {"accept": "application/json", "GROCY-API-KEY": api_key}
 
-    def _do_get_request(self, end_url: str):
+    def _do_get_request(self, end_url: str, query_filters: List[str] = None):
         req_url = urljoin(self._base_url, end_url)
-        resp = requests.get(req_url, verify=self._verify_ssl, headers=self._headers)
+        params = None
+        if query_filters:
+            params = {"query[]": query_filters}
+        resp = requests.get(
+            req_url, verify=self._verify_ssl, headers=self._headers, params=params
+        )
 
         _LOGGER.debug("-->\tGET /%s", end_url)
         _LOGGER.debug("<--\t%d for /%s", resp.status_code, end_url)
@@ -379,8 +384,8 @@ class GrocyApiClient(object):
         if parsed_json:
             return ProductDetailsResponse(**parsed_json)
 
-    def get_chores(self) -> List[CurrentChoreResponse]:
-        parsed_json = self._do_get_request("chores")
+    def get_chores(self, query_filters: List[str] = None) -> List[CurrentChoreResponse]:
+        parsed_json = self._do_get_request("chores", query_filters)
         return [CurrentChoreResponse(**chore) for chore in parsed_json]
 
     def get_chore(self, chore_id: int) -> ChoreDetailsResponse:
@@ -558,8 +563,10 @@ class GrocyApiClient(object):
             stockLog = [StockLogResponse(**response) for response in parsed_json]
             return stockLog[0]
 
-    def get_shopping_list(self) -> List[ShoppingListItem]:
-        parsed_json = self._do_get_request("objects/shopping_list")
+    def get_shopping_list(
+        self, query_filters: List[str] = None
+    ) -> List[ShoppingListItem]:
+        parsed_json = self._do_get_request("objects/shopping_list", query_filters)
         return [ShoppingListItem(**response) for response in parsed_json]
 
     def add_missing_product_to_shopping_list(self, shopping_list_id: int = None):
@@ -600,8 +607,8 @@ class GrocyApiClient(object):
         }
         self._do_post_request("stock/shoppinglist/remove-product", data)
 
-    def get_product_groups(self) -> List[LocationData]:
-        parsed_json = self._do_get_request("objects/product_groups")
+    def get_product_groups(self, query_filters: List[str] = None) -> List[LocationData]:
+        parsed_json = self._do_get_request("objects/product_groups", query_filters)
         return [LocationData(**response) for response in parsed_json]
 
     def upload_product_picture(self, product_id: int, pic_path: str):
@@ -628,8 +635,8 @@ class GrocyApiClient(object):
         last_change_timestamp = parse_date(resp.get("changed_time"))
         return last_change_timestamp
 
-    def get_tasks(self) -> List[TaskResponse]:
-        parsed_json = self._do_get_request("tasks")
+    def get_tasks(self, query_filters: List[str] = None) -> List[TaskResponse]:
+        parsed_json = self._do_get_request("tasks", query_filters)
         return [TaskResponse(**data) for data in parsed_json]
 
     def get_task(self, task_id: int) -> TaskResponse:
@@ -645,8 +652,8 @@ class GrocyApiClient(object):
         data = {"done_time": localized_done_time.isoformat()}
         self._do_post_request(url, data)
 
-    def get_meal_plan(self) -> List[MealPlanResponse]:
-        parsed_json = self._do_get_request("objects/meal_plan")
+    def get_meal_plan(self, query_filters: List[str] = None) -> List[MealPlanResponse]:
+        parsed_json = self._do_get_request("objects/meal_plan", query_filters)
         return [MealPlanResponse(**data) for data in parsed_json]
 
     def get_recipe(self, object_id: int) -> RecipeDetailsResponse:
@@ -654,8 +661,10 @@ class GrocyApiClient(object):
         if parsed_json:
             return RecipeDetailsResponse(**parsed_json)
 
-    def get_batteries(self) -> List[CurrentBatteryResponse]:
-        parsed_json = self._do_get_request("batteries")
+    def get_batteries(
+        self, query_filters: List[str] = None
+    ) -> List[CurrentBatteryResponse]:
+        parsed_json = self._do_get_request("batteries", query_filters)
         if parsed_json:
             return [CurrentBatteryResponse(**data) for data in parsed_json]
 
@@ -679,11 +688,17 @@ class GrocyApiClient(object):
     def delete_generic(self, entity_type: str, object_id: int):
         return self._do_delete_request(f"objects/{entity_type}/{object_id}")
 
-    def get_generic_objects_for_type(self, entity_type: str):
-        return self._do_get_request(f"objects/{entity_type}")
+    def get_generic_objects_for_type(
+        self, entity_type: str, query_filters: List[str] = None
+    ):
+        return self._do_get_request(f"objects/{entity_type}", query_filters)
 
-    def get_meal_plan_sections(self) -> List[MealPlanSectionResponse]:
-        parsed_json = self.get_generic_objects_for_type(EntityType.MEAL_PLAN_SECTIONS)
+    def get_meal_plan_sections(
+        self, query_filters: List[str] = None
+    ) -> List[MealPlanSectionResponse]:
+        parsed_json = self.get_generic_objects_for_type(
+            EntityType.MEAL_PLAN_SECTIONS, query_filters
+        )
         if parsed_json:
             return [MealPlanSectionResponse(**resp) for resp in parsed_json]
 


Test patch:
diff --git a/test/cassettes/test_battery/TestBattery.test_get_batteries_filters_invalid.yaml b/test/cassettes/test_battery/TestBattery.test_get_batteries_filters_invalid.yaml
new file mode 100644
index 0000000..7b6c6de
--- /dev/null
+++ b/test/cassettes/test_battery/TestBattery.test_get_batteries_filters_invalid.yaml
@@ -0,0 +1,64 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      User-Agent:
+      - python-requests/2.28.0
+      accept:
+      - application/json
+    method: GET
+    uri: https://localhost/api/batteries?query%5B%5D=invalid
+  response:
+    body:
+      string: '{"error_message":"Invalid query","error_details":{"stack_trace":"#0
+        \/var\/www\/controllers\/BaseApiController.php(50): Grocy\\Controllers\\BaseApiController->filter()\n#1
+        \/var\/www\/controllers\/BaseApiController.php(42): Grocy\\Controllers\\BaseApiController->queryData()\n#2
+        \/var\/www\/controllers\/BatteriesApiController.php(25): Grocy\\Controllers\\BaseApiController->FilteredApiResponse()\n#3
+        \/var\/www\/vendor\/slim\/slim\/Slim\/Handlers\/Strategies\/RequestResponse.php(43):
+        Grocy\\Controllers\\BatteriesApiController->Current()\n#4 \/var\/www\/vendor\/slim\/slim\/Slim\/Routing\/Route.php(384):
+        Slim\\Handlers\\Strategies\\RequestResponse->__invoke()\n#5 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(81):
+        Slim\\Routing\\Route->handle()\n#6 \/var\/www\/middleware\/JsonMiddleware.php(13):
+        Slim\\MiddlewareDispatcher->handle()\n#7 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(209):
+        Grocy\\Middleware\\JsonMiddleware->__invoke()\n#8 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(81):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#9 \/var\/www\/vendor\/slim\/slim\/Slim\/Routing\/Route.php(341):
+        Slim\\MiddlewareDispatcher->handle()\n#10 \/var\/www\/vendor\/slim\/slim\/Slim\/Routing\/RouteRunner.php(84):
+        Slim\\Routing\\Route->run()\n#11 \/var\/www\/middleware\/AuthMiddleware.php(48):
+        Slim\\Routing\\RouteRunner->handle()\n#12 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(313):
+        Grocy\\Middleware\\AuthMiddleware->__invoke()\n#13 \/var\/www\/vendor\/slim\/slim\/Slim\/Middleware\/RoutingMiddleware.php(59):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#14 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(147):
+        Slim\\Middleware\\RoutingMiddleware->process()\n#15 \/var\/www\/vendor\/slim\/slim\/Slim\/Middleware\/ErrorMiddleware.php(107):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#16 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(147):
+        Slim\\Middleware\\ErrorMiddleware->process()\n#17 \/var\/www\/middleware\/CorsMiddleware.php(30):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#18 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(313):
+        Grocy\\Middleware\\CorsMiddleware->__invoke()\n#19 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(81):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#20 \/var\/www\/vendor\/slim\/slim\/Slim\/App.php(215):
+        Slim\\MiddlewareDispatcher->handle()\n#21 \/var\/www\/vendor\/slim\/slim\/Slim\/App.php(199):
+        Slim\\App->handle()\n#22 \/var\/www\/app.php(106): Slim\\App->run()\n#23 \/var\/www\/public\/index.php(45):
+        require_once(''...'')\n#24 {main}","file":"\/var\/www\/controllers\/BaseApiController.php","line":95}}'
+    headers:
+      Access-Control-Allow-Headers:
+      - '*'
+      Access-Control-Allow-Methods:
+      - GET, POST, PUT, DELETE, OPTIONS
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Type:
+      - application/json
+      Date:
+      - Wed, 06 Jul 2022 18:52:57 GMT
+      Server:
+      - nginx/1.22.0
+      Transfer-Encoding:
+      - chunked
+      X-Powered-By:
+      - PHP/8.0.20
+    status:
+      code: 500
+      message: Internal Server Error
+version: 1
diff --git a/test/cassettes/test_battery/TestBattery.test_get_batteries_filters_valid.yaml b/test/cassettes/test_battery/TestBattery.test_get_batteries_filters_valid.yaml
new file mode 100644
index 0000000..ba67fb5
--- /dev/null
+++ b/test/cassettes/test_battery/TestBattery.test_get_batteries_filters_valid.yaml
@@ -0,0 +1,44 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      User-Agent:
+      - python-requests/2.28.0
+      accept:
+      - application/json
+    method: GET
+    uri: https://localhost/api/batteries?query%5B%5D=next_estimated_charge_time%3C2022-06-20
+  response:
+    body:
+      string: '[{"id":"1","battery_id":"1","last_tracked_time":"2021-12-19 23:59:59","next_estimated_charge_time":"2022-06-17
+        23:59:59"},{"id":"3","battery_id":"3","last_tracked_time":"2022-04-13 19:33:45","next_estimated_charge_time":"2022-06-12
+        19:33:45"}]'
+    headers:
+      Access-Control-Allow-Headers:
+      - '*'
+      Access-Control-Allow-Methods:
+      - GET, POST, PUT, DELETE, OPTIONS
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Type:
+      - application/json
+      Date:
+      - Wed, 06 Jul 2022 18:52:57 GMT
+      Server:
+      - nginx/1.22.0
+      Transfer-Encoding:
+      - chunked
+      X-Powered-By:
+      - PHP/8.0.20
+      content-length:
+      - '243'
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/test/cassettes/test_chores/TestChores.test_get_chores_filters_invalid.yaml b/test/cassettes/test_chores/TestChores.test_get_chores_filters_invalid.yaml
new file mode 100644
index 0000000..ac2a764
--- /dev/null
+++ b/test/cassettes/test_chores/TestChores.test_get_chores_filters_invalid.yaml
@@ -0,0 +1,64 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      User-Agent:
+      - python-requests/2.28.0
+      accept:
+      - application/json
+    method: GET
+    uri: https://localhost/api/chores?query%5B%5D=invalid
+  response:
+    body:
+      string: '{"error_message":"Invalid query","error_details":{"stack_trace":"#0
+        \/var\/www\/controllers\/BaseApiController.php(50): Grocy\\Controllers\\BaseApiController->filter()\n#1
+        \/var\/www\/controllers\/BaseApiController.php(42): Grocy\\Controllers\\BaseApiController->queryData()\n#2
+        \/var\/www\/controllers\/ChoresApiController.php(59): Grocy\\Controllers\\BaseApiController->FilteredApiResponse()\n#3
+        \/var\/www\/vendor\/slim\/slim\/Slim\/Handlers\/Strategies\/RequestResponse.php(43):
+        Grocy\\Controllers\\ChoresApiController->Current()\n#4 \/var\/www\/vendor\/slim\/slim\/Slim\/Routing\/Route.php(384):
+        Slim\\Handlers\\Strategies\\RequestResponse->__invoke()\n#5 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(81):
+        Slim\\Routing\\Route->handle()\n#6 \/var\/www\/middleware\/JsonMiddleware.php(13):
+        Slim\\MiddlewareDispatcher->handle()\n#7 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(209):
+        Grocy\\Middleware\\JsonMiddleware->__invoke()\n#8 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(81):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#9 \/var\/www\/vendor\/slim\/slim\/Slim\/Routing\/Route.php(341):
+        Slim\\MiddlewareDispatcher->handle()\n#10 \/var\/www\/vendor\/slim\/slim\/Slim\/Routing\/RouteRunner.php(84):
+        Slim\\Routing\\Route->run()\n#11 \/var\/www\/middleware\/AuthMiddleware.php(48):
+        Slim\\Routing\\RouteRunner->handle()\n#12 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(313):
+        Grocy\\Middleware\\AuthMiddleware->__invoke()\n#13 \/var\/www\/vendor\/slim\/slim\/Slim\/Middleware\/RoutingMiddleware.php(59):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#14 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(147):
+        Slim\\Middleware\\RoutingMiddleware->process()\n#15 \/var\/www\/vendor\/slim\/slim\/Slim\/Middleware\/ErrorMiddleware.php(107):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#16 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(147):
+        Slim\\Middleware\\ErrorMiddleware->process()\n#17 \/var\/www\/middleware\/CorsMiddleware.php(30):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#18 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(313):
+        Grocy\\Middleware\\CorsMiddleware->__invoke()\n#19 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(81):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#20 \/var\/www\/vendor\/slim\/slim\/Slim\/App.php(215):
+        Slim\\MiddlewareDispatcher->handle()\n#21 \/var\/www\/vendor\/slim\/slim\/Slim\/App.php(199):
+        Slim\\App->handle()\n#22 \/var\/www\/app.php(106): Slim\\App->run()\n#23 \/var\/www\/public\/index.php(45):
+        require_once(''...'')\n#24 {main}","file":"\/var\/www\/controllers\/BaseApiController.php","line":95}}'
+    headers:
+      Access-Control-Allow-Headers:
+      - '*'
+      Access-Control-Allow-Methods:
+      - GET, POST, PUT, DELETE, OPTIONS
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Type:
+      - application/json
+      Date:
+      - Wed, 06 Jul 2022 18:52:58 GMT
+      Server:
+      - nginx/1.22.0
+      Transfer-Encoding:
+      - chunked
+      X-Powered-By:
+      - PHP/8.0.20
+    status:
+      code: 500
+      message: Internal Server Error
+version: 1
diff --git a/test/cassettes/test_chores/TestChores.test_get_chores_filters_valid.yaml b/test/cassettes/test_chores/TestChores.test_get_chores_filters_valid.yaml
new file mode 100644
index 0000000..b5f7086
--- /dev/null
+++ b/test/cassettes/test_chores/TestChores.test_get_chores_filters_valid.yaml
@@ -0,0 +1,89 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      User-Agent:
+      - python-requests/2.28.0
+      accept:
+      - application/json
+    method: GET
+    uri: https://localhost/api/chores?query%5B%5D=next_execution_assigned_to_user_id%3D1
+  response:
+    body:
+      string: '[{"id":"1","chore_id":"1","chore_name":"Change towels in the bathroom","last_tracked_time":"2022-06-17
+        00:00:00","next_estimated_execution_time":"2022-06-20 23:59:59","track_date_only":"1","next_execution_assigned_to_user_id":"1","is_rescheduled":"0","is_reassigned":"0"}]'
+    headers:
+      Access-Control-Allow-Headers:
+      - '*'
+      Access-Control-Allow-Methods:
+      - GET, POST, PUT, DELETE, OPTIONS
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Type:
+      - application/json
+      Date:
+      - Wed, 06 Jul 2022 18:52:58 GMT
+      Server:
+      - nginx/1.22.0
+      Transfer-Encoding:
+      - chunked
+      X-Powered-By:
+      - PHP/8.0.20
+      content-length:
+      - '272'
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      User-Agent:
+      - python-requests/2.28.0
+      accept:
+      - application/json
+    method: GET
+    uri: https://localhost/api/chores/1
+  response:
+    body:
+      string: '{"chore":{"id":"1","name":"Change towels in the bathroom","description":null,"period_type":"hourly","period_days":null,"row_created_timestamp":"2022-06-17
+        19:33:37","period_config":null,"track_date_only":"1","rollover":"0","assignment_type":"random","assignment_config":"1,2,3,4","next_execution_assigned_to_user_id":"1","consume_product_on_execution":"0","product_id":null,"product_amount":null,"period_interval":"72","active":"1","start_date":"2021-01-01","rescheduled_date":null,"rescheduled_next_execution_assigned_to_user_id":null},"last_tracked":"2022-06-17
+        00:00:00","tracked_count":20,"last_done_by":{"id":"4","username":"Demo User
+        4","first_name":null,"last_name":null,"row_created_timestamp":"2022-06-17
+        19:33:35","display_name":"Demo User 4","picture_file_name":null},"next_estimated_execution_time":"2022-06-20
+        23:59:59","next_execution_assigned_user":{"id":"1","username":"Demo User","first_name":null,"last_name":null,"row_created_timestamp":"2022-06-17
+        19:33:31","display_name":"Demo User","picture_file_name":null},"average_execution_frequency_hours":"81.8181818181818"}'
+    headers:
+      Access-Control-Allow-Headers:
+      - '*'
+      Access-Control-Allow-Methods:
+      - GET, POST, PUT, DELETE, OPTIONS
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Type:
+      - application/json
+      Date:
+      - Wed, 06 Jul 2022 18:52:58 GMT
+      Server:
+      - nginx/1.22.0
+      Transfer-Encoding:
+      - chunked
+      X-Powered-By:
+      - PHP/8.0.20
+      content-length:
+      - '1086'
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/test/cassettes/test_generic/TestGeneric.test_get_generic_objects_for_type_filters_invalid.yaml b/test/cassettes/test_generic/TestGeneric.test_get_generic_objects_for_type_filters_invalid.yaml
new file mode 100644
index 0000000..f495600
--- /dev/null
+++ b/test/cassettes/test_generic/TestGeneric.test_get_generic_objects_for_type_filters_invalid.yaml
@@ -0,0 +1,63 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      User-Agent:
+      - python-requests/2.28.0
+      accept:
+      - application/json
+    method: GET
+    uri: https://localhost/api/objects/shopping_locations?query%5B%5D=invalid
+  response:
+    body:
+      string: '{"error_message":"Invalid query","error_details":{"stack_trace":"#0
+        \/var\/www\/controllers\/BaseApiController.php(50): Grocy\\Controllers\\BaseApiController->filter()\n#1
+        \/var\/www\/controllers\/GenericEntityApiController.php(152): Grocy\\Controllers\\BaseApiController->queryData()\n#2
+        \/var\/www\/vendor\/slim\/slim\/Slim\/Handlers\/Strategies\/RequestResponse.php(43):
+        Grocy\\Controllers\\GenericEntityApiController->GetObjects()\n#3 \/var\/www\/vendor\/slim\/slim\/Slim\/Routing\/Route.php(384):
+        Slim\\Handlers\\Strategies\\RequestResponse->__invoke()\n#4 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(81):
+        Slim\\Routing\\Route->handle()\n#5 \/var\/www\/middleware\/JsonMiddleware.php(13):
+        Slim\\MiddlewareDispatcher->handle()\n#6 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(209):
+        Grocy\\Middleware\\JsonMiddleware->__invoke()\n#7 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(81):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#8 \/var\/www\/vendor\/slim\/slim\/Slim\/Routing\/Route.php(341):
+        Slim\\MiddlewareDispatcher->handle()\n#9 \/var\/www\/vendor\/slim\/slim\/Slim\/Routing\/RouteRunner.php(84):
+        Slim\\Routing\\Route->run()\n#10 \/var\/www\/middleware\/AuthMiddleware.php(48):
+        Slim\\Routing\\RouteRunner->handle()\n#11 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(313):
+        Grocy\\Middleware\\AuthMiddleware->__invoke()\n#12 \/var\/www\/vendor\/slim\/slim\/Slim\/Middleware\/RoutingMiddleware.php(59):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#13 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(147):
+        Slim\\Middleware\\RoutingMiddleware->process()\n#14 \/var\/www\/vendor\/slim\/slim\/Slim\/Middleware\/ErrorMiddleware.php(107):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#15 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(147):
+        Slim\\Middleware\\ErrorMiddleware->process()\n#16 \/var\/www\/middleware\/CorsMiddleware.php(30):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#17 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(313):
+        Grocy\\Middleware\\CorsMiddleware->__invoke()\n#18 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(81):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#19 \/var\/www\/vendor\/slim\/slim\/Slim\/App.php(215):
+        Slim\\MiddlewareDispatcher->handle()\n#20 \/var\/www\/vendor\/slim\/slim\/Slim\/App.php(199):
+        Slim\\App->handle()\n#21 \/var\/www\/app.php(106): Slim\\App->run()\n#22 \/var\/www\/public\/index.php(45):
+        require_once(''...'')\n#23 {main}","file":"\/var\/www\/controllers\/BaseApiController.php","line":95}}'
+    headers:
+      Access-Control-Allow-Headers:
+      - '*'
+      Access-Control-Allow-Methods:
+      - GET, POST, PUT, DELETE, OPTIONS
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Type:
+      - application/json
+      Date:
+      - Wed, 06 Jul 2022 18:52:58 GMT
+      Server:
+      - nginx/1.22.0
+      Transfer-Encoding:
+      - chunked
+      X-Powered-By:
+      - PHP/8.0.20
+    status:
+      code: 500
+      message: Internal Server Error
+version: 1
diff --git a/test/cassettes/test_generic/TestGeneric.test_get_generic_objects_for_type_filters_valid.yaml b/test/cassettes/test_generic/TestGeneric.test_get_generic_objects_for_type_filters_valid.yaml
new file mode 100644
index 0000000..f13e79a
--- /dev/null
+++ b/test/cassettes/test_generic/TestGeneric.test_get_generic_objects_for_type_filters_valid.yaml
@@ -0,0 +1,43 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      User-Agent:
+      - python-requests/2.28.0
+      accept:
+      - application/json
+    method: GET
+    uri: https://localhost/api/objects/shopping_locations?query%5B%5D=name%3DWalmart
+  response:
+    body:
+      string: '[{"id":"1","name":"Walmart","description":null,"row_created_timestamp":"2022-06-17
+        19:33:35"}]'
+    headers:
+      Access-Control-Allow-Headers:
+      - '*'
+      Access-Control-Allow-Methods:
+      - GET, POST, PUT, DELETE, OPTIONS
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Type:
+      - application/json
+      Date:
+      - Wed, 06 Jul 2022 18:52:58 GMT
+      Server:
+      - nginx/1.22.0
+      Transfer-Encoding:
+      - chunked
+      X-Powered-By:
+      - PHP/8.0.20
+      content-length:
+      - '94'
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/test/cassettes/test_meal_plan/TestMealPlan.test_get_meal_plan_filters_invalid.yaml b/test/cassettes/test_meal_plan/TestMealPlan.test_get_meal_plan_filters_invalid.yaml
new file mode 100644
index 0000000..73f59a4
--- /dev/null
+++ b/test/cassettes/test_meal_plan/TestMealPlan.test_get_meal_plan_filters_invalid.yaml
@@ -0,0 +1,63 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      User-Agent:
+      - python-requests/2.28.0
+      accept:
+      - application/json
+    method: GET
+    uri: https://localhost/api/objects/meal_plan?query%5B%5D=invalid
+  response:
+    body:
+      string: '{"error_message":"Invalid query","error_details":{"stack_trace":"#0
+        \/var\/www\/controllers\/BaseApiController.php(50): Grocy\\Controllers\\BaseApiController->filter()\n#1
+        \/var\/www\/controllers\/GenericEntityApiController.php(152): Grocy\\Controllers\\BaseApiController->queryData()\n#2
+        \/var\/www\/vendor\/slim\/slim\/Slim\/Handlers\/Strategies\/RequestResponse.php(43):
+        Grocy\\Controllers\\GenericEntityApiController->GetObjects()\n#3 \/var\/www\/vendor\/slim\/slim\/Slim\/Routing\/Route.php(384):
+        Slim\\Handlers\\Strategies\\RequestResponse->__invoke()\n#4 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(81):
+        Slim\\Routing\\Route->handle()\n#5 \/var\/www\/middleware\/JsonMiddleware.php(13):
+        Slim\\MiddlewareDispatcher->handle()\n#6 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(209):
+        Grocy\\Middleware\\JsonMiddleware->__invoke()\n#7 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(81):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#8 \/var\/www\/vendor\/slim\/slim\/Slim\/Routing\/Route.php(341):
+        Slim\\MiddlewareDispatcher->handle()\n#9 \/var\/www\/vendor\/slim\/slim\/Slim\/Routing\/RouteRunner.php(84):
+        Slim\\Routing\\Route->run()\n#10 \/var\/www\/middleware\/AuthMiddleware.php(48):
+        Slim\\Routing\\RouteRunner->handle()\n#11 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(313):
+        Grocy\\Middleware\\AuthMiddleware->__invoke()\n#12 \/var\/www\/vendor\/slim\/slim\/Slim\/Middleware\/RoutingMiddleware.php(59):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#13 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(147):
+        Slim\\Middleware\\RoutingMiddleware->process()\n#14 \/var\/www\/vendor\/slim\/slim\/Slim\/Middleware\/ErrorMiddleware.php(107):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#15 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(147):
+        Slim\\Middleware\\ErrorMiddleware->process()\n#16 \/var\/www\/middleware\/CorsMiddleware.php(30):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#17 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(313):
+        Grocy\\Middleware\\CorsMiddleware->__invoke()\n#18 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(81):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#19 \/var\/www\/vendor\/slim\/slim\/Slim\/App.php(215):
+        Slim\\MiddlewareDispatcher->handle()\n#20 \/var\/www\/vendor\/slim\/slim\/Slim\/App.php(199):
+        Slim\\App->handle()\n#21 \/var\/www\/app.php(106): Slim\\App->run()\n#22 \/var\/www\/public\/index.php(45):
+        require_once(''...'')\n#23 {main}","file":"\/var\/www\/controllers\/BaseApiController.php","line":95}}'
+    headers:
+      Access-Control-Allow-Headers:
+      - '*'
+      Access-Control-Allow-Methods:
+      - GET, POST, PUT, DELETE, OPTIONS
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Type:
+      - application/json
+      Date:
+      - Wed, 06 Jul 2022 18:52:59 GMT
+      Server:
+      - nginx/1.22.0
+      Transfer-Encoding:
+      - chunked
+      X-Powered-By:
+      - PHP/8.0.20
+    status:
+      code: 500
+      message: Internal Server Error
+version: 1
diff --git a/test/cassettes/test_meal_plan/TestMealPlan.test_get_meal_plan_filters_valid.yaml b/test/cassettes/test_meal_plan/TestMealPlan.test_get_meal_plan_filters_valid.yaml
new file mode 100644
index 0000000..061e837
--- /dev/null
+++ b/test/cassettes/test_meal_plan/TestMealPlan.test_get_meal_plan_filters_valid.yaml
@@ -0,0 +1,84 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      User-Agent:
+      - python-requests/2.28.0
+      accept:
+      - application/json
+    method: GET
+    uri: https://localhost/api/objects/meal_plan?query%5B%5D=day%3E%3D2022-06-15&query%5B%5D=product_amount%3E0
+  response:
+    body:
+      string: '[{"id":"11","day":"2022-06-15","type":"product","recipe_id":null,"recipe_servings":"1","note":null,"product_id":"25","product_amount":"1.0","product_qu_id":null,"row_created_timestamp":"2022-06-17
+        19:33:36","done":"0","section_id":"1"}]'
+    headers:
+      Access-Control-Allow-Headers:
+      - '*'
+      Access-Control-Allow-Methods:
+      - GET, POST, PUT, DELETE, OPTIONS
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Type:
+      - application/json
+      Date:
+      - Wed, 06 Jul 2022 18:52:59 GMT
+      Server:
+      - nginx/1.22.0
+      Transfer-Encoding:
+      - chunked
+      X-Powered-By:
+      - PHP/8.0.20
+      content-length:
+      - '236'
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      User-Agent:
+      - python-requests/2.28.0
+      accept:
+      - application/json
+    method: GET
+    uri: https://localhost/api/objects/meal_plan_sections?query%5B%5D=id%3D1
+  response:
+    body:
+      string: '[{"id":"1","name":"Breakfast","sort_number":"10","row_created_timestamp":"2022-06-17
+        19:33:36","time_info":null}]'
+    headers:
+      Access-Control-Allow-Headers:
+      - '*'
+      Access-Control-Allow-Methods:
+      - GET, POST, PUT, DELETE, OPTIONS
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Type:
+      - application/json
+      Date:
+      - Wed, 06 Jul 2022 18:52:59 GMT
+      Server:
+      - nginx/1.22.0
+      Transfer-Encoding:
+      - chunked
+      X-Powered-By:
+      - PHP/8.0.20
+      content-length:
+      - '113'
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/test/cassettes/test_meal_plan_sections/TestMealPlanSections.test_get_sections_filters_invalid.yaml b/test/cassettes/test_meal_plan_sections/TestMealPlanSections.test_get_sections_filters_invalid.yaml
new file mode 100644
index 0000000..e39074b
--- /dev/null
+++ b/test/cassettes/test_meal_plan_sections/TestMealPlanSections.test_get_sections_filters_invalid.yaml
@@ -0,0 +1,63 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      User-Agent:
+      - python-requests/2.28.0
+      accept:
+      - application/json
+    method: GET
+    uri: https://localhost/api/objects/meal_plan_sections?query%5B%5D=invalid
+  response:
+    body:
+      string: '{"error_message":"Invalid query","error_details":{"stack_trace":"#0
+        \/var\/www\/controllers\/BaseApiController.php(50): Grocy\\Controllers\\BaseApiController->filter()\n#1
+        \/var\/www\/controllers\/GenericEntityApiController.php(152): Grocy\\Controllers\\BaseApiController->queryData()\n#2
+        \/var\/www\/vendor\/slim\/slim\/Slim\/Handlers\/Strategies\/RequestResponse.php(43):
+        Grocy\\Controllers\\GenericEntityApiController->GetObjects()\n#3 \/var\/www\/vendor\/slim\/slim\/Slim\/Routing\/Route.php(384):
+        Slim\\Handlers\\Strategies\\RequestResponse->__invoke()\n#4 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(81):
+        Slim\\Routing\\Route->handle()\n#5 \/var\/www\/middleware\/JsonMiddleware.php(13):
+        Slim\\MiddlewareDispatcher->handle()\n#6 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(209):
+        Grocy\\Middleware\\JsonMiddleware->__invoke()\n#7 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(81):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#8 \/var\/www\/vendor\/slim\/slim\/Slim\/Routing\/Route.php(341):
+        Slim\\MiddlewareDispatcher->handle()\n#9 \/var\/www\/vendor\/slim\/slim\/Slim\/Routing\/RouteRunner.php(84):
+        Slim\\Routing\\Route->run()\n#10 \/var\/www\/middleware\/AuthMiddleware.php(48):
+        Slim\\Routing\\RouteRunner->handle()\n#11 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(313):
+        Grocy\\Middleware\\AuthMiddleware->__invoke()\n#12 \/var\/www\/vendor\/slim\/slim\/Slim\/Middleware\/RoutingMiddleware.php(59):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#13 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(147):
+        Slim\\Middleware\\RoutingMiddleware->process()\n#14 \/var\/www\/vendor\/slim\/slim\/Slim\/Middleware\/ErrorMiddleware.php(107):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#15 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(147):
+        Slim\\Middleware\\ErrorMiddleware->process()\n#16 \/var\/www\/middleware\/CorsMiddleware.php(30):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#17 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(313):
+        Grocy\\Middleware\\CorsMiddleware->__invoke()\n#18 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(81):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#19 \/var\/www\/vendor\/slim\/slim\/Slim\/App.php(215):
+        Slim\\MiddlewareDispatcher->handle()\n#20 \/var\/www\/vendor\/slim\/slim\/Slim\/App.php(199):
+        Slim\\App->handle()\n#21 \/var\/www\/app.php(106): Slim\\App->run()\n#22 \/var\/www\/public\/index.php(45):
+        require_once(''...'')\n#23 {main}","file":"\/var\/www\/controllers\/BaseApiController.php","line":95}}'
+    headers:
+      Access-Control-Allow-Headers:
+      - '*'
+      Access-Control-Allow-Methods:
+      - GET, POST, PUT, DELETE, OPTIONS
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Type:
+      - application/json
+      Date:
+      - Wed, 06 Jul 2022 18:52:59 GMT
+      Server:
+      - nginx/1.22.0
+      Transfer-Encoding:
+      - chunked
+      X-Powered-By:
+      - PHP/8.0.20
+    status:
+      code: 500
+      message: Internal Server Error
+version: 1
diff --git a/test/cassettes/test_meal_plan_sections/TestMealPlanSections.test_get_sections_filters_valid.yaml b/test/cassettes/test_meal_plan_sections/TestMealPlanSections.test_get_sections_filters_valid.yaml
new file mode 100644
index 0000000..054ce9b
--- /dev/null
+++ b/test/cassettes/test_meal_plan_sections/TestMealPlanSections.test_get_sections_filters_valid.yaml
@@ -0,0 +1,43 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      User-Agent:
+      - python-requests/2.28.0
+      accept:
+      - application/json
+    method: GET
+    uri: https://localhost/api/objects/meal_plan_sections?query%5B%5D=name%3DBreakfast
+  response:
+    body:
+      string: '[{"id":"1","name":"Breakfast","sort_number":"10","row_created_timestamp":"2022-06-17
+        19:33:36","time_info":null}]'
+    headers:
+      Access-Control-Allow-Headers:
+      - '*'
+      Access-Control-Allow-Methods:
+      - GET, POST, PUT, DELETE, OPTIONS
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Type:
+      - application/json
+      Date:
+      - Wed, 06 Jul 2022 18:52:59 GMT
+      Server:
+      - nginx/1.22.0
+      Transfer-Encoding:
+      - chunked
+      X-Powered-By:
+      - PHP/8.0.20
+      content-length:
+      - '113'
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/test/cassettes/test_product_groups/TestProductGroups.test_get_product_groups_filters_invalid.yaml b/test/cassettes/test_product_groups/TestProductGroups.test_get_product_groups_filters_invalid.yaml
new file mode 100644
index 0000000..98e3593
--- /dev/null
+++ b/test/cassettes/test_product_groups/TestProductGroups.test_get_product_groups_filters_invalid.yaml
@@ -0,0 +1,63 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      User-Agent:
+      - python-requests/2.28.0
+      accept:
+      - application/json
+    method: GET
+    uri: https://localhost/api/objects/product_groups?query%5B%5D=invalid
+  response:
+    body:
+      string: '{"error_message":"Invalid query","error_details":{"stack_trace":"#0
+        \/var\/www\/controllers\/BaseApiController.php(50): Grocy\\Controllers\\BaseApiController->filter()\n#1
+        \/var\/www\/controllers\/GenericEntityApiController.php(152): Grocy\\Controllers\\BaseApiController->queryData()\n#2
+        \/var\/www\/vendor\/slim\/slim\/Slim\/Handlers\/Strategies\/RequestResponse.php(43):
+        Grocy\\Controllers\\GenericEntityApiController->GetObjects()\n#3 \/var\/www\/vendor\/slim\/slim\/Slim\/Routing\/Route.php(384):
+        Slim\\Handlers\\Strategies\\RequestResponse->__invoke()\n#4 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(81):
+        Slim\\Routing\\Route->handle()\n#5 \/var\/www\/middleware\/JsonMiddleware.php(13):
+        Slim\\MiddlewareDispatcher->handle()\n#6 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(209):
+        Grocy\\Middleware\\JsonMiddleware->__invoke()\n#7 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(81):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#8 \/var\/www\/vendor\/slim\/slim\/Slim\/Routing\/Route.php(341):
+        Slim\\MiddlewareDispatcher->handle()\n#9 \/var\/www\/vendor\/slim\/slim\/Slim\/Routing\/RouteRunner.php(84):
+        Slim\\Routing\\Route->run()\n#10 \/var\/www\/middleware\/AuthMiddleware.php(48):
+        Slim\\Routing\\RouteRunner->handle()\n#11 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(313):
+        Grocy\\Middleware\\AuthMiddleware->__invoke()\n#12 \/var\/www\/vendor\/slim\/slim\/Slim\/Middleware\/RoutingMiddleware.php(59):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#13 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(147):
+        Slim\\Middleware\\RoutingMiddleware->process()\n#14 \/var\/www\/vendor\/slim\/slim\/Slim\/Middleware\/ErrorMiddleware.php(107):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#15 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(147):
+        Slim\\Middleware\\ErrorMiddleware->process()\n#16 \/var\/www\/middleware\/CorsMiddleware.php(30):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#17 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(313):
+        Grocy\\Middleware\\CorsMiddleware->__invoke()\n#18 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(81):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#19 \/var\/www\/vendor\/slim\/slim\/Slim\/App.php(215):
+        Slim\\MiddlewareDispatcher->handle()\n#20 \/var\/www\/vendor\/slim\/slim\/Slim\/App.php(199):
+        Slim\\App->handle()\n#21 \/var\/www\/app.php(106): Slim\\App->run()\n#22 \/var\/www\/public\/index.php(45):
+        require_once(''...'')\n#23 {main}","file":"\/var\/www\/controllers\/BaseApiController.php","line":95}}'
+    headers:
+      Access-Control-Allow-Headers:
+      - '*'
+      Access-Control-Allow-Methods:
+      - GET, POST, PUT, DELETE, OPTIONS
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Type:
+      - application/json
+      Date:
+      - Wed, 06 Jul 2022 18:52:59 GMT
+      Server:
+      - nginx/1.22.0
+      Transfer-Encoding:
+      - chunked
+      X-Powered-By:
+      - PHP/8.0.20
+    status:
+      code: 500
+      message: Internal Server Error
+version: 1
diff --git a/test/cassettes/test_product_groups/TestProductGroups.test_get_product_groups_filters_valid.yaml b/test/cassettes/test_product_groups/TestProductGroups.test_get_product_groups_filters_valid.yaml
new file mode 100644
index 0000000..b290586
--- /dev/null
+++ b/test/cassettes/test_product_groups/TestProductGroups.test_get_product_groups_filters_valid.yaml
@@ -0,0 +1,43 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      User-Agent:
+      - python-requests/2.28.0
+      accept:
+      - application/json
+    method: GET
+    uri: https://localhost/api/objects/product_groups?query%5B%5D=id%3D6
+  response:
+    body:
+      string: '[{"id":"6","name":"06 Refrigerated products","description":null,"row_created_timestamp":"2022-06-17
+        19:33:35"}]'
+    headers:
+      Access-Control-Allow-Headers:
+      - '*'
+      Access-Control-Allow-Methods:
+      - GET, POST, PUT, DELETE, OPTIONS
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Type:
+      - application/json
+      Date:
+      - Wed, 06 Jul 2022 18:52:59 GMT
+      Server:
+      - nginx/1.22.0
+      Transfer-Encoding:
+      - chunked
+      X-Powered-By:
+      - PHP/8.0.20
+      content-length:
+      - '111'
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/test/cassettes/test_shoppinglist/TestShoppingList.test_get_shopping_list_filters_invalid.yaml b/test/cassettes/test_shoppinglist/TestShoppingList.test_get_shopping_list_filters_invalid.yaml
new file mode 100644
index 0000000..871aa2f
--- /dev/null
+++ b/test/cassettes/test_shoppinglist/TestShoppingList.test_get_shopping_list_filters_invalid.yaml
@@ -0,0 +1,63 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      User-Agent:
+      - python-requests/2.28.0
+      accept:
+      - application/json
+    method: GET
+    uri: https://localhost/api/objects/shopping_list?query%5B%5D=invalid
+  response:
+    body:
+      string: '{"error_message":"Invalid query","error_details":{"stack_trace":"#0
+        \/var\/www\/controllers\/BaseApiController.php(50): Grocy\\Controllers\\BaseApiController->filter()\n#1
+        \/var\/www\/controllers\/GenericEntityApiController.php(152): Grocy\\Controllers\\BaseApiController->queryData()\n#2
+        \/var\/www\/vendor\/slim\/slim\/Slim\/Handlers\/Strategies\/RequestResponse.php(43):
+        Grocy\\Controllers\\GenericEntityApiController->GetObjects()\n#3 \/var\/www\/vendor\/slim\/slim\/Slim\/Routing\/Route.php(384):
+        Slim\\Handlers\\Strategies\\RequestResponse->__invoke()\n#4 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(81):
+        Slim\\Routing\\Route->handle()\n#5 \/var\/www\/middleware\/JsonMiddleware.php(13):
+        Slim\\MiddlewareDispatcher->handle()\n#6 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(209):
+        Grocy\\Middleware\\JsonMiddleware->__invoke()\n#7 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(81):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#8 \/var\/www\/vendor\/slim\/slim\/Slim\/Routing\/Route.php(341):
+        Slim\\MiddlewareDispatcher->handle()\n#9 \/var\/www\/vendor\/slim\/slim\/Slim\/Routing\/RouteRunner.php(84):
+        Slim\\Routing\\Route->run()\n#10 \/var\/www\/middleware\/AuthMiddleware.php(48):
+        Slim\\Routing\\RouteRunner->handle()\n#11 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(313):
+        Grocy\\Middleware\\AuthMiddleware->__invoke()\n#12 \/var\/www\/vendor\/slim\/slim\/Slim\/Middleware\/RoutingMiddleware.php(59):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#13 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(147):
+        Slim\\Middleware\\RoutingMiddleware->process()\n#14 \/var\/www\/vendor\/slim\/slim\/Slim\/Middleware\/ErrorMiddleware.php(107):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#15 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(147):
+        Slim\\Middleware\\ErrorMiddleware->process()\n#16 \/var\/www\/middleware\/CorsMiddleware.php(30):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#17 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(313):
+        Grocy\\Middleware\\CorsMiddleware->__invoke()\n#18 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(81):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#19 \/var\/www\/vendor\/slim\/slim\/Slim\/App.php(215):
+        Slim\\MiddlewareDispatcher->handle()\n#20 \/var\/www\/vendor\/slim\/slim\/Slim\/App.php(199):
+        Slim\\App->handle()\n#21 \/var\/www\/app.php(106): Slim\\App->run()\n#22 \/var\/www\/public\/index.php(45):
+        require_once(''...'')\n#23 {main}","file":"\/var\/www\/controllers\/BaseApiController.php","line":95}}'
+    headers:
+      Access-Control-Allow-Headers:
+      - '*'
+      Access-Control-Allow-Methods:
+      - GET, POST, PUT, DELETE, OPTIONS
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Type:
+      - application/json
+      Date:
+      - Wed, 06 Jul 2022 18:52:59 GMT
+      Server:
+      - nginx/1.22.0
+      Transfer-Encoding:
+      - chunked
+      X-Powered-By:
+      - PHP/8.0.20
+    status:
+      code: 500
+      message: Internal Server Error
+version: 1
diff --git a/test/cassettes/test_shoppinglist/TestShoppingList.test_get_shopping_list_filters_valid.yaml b/test/cassettes/test_shoppinglist/TestShoppingList.test_get_shopping_list_filters_valid.yaml
new file mode 100644
index 0000000..7757ca9
--- /dev/null
+++ b/test/cassettes/test_shoppinglist/TestShoppingList.test_get_shopping_list_filters_valid.yaml
@@ -0,0 +1,43 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      User-Agent:
+      - python-requests/2.28.0
+      accept:
+      - application/json
+    method: GET
+    uri: https://localhost/api/objects/shopping_list?query%5B%5D=note~snacks
+  response:
+    body:
+      string: '[{"id":"1","product_id":null,"note":"Some good snacks","amount":"1","row_created_timestamp":"2022-06-17
+        19:33:36","shopping_list_id":"1","done":"0","qu_id":null}]'
+    headers:
+      Access-Control-Allow-Headers:
+      - '*'
+      Access-Control-Allow-Methods:
+      - GET, POST, PUT, DELETE, OPTIONS
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Type:
+      - application/json
+      Date:
+      - Wed, 06 Jul 2022 18:52:59 GMT
+      Server:
+      - nginx/1.22.0
+      Transfer-Encoding:
+      - chunked
+      X-Powered-By:
+      - PHP/8.0.20
+      content-length:
+      - '162'
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/test/cassettes/test_tasks/TestTasks.test_get_tasks_filters_invalid.yaml b/test/cassettes/test_tasks/TestTasks.test_get_tasks_filters_invalid.yaml
new file mode 100644
index 0000000..aaa38f4
--- /dev/null
+++ b/test/cassettes/test_tasks/TestTasks.test_get_tasks_filters_invalid.yaml
@@ -0,0 +1,64 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      User-Agent:
+      - python-requests/2.28.0
+      accept:
+      - application/json
+    method: GET
+    uri: https://localhost/api/tasks?query%5B%5D=invalid
+  response:
+    body:
+      string: '{"error_message":"Invalid query","error_details":{"stack_trace":"#0
+        \/var\/www\/controllers\/BaseApiController.php(50): Grocy\\Controllers\\BaseApiController->filter()\n#1
+        \/var\/www\/controllers\/BaseApiController.php(42): Grocy\\Controllers\\BaseApiController->queryData()\n#2
+        \/var\/www\/controllers\/TasksApiController.php(11): Grocy\\Controllers\\BaseApiController->FilteredApiResponse()\n#3
+        \/var\/www\/vendor\/slim\/slim\/Slim\/Handlers\/Strategies\/RequestResponse.php(43):
+        Grocy\\Controllers\\TasksApiController->Current()\n#4 \/var\/www\/vendor\/slim\/slim\/Slim\/Routing\/Route.php(384):
+        Slim\\Handlers\\Strategies\\RequestResponse->__invoke()\n#5 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(81):
+        Slim\\Routing\\Route->handle()\n#6 \/var\/www\/middleware\/JsonMiddleware.php(13):
+        Slim\\MiddlewareDispatcher->handle()\n#7 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(209):
+        Grocy\\Middleware\\JsonMiddleware->__invoke()\n#8 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(81):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#9 \/var\/www\/vendor\/slim\/slim\/Slim\/Routing\/Route.php(341):
+        Slim\\MiddlewareDispatcher->handle()\n#10 \/var\/www\/vendor\/slim\/slim\/Slim\/Routing\/RouteRunner.php(84):
+        Slim\\Routing\\Route->run()\n#11 \/var\/www\/middleware\/AuthMiddleware.php(48):
+        Slim\\Routing\\RouteRunner->handle()\n#12 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(313):
+        Grocy\\Middleware\\AuthMiddleware->__invoke()\n#13 \/var\/www\/vendor\/slim\/slim\/Slim\/Middleware\/RoutingMiddleware.php(59):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#14 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(147):
+        Slim\\Middleware\\RoutingMiddleware->process()\n#15 \/var\/www\/vendor\/slim\/slim\/Slim\/Middleware\/ErrorMiddleware.php(107):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#16 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(147):
+        Slim\\Middleware\\ErrorMiddleware->process()\n#17 \/var\/www\/middleware\/CorsMiddleware.php(30):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#18 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(313):
+        Grocy\\Middleware\\CorsMiddleware->__invoke()\n#19 \/var\/www\/vendor\/slim\/slim\/Slim\/MiddlewareDispatcher.php(81):
+        Psr\\Http\\Server\\RequestHandlerInterface@anonymous->handle()\n#20 \/var\/www\/vendor\/slim\/slim\/Slim\/App.php(215):
+        Slim\\MiddlewareDispatcher->handle()\n#21 \/var\/www\/vendor\/slim\/slim\/Slim\/App.php(199):
+        Slim\\App->handle()\n#22 \/var\/www\/app.php(106): Slim\\App->run()\n#23 \/var\/www\/public\/index.php(45):
+        require_once(''...'')\n#24 {main}","file":"\/var\/www\/controllers\/BaseApiController.php","line":95}}'
+    headers:
+      Access-Control-Allow-Headers:
+      - '*'
+      Access-Control-Allow-Methods:
+      - GET, POST, PUT, DELETE, OPTIONS
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Type:
+      - application/json
+      Date:
+      - Wed, 06 Jul 2022 18:52:59 GMT
+      Server:
+      - nginx/1.22.0
+      Transfer-Encoding:
+      - chunked
+      X-Powered-By:
+      - PHP/8.0.20
+    status:
+      code: 500
+      message: Internal Server Error
+version: 1
diff --git a/test/cassettes/test_tasks/TestTasks.test_get_tasks_filters_valid.yaml b/test/cassettes/test_tasks/TestTasks.test_get_tasks_filters_valid.yaml
new file mode 100644
index 0000000..769393a
--- /dev/null
+++ b/test/cassettes/test_tasks/TestTasks.test_get_tasks_filters_valid.yaml
@@ -0,0 +1,44 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      User-Agent:
+      - python-requests/2.28.0
+      accept:
+      - application/json
+    method: GET
+    uri: https://localhost/api/tasks?query%5B%5D=category_id%3D1
+  response:
+    body:
+      string: '[{"id":"2","name":"Task2","description":null,"due_date":"2022-06-16","done":"0","done_timestamp":null,"category_id":"1","assigned_to_user_id":"1","row_created_timestamp":"2022-06-17
+        19:33:37"},{"id":"3","name":"Task3","description":null,"due_date":"2022-06-17","done":"0","done_timestamp":null,"category_id":"1","assigned_to_user_id":"1","row_created_timestamp":"2022-06-17
+        19:33:37"}]'
+    headers:
+      Access-Control-Allow-Headers:
+      - '*'
+      Access-Control-Allow-Methods:
+      - GET, POST, PUT, DELETE, OPTIONS
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Type:
+      - application/json
+      Date:
+      - Wed, 06 Jul 2022 18:52:59 GMT
+      Server:
+      - nginx/1.22.0
+      Transfer-Encoding:
+      - chunked
+      X-Powered-By:
+      - PHP/8.0.20
+      content-length:
+      - '385'
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/test/conftest.py b/test/conftest.py
index be22a9f..6cfcb07 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -1,4 +1,5 @@
 from test.test_const import CONST_BASE_URL, CONST_PORT, CONST_SSL
+from typing import List
 
 import pytest
 
@@ -18,4 +19,9 @@ def grocy_api_client(grocy):
 
 @pytest.fixture
 def vcr_config():
-    return {"record_mode": "once"}
+    yield {"record_mode": "once", "decode_compressed_response": True}
+
+
+@pytest.fixture
+def invalid_query_filter() -> List[str]:
+    yield ["invalid"]
diff --git a/test/test_battery.py b/test/test_battery.py
index c4fd365..48c42a8 100644
--- a/test/test_battery.py
+++ b/test/test_battery.py
@@ -2,6 +2,8 @@ from datetime import datetime
 
 import pytest
 
+from pygrocy.errors import GrocyError
+
 
 class TestBattery:
     @pytest.mark.vcr
@@ -29,3 +31,19 @@ class TestBattery:
     @pytest.mark.vcr
     def test_charge_battery(self, grocy):
         assert grocy.charge_battery(1)
+
+    @pytest.mark.vcr
+    def test_get_batteries_filters_valid(self, grocy):
+        query_filter = ["next_estimated_charge_time<2022-06-20"]
+        batteries = grocy.batteries(query_filters=query_filter)
+
+        for item in batteries:
+            assert item.next_estimated_charge_time < datetime(2022, 6, 20)
+
+    @pytest.mark.vcr
+    def test_get_batteries_filters_invalid(self, grocy, invalid_query_filter):
+        with pytest.raises(GrocyError) as exc_info:
+            grocy.batteries(query_filters=invalid_query_filter)
+
+        error = exc_info.value
+        assert error.status_code == 500
diff --git a/test/test_chores.py b/test/test_chores.py
index da98736..27be7ad 100644
--- a/test/test_chores.py
+++ b/test/test_chores.py
@@ -4,7 +4,7 @@ import pytest
 
 from pygrocy.data_models.chore import AssignmentType, Chore, PeriodType
 from pygrocy.data_models.user import User
-from pygrocy.errors.grocy_error import GrocyError
+from pygrocy.errors import GrocyError
 
 
 class TestChores:
@@ -63,3 +63,19 @@ class TestChores:
 
         error = exc_info.value
         assert error.status_code == 400
+
+    @pytest.mark.vcr
+    def test_get_chores_filters_valid(self, grocy):
+        query_filter = ["next_execution_assigned_to_user_id=1"]
+        chores = grocy.chores(get_details=True, query_filters=query_filter)
+
+        for item in chores:
+            assert item.next_execution_assigned_to_user_id == 1
+
+    @pytest.mark.vcr
+    def test_get_chores_filters_invalid(self, grocy, invalid_query_filter):
+        with pytest.raises(GrocyError) as exc_info:
+            grocy.chores(get_details=True, query_filters=invalid_query_filter)
+
+        error = exc_info.value
+        assert error.status_code == 500
diff --git a/test/test_generic.py b/test/test_generic.py
index 7c47b24..2243c9e 100644
--- a/test/test_generic.py
+++ b/test/test_generic.py
@@ -1,7 +1,7 @@
 import pytest
 
 from pygrocy.data_models.generic import EntityType
-from pygrocy.errors.grocy_error import GrocyError
+from pygrocy.errors import GrocyError
 
 
 class TestGeneric:
@@ -58,3 +58,24 @@ class TestGeneric:
 
         error = exc_info.value
         assert error.status_code == 404
+
+    @pytest.mark.vcr
+    def test_get_generic_objects_for_type_filters_valid(self, grocy):
+        query_filter = ["name=Walmart"]
+        shopping_locations = grocy.get_generic_objects_for_type(
+            EntityType.SHOPPING_LOCATIONS, query_filters=query_filter
+        )
+
+        assert len(shopping_locations) == 1
+
+    @pytest.mark.vcr
+    def test_get_generic_objects_for_type_filters_invalid(
+        self, grocy, invalid_query_filter
+    ):
+        with pytest.raises(GrocyError) as exc_info:
+            grocy.get_generic_objects_for_type(
+                EntityType.SHOPPING_LOCATIONS, query_filters=invalid_query_filter
+            )
+
+        error = exc_info.value
+        assert error.status_code == 500
diff --git a/test/test_meal_plan.py b/test/test_meal_plan.py
index b294447..ca45b53 100644
--- a/test/test_meal_plan.py
+++ b/test/test_meal_plan.py
@@ -1,6 +1,9 @@
+import datetime
+
 import pytest
 
 from pygrocy.data_models.meal_items import MealPlanItemType, MealPlanSection, RecipeItem
+from pygrocy.errors import GrocyError
 
 
 class TestMealPlan:
@@ -56,3 +59,19 @@ class TestMealPlan:
             item for item in meal_plan if item.type == MealPlanItemType.PRODUCT
         )
         assert product_entry.product_id == 3
+
+    @pytest.mark.vcr
+    def test_get_meal_plan_filters_valid(self, grocy):
+        query_filter = ["day>=2022-06-15", "product_amount>0"]
+        meal_plans = grocy.meal_plan(get_details=True, query_filters=query_filter)
+
+        for item in meal_plans:
+            assert item.day >= datetime.date(2022, 6, 15)
+
+    @pytest.mark.vcr
+    def test_get_meal_plan_filters_invalid(self, grocy, invalid_query_filter):
+        with pytest.raises(GrocyError) as exc_info:
+            grocy.meal_plan(get_details=True, query_filters=invalid_query_filter)
+
+        error = exc_info.value
+        assert error.status_code == 500
diff --git a/test/test_meal_plan_sections.py b/test/test_meal_plan_sections.py
index 29e763b..0c7d85a 100644
--- a/test/test_meal_plan_sections.py
+++ b/test/test_meal_plan_sections.py
@@ -2,6 +2,8 @@ import datetime
 
 import pytest
 
+from pygrocy.errors import GrocyError
+
 
 class TestMealPlanSections:
     @pytest.mark.vcr
@@ -25,3 +27,19 @@ class TestMealPlanSections:
     def test_get_section_by_id_invalid(self, grocy):
         section = grocy.meal_plan_section(1000)
         assert section is None
+
+    @pytest.mark.vcr
+    def test_get_sections_filters_valid(self, grocy):
+        query_filter = ["name=Breakfast"]
+        sections = grocy.meal_plan_sections(query_filters=query_filter)
+
+        for item in sections:
+            assert item.name == "Breakfast"
+
+    @pytest.mark.vcr
+    def test_get_sections_filters_invalid(self, grocy, invalid_query_filter):
+        with pytest.raises(GrocyError) as exc_info:
+            grocy.meal_plan_sections(query_filters=invalid_query_filter)
+
+        error = exc_info.value
+        assert error.status_code == 500
diff --git a/test/test_product_groups.py b/test/test_product_groups.py
index 846a5c1..7e06818 100644
--- a/test/test_product_groups.py
+++ b/test/test_product_groups.py
@@ -1,6 +1,7 @@
 import pytest
 
 from pygrocy.data_models.product import Group
+from pygrocy.errors import GrocyError
 
 
 class TestProductGroups:
@@ -20,3 +21,19 @@ class TestProductGroups:
         group = next(group for group in product_groups_list if group.id == 1)
         assert group.name == "01 Sweets"
         assert group.description is None
+
+    @pytest.mark.vcr
+    def test_get_product_groups_filters_valid(self, grocy):
+        query_filter = ["id=6"]
+        product_groups = grocy.product_groups(query_filters=query_filter)
+
+        for item in product_groups:
+            assert item.id == 6
+
+    @pytest.mark.vcr
+    def test_get_product_groups_filters_invalid(self, grocy, invalid_query_filter):
+        with pytest.raises(GrocyError) as exc_info:
+            grocy.product_groups(query_filters=invalid_query_filter)
+
+        error = exc_info.value
+        assert error.status_code == 500
diff --git a/test/test_shoppinglist.py b/test/test_shoppinglist.py
index faabe2e..2cc40c6 100644
--- a/test/test_shoppinglist.py
+++ b/test/test_shoppinglist.py
@@ -60,3 +60,21 @@ class TestShoppingList:
     @pytest.mark.vcr
     def test_remove_product_in_shopping_list_valid(self, grocy):
         grocy.remove_product_in_shopping_list(20)
+
+    @pytest.mark.vcr
+    def test_get_shopping_list_filters_valid(self, grocy):
+        query_filter = ["note~snacks"]
+        shopping_list = grocy.shopping_list(
+            get_details=True, query_filters=query_filter
+        )
+
+        for item in shopping_list:
+            assert "snacks" in item.note
+
+    @pytest.mark.vcr
+    def test_get_shopping_list_filters_invalid(self, grocy, invalid_query_filter):
+        with pytest.raises(GrocyError) as exc_info:
+            grocy.shopping_list(get_details=True, query_filters=invalid_query_filter)
+
+        error = exc_info.value
+        assert error.status_code == 500
diff --git a/test/test_tasks.py b/test/test_tasks.py
index 0dd149b..a0e80ca 100644
--- a/test/test_tasks.py
+++ b/test/test_tasks.py
@@ -47,3 +47,19 @@ class TestTasks:
 
         error = exc_info.value
         assert error.status_code == 400
+
+    @pytest.mark.vcr
+    def test_get_tasks_filters_valid(self, grocy):
+        query_filter = ["category_id=1"]
+        tasks = grocy.tasks(query_filters=query_filter)
+
+        for item in tasks:
+            assert item.category_id == 1
+
+    @pytest.mark.vcr
+    def test_get_tasks_filters_invalid(self, grocy, invalid_query_filter):
+        with pytest.raises(GrocyError) as exc_info:
+            grocy.tasks(query_filters=invalid_query_filter)
+
+        error = exc_info.value
+        assert error.status_code == 500