diff --git a/README.md b/README.md index b61881e..8d420ce 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ `pip install pygrocy` ## Usage -Import the package: +Import the package: ```python from pygrocy import Grocy ``` @@ -38,4 +38,4 @@ for entry in grocy.stock(): If you need help using pygrocy check the [discussions](https://github.com/SebRut/pygrocy/issues) section. Feel free to create an issue for feature requests, bugs and errors in the library. ## Development testing -You need tox and Python 3.6/8/9 to run the tests. Navigate to the root dir of `pygrocy` and execute `tox` to run the tests. +You need tox and Python 3.8/9/10 to run the tests. Navigate to the root dir of `pygrocy` and execute `tox` to run the tests. diff --git a/pygrocy/grocy_api_client.py b/pygrocy/grocy_api_client.py index 5a171c8..012a2d4 100644 --- a/pygrocy/grocy_api_client.py +++ b/pygrocy/grocy_api_client.py @@ -3,11 +3,11 @@ import logging from datetime import datetime from enum import Enum -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Any from urllib.parse import urljoin import requests -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, validator from pydantic.schema import date from pygrocy import EntityType @@ -21,6 +21,17 @@ _LOGGER.setLevel(logging.INFO) +def _field_not_empty_validator(field_name: str): + """Reusable Pydantic field pre-validator to convert empty str to None.""" + return validator(field_name, allow_reuse=True, pre=True)(_none_if_empty_str) + + +def _none_if_empty_str(value: Any): + if isinstance(value, str) and value == "": + return None + return value + + class ShoppingListItem(BaseModel): id: int product_id: Optional[int] = None @@ -76,7 +87,7 @@ class ProductData(BaseModel): id: int name: str description: Optional[str] = None - location_id: int + location_id: Optional[int] = None product_group_id: Optional[int] = None qu_id_stock: int qu_id_purchase: int @@ -87,6 +98,9 @@ class ProductData(BaseModel): min_stock_amount: Optional[float] default_best_before_days: int + location_id_validator = _field_not_empty_validator("location_id") + product_group_id_validator = _field_not_empty_validator("product_group_id") + class ChoreData(BaseModel): id: int @@ -102,6 +116,10 @@ class ChoreData(BaseModel): next_execution_assigned_to_user_id: Optional[int] = None userfields: Optional[Dict] + next_execution_assigned_to_user_id_validator = _field_not_empty_validator( + "next_execution_assigned_to_user_id" + ) + class UserDto(BaseModel): id: int @@ -113,8 +131,8 @@ class UserDto(BaseModel): class CurrentChoreResponse(BaseModel): chore_id: int - last_tracked_time: Optional[datetime] - next_estimated_execution_time: datetime + last_tracked_time: Optional[datetime] = None + next_estimated_execution_time: Optional[datetime] = None class CurrentStockResponse(BaseModel): @@ -160,7 +178,7 @@ class ProductDetailsResponse(BaseModel): class ChoreDetailsResponse(BaseModel): chore: ChoreData last_tracked: Optional[datetime] = None - next_estimated_execution_time: datetime + next_estimated_execution_time: Optional[datetime] = None track_count: int = 0 next_execution_assigned_user: Optional[UserDto] = None last_done_by: Optional[UserDto] = None @@ -193,11 +211,14 @@ class TaskResponse(BaseModel): assigned_to_user: Optional[UserDto] = None userfields: Optional[Dict] = None + category_id_validator = _field_not_empty_validator("category_id") + assigned_to_user_id_validator = _field_not_empty_validator("assigned_to_user_id") + class CurrentBatteryResponse(BaseModel): id: int last_tracked_time: Optional[datetime] = None - next_estimated_charge_time: datetime + next_estimated_charge_time: Optional[datetime] = None class BatteryData(BaseModel): @@ -214,7 +235,7 @@ class BatteryDetailsResponse(BaseModel): battery: BatteryData charge_cycles_count: int last_charged: Optional[datetime] = None - next_estimated_charge_time: datetime + next_estimated_charge_time: Optional[datetime] = None class MealPlanSectionResponse(BaseModel): @@ -223,6 +244,8 @@ class MealPlanSectionResponse(BaseModel): sort_number: Optional[int] = None row_created_timestamp: datetime + sort_number_validator = _field_not_empty_validator("sort_number") + class StockLogResponse(BaseModel): id: int @@ -620,7 +643,7 @@ def get_recipe(self, object_id: int) -> RecipeDetailsResponse: return RecipeDetailsResponse(**parsed_json) def get_batteries(self) -> List[CurrentBatteryResponse]: - parsed_json = self._do_get_request(f"batteries") + parsed_json = self._do_get_request("batteries") if parsed_json: return [CurrentBatteryResponse(**data) for data in parsed_json] diff --git a/test/test_product.py b/test/test_product.py index 86e5446..8619b1c 100644 --- a/test/test_product.py +++ b/test/test_product.py @@ -26,7 +26,7 @@ def test_product_get_details_valid(self, grocy): assert product.qu_factor_purchase_to_stock == 1.0 assert product.default_quantity_unit_purchase.id == 5 assert product.default_quantity_unit_purchase.name == "Tin" - assert product.default_quantity_unit_purchase.description == None + assert product.default_quantity_unit_purchase.description is None assert product.default_quantity_unit_purchase.name_plural == "Tins" assert len(product.product_barcodes) == 2