Spaces:
Running
Running
""" | |
Simplified Notion client for FoodWise inventory management. | |
""" | |
import os | |
from typing import List, Dict, Any, Optional | |
from datetime import datetime, timedelta, date | |
from notion_client import Client | |
from pydantic import ValidationError | |
from ..database.db_models import PartialFoodInventoryItem | |
class FoodWiseNotionClient: | |
"""Simple client for FoodWise Notion database.""" | |
def __init__(self): | |
"""Initialize the Notion client.""" | |
self.client = Client(auth=os.getenv("NOTION_SECRET")) | |
self.database_id = os.getenv("NOTION_INVENTORY_DB_ID") | |
self.shopping_database_id = os.getenv("NOTION_SHOPPING_DB_ID") | |
# Do not enforce database presence at construction time. | |
# Each method will validate only the specific DB it requires. | |
def query_inventory(self, filter_conditions=None): | |
"""Query inventory items.""" | |
if not self.database_id: | |
raise ValueError("NOTION_INVENTORY_DB_ID must be set in environment") | |
try: | |
params = {"database_id": self.database_id} | |
if filter_conditions: | |
params["filter"] = filter_conditions | |
response = self.client.databases.query(**params) | |
return [self._simplify_item(page) for page in response["results"]] | |
except Exception as e: | |
raise Exception(f"Failed to query inventory: {e}") | |
def add_inventory_item(self, item_data): | |
"""Add a new item to inventory.""" | |
if not self.database_id: | |
raise ValueError("NOTION_INVENTORY_DB_ID must be set in environment") | |
try: | |
properties = {} | |
# Required fields | |
if "name" in item_data: | |
properties["Name"] = {"title": [{"text": {"content": item_data["name"]}}]} | |
if "category" in item_data: | |
properties["Category"] = {"select": {"name": item_data["category"]}} | |
if "storage_type" in item_data: | |
properties["Storage Type"] = {"select": {"name": item_data["storage_type"]}} | |
if "quantity" in item_data: | |
properties["Quantity"] = {"number": float(item_data["quantity"])} | |
if "unit" in item_data: | |
properties["Unit"] = {"select": {"name": item_data["unit"]}} | |
# Optional fields | |
if item_data.get("best_by_date"): | |
properties["Best By Date"] = {"date": {"start": item_data["best_by_date"]}} | |
if item_data.get("purchase_date"): | |
properties["Purchase Date"] = {"date": {"start": item_data["purchase_date"]}} | |
if item_data.get("fridge_zone"): | |
properties["Fridge Zone"] = {"select": {"name": item_data["fridge_zone"]}} | |
if item_data.get("location_shelf"): | |
properties["Location/Shelf"] = {"select": {"name": item_data["location_shelf"]}} | |
if item_data.get("cooked_raw_status"): | |
properties["Cooked/Raw Status"] = {"select": {"name": item_data["cooked_raw_status"]}} | |
if "temperature_sensitive" in item_data: | |
properties["Temperature Sensitive"] = {"checkbox": bool(item_data["temperature_sensitive"])} | |
if item_data.get("prep_notes"): | |
properties["Prep Notes"] = {"rich_text": [{"text": {"content": item_data["prep_notes"]}}]} | |
if item_data.get("tags"): | |
properties["Tags"] = {"multi_select": [{"name": tag} for tag in item_data["tags"]]} | |
response = self.client.pages.create( | |
parent={"database_id": self.database_id}, | |
properties=properties | |
) | |
return self._simplify_item(response) | |
except Exception as e: | |
raise Exception(f"Failed to add item: {e}") | |
def search_items(self, search_term): | |
"""Search for items by name.""" | |
filter_condition = { | |
"property": "Name", | |
"title": {"contains": search_term} | |
} | |
return self.query_inventory(filter_condition) | |
def get_expiring_items(self, days=7): | |
"""Get items expiring within specified days.""" | |
if not self.database_id: | |
raise ValueError("NOTION_INVENTORY_DB_ID must be set in environment") | |
target_date = (datetime.now() + timedelta(days=days)).isoformat()[:10] # YYYY-MM-DD format | |
filter_condition = { | |
"and": [ | |
{ | |
"property": "Best By Date", | |
"date": {"on_or_before": target_date} | |
}, | |
{ | |
"property": "Best By Date", | |
"date": {"is_not_empty": True} | |
} | |
] | |
} | |
return self.query_inventory(filter_condition) | |
def update_inventory_item(self, page_id: str, updated_data: dict) -> dict: | |
"""Update an existing inventory item with partial data validation.""" | |
if not self.database_id: | |
raise ValueError("NOTION_INVENTORY_DB_ID must be set in environment") | |
try: | |
# Validate input data | |
validated = PartialFoodInventoryItem.model_validate(updated_data) | |
data_dict = validated.model_dump(exclude_none=True) | |
# Mapping from field names to Notion property formats | |
property_map = { | |
"name": lambda v: {"title": [{"text": {"content": v}}]}, | |
"category": lambda v: {"select": {"name": v}}, | |
"storage_type": lambda v: {"select": {"name": v}}, | |
"quantity": lambda v: {"number": float(v)}, | |
"unit": lambda v: {"select": {"name": v}}, | |
"best_by_date": lambda v: {"date": {"start": v.isoformat() if isinstance(v, date) else v}}, | |
"purchase_date": lambda v: {"date": {"start": v.isoformat() if isinstance(v, date) else v}}, | |
"opened_date": lambda v: {"date": {"start": v.isoformat() if isinstance(v, date) else v}}, | |
"freeze_date": lambda v: {"date": {"start": v.isoformat() if isinstance(v, date) else v}}, | |
"fridge_zone": lambda v: {"select": {"name": v}}, | |
"location_shelf": lambda v: {"select": {"name": v}}, | |
"cooked_raw_status": lambda v: {"select": {"name": v}}, | |
"temperature_sensitive": lambda v: {"checkbox": bool(v)}, | |
"prep_notes": lambda v: {"rich_text": [{"text": {"content": v}}]}, | |
"tags": lambda v: {"multi_select": [{"name": tag} for tag in v]} | |
} | |
# Build Notion properties, using capitalized field names | |
properties = {} | |
for field, value in data_dict.items(): | |
notion_field = field.replace("_", " ").title().replace("By Date", "By Date").replace("Raw Status", "Raw Status") | |
if field in property_map: | |
properties[notion_field] = property_map[field](value) | |
if not properties: | |
raise ValueError("No valid fields provided for update") | |
response = self.client.pages.update( | |
page_id=page_id, | |
properties=properties | |
) | |
return self._simplify_item(response) | |
except ValidationError as ve: | |
raise ValueError(f"Invalid update data: {ve}") | |
except Exception as e: | |
raise Exception(f"Failed to update item: {e}") | |
def remove_inventory_item(self, page_id): | |
"""Remove (archive) an inventory item.""" | |
if not self.database_id: | |
raise ValueError("NOTION_INVENTORY_DB_ID must be set in environment") | |
try: | |
response = self.client.pages.update( | |
page_id=page_id, | |
archived=True | |
) | |
return {"success": True, "message": f"Item {page_id} archived successfully"} | |
except Exception as e: | |
raise Exception(f"Failed to remove item: {e}") | |
def _simplify_item(self, page): | |
"""Convert Notion page to simple dictionary.""" | |
props = page["properties"] | |
def get_title(prop): | |
return prop["title"][0]["text"]["content"] if prop.get("title") and len(prop["title"]) > 0 else "" | |
def get_select(prop): | |
return prop["select"]["name"] if prop.get("select") else None | |
def get_number(prop): | |
return prop.get("number") | |
def get_checkbox(prop): | |
return prop.get("checkbox", False) | |
def get_date(prop): | |
return prop["date"]["start"] if prop.get("date") else None | |
def get_rich_text(prop): | |
return prop["rich_text"][0]["text"]["content"] if prop.get("rich_text") and len(prop["rich_text"]) > 0 else "" | |
def get_multi_select(prop): | |
return [item["name"] for item in prop.get("multi_select", [])] | |
return { | |
"id": page["id"], | |
"url": page["url"], | |
"name": get_title(props.get("Name", {})), | |
"category": get_select(props.get("Category", {})), | |
"storage_type": get_select(props.get("Storage Type", {})), | |
"quantity": get_number(props.get("Quantity", {})), | |
"unit": get_select(props.get("Unit", {})), | |
"best_by_date": get_date(props.get("Best By Date", {})), | |
"purchase_date": get_date(props.get("Purchase Date", {})), | |
"opened_date": get_date(props.get("Opened Date", {})), | |
"location_shelf": get_select(props.get("Location/Shelf", {})), | |
"fridge_zone": get_select(props.get("Fridge Zone", {})), | |
"tags": get_multi_select(props.get("Tags", {})), | |
"temperature_sensitive": get_checkbox(props.get("Temperature Sensitive", {})), | |
"cooked_raw_status": get_select(props.get("Cooked/Raw Status", {})), | |
"prep_notes": get_rich_text(props.get("Prep Notes", {})) | |
} | |
# ===== SHOPPING LIST METHODS ===== | |
def query_shopping_list(self, filter_conditions=None): | |
"""Query shopping list items.""" | |
if not self.shopping_database_id: | |
raise ValueError("NOTION_SHOPPING_DB_ID must be set in environment to use shopping list features") | |
try: | |
params = {"database_id": self.shopping_database_id} | |
if filter_conditions: | |
params["filter"] = filter_conditions | |
response = self.client.databases.query(**params) | |
return [self._simplify_shopping_item(page) for page in response["results"]] | |
except Exception as e: | |
raise Exception(f"Failed to query shopping list: {e}") | |
def add_shopping_item(self, item_data): | |
"""Add a new item to shopping list.""" | |
if not self.shopping_database_id: | |
raise ValueError("NOTION_SHOPPING_DB_ID must be set in environment to use shopping list features") | |
try: | |
properties = {} | |
# Required fields | |
if "name" in item_data: | |
properties["Item Name"] = {"title": [{"text": {"content": item_data["name"]}}]} | |
if "quantity" in item_data: | |
properties["Quantity"] = {"number": float(item_data["quantity"])} | |
if "unit" in item_data: | |
properties["Unit"] = {"rich_text": [{"text": {"content": item_data["unit"]}}]} | |
if "priority" in item_data: | |
properties["Priority"] = {"select": {"name": item_data["priority"]}} | |
if "status" in item_data: | |
properties["Status"] = {"select": {"name": item_data["status"]}} | |
# Optional fields | |
if "store" in item_data and item_data["store"]: | |
properties["Store"] = {"select": {"name": item_data["store"]}} | |
if "category" in item_data and item_data["category"]: | |
properties["Category"] = {"select": {"name": item_data["category"]}} | |
if "estimated_price" in item_data and item_data["estimated_price"]: | |
properties["Estimated Price"] = {"number": float(item_data["estimated_price"])} | |
if "brand_preference" in item_data and item_data["brand_preference"]: | |
properties["Brand Preference"] = {"rich_text": [{"text": {"content": item_data["brand_preference"]}}]} | |
if "size_package" in item_data and item_data["size_package"]: | |
properties["Size/Package"] = {"rich_text": [{"text": {"content": item_data["size_package"]}}]} | |
if "notes" in item_data and item_data["notes"]: | |
properties["Notes"] = {"rich_text": [{"text": {"content": item_data["notes"]}}]} | |
if "recipe_source" in item_data and item_data["recipe_source"]: | |
properties["Recipe Source"] = {"rich_text": [{"text": {"content": item_data["recipe_source"]}}]} | |
# Add current date | |
properties["Date Added"] = {"date": {"start": datetime.now().strftime("%Y-%m-%d")}} | |
response = self.client.pages.create( | |
parent={"database_id": self.shopping_database_id}, | |
properties=properties | |
) | |
return self._simplify_shopping_item(response) | |
except Exception as e: | |
raise Exception(f"Failed to add shopping item: {e}") | |
def update_shopping_item(self, page_id, updated_data): | |
"""Update an existing shopping item.""" | |
if not self.shopping_database_id: | |
raise ValueError("NOTION_SHOPPING_DB_ID must be set in environment to use shopping list features") | |
try: | |
properties = {} | |
# Map field names to Notion properties | |
field_mapping = { | |
"name": "Item Name", | |
"quantity": "Quantity", | |
"unit": "Unit", | |
"store": "Store", | |
"priority": "Priority", | |
"category": "Category", | |
"estimated_price": "Estimated Price", | |
"brand_preference": "Brand Preference", | |
"size_package": "Size/Package", | |
"notes": "Notes", | |
"recipe_source": "Recipe Source", | |
"status": "Status" | |
} | |
for field, value in updated_data.items(): | |
if field in field_mapping: | |
notion_field = field_mapping[field] | |
if field == "name": | |
properties[notion_field] = {"title": [{"text": {"content": str(value)}}]} | |
elif field in ["quantity", "estimated_price"]: | |
properties[notion_field] = {"number": float(value)} | |
elif field in ["store", "priority", "category", "status"]: | |
properties[notion_field] = {"select": {"name": str(value)}} | |
else: # Text fields | |
properties[notion_field] = {"rich_text": [{"text": {"content": str(value)}}]} | |
response = self.client.pages.update( | |
page_id=page_id, | |
properties=properties | |
) | |
return self._simplify_shopping_item(response) | |
except Exception as e: | |
raise Exception(f"Failed to update shopping item: {e}") | |
def remove_shopping_item(self, page_id): | |
"""Remove (archive) a shopping item.""" | |
if not self.shopping_database_id: | |
raise ValueError("NOTION_SHOPPING_DB_ID must be set in environment to use shopping list features") | |
try: | |
self.client.pages.update( | |
page_id=page_id, | |
archived=True | |
) | |
return {"success": True, "message": f"Shopping item {page_id} archived successfully"} | |
except Exception as e: | |
raise Exception(f"Failed to remove shopping item: {e}") | |
def _simplify_shopping_item(self, page): | |
"""Convert Notion page to simplified shopping item dict.""" | |
props = page.get("properties", {}) | |
# Helper functions to safely extract data | |
def get_title(prop): | |
if prop.get("title") and len(prop["title"]) > 0: | |
return prop["title"][0]["text"]["content"] | |
return None | |
def get_rich_text(prop): | |
if prop.get("rich_text") and len(prop["rich_text"]) > 0: | |
return prop["rich_text"][0]["text"]["content"] | |
return None | |
def get_number(prop): | |
return prop.get("number") | |
def get_select(prop): | |
if prop.get("select"): | |
return prop["select"]["name"] | |
return None | |
def get_date(prop): | |
if prop.get("date") and prop["date"].get("start"): | |
return prop["date"]["start"] | |
return None | |
return { | |
"id": page["id"], | |
"url": page["url"], | |
"name": get_title(props.get("Item Name", {})), | |
"quantity": get_number(props.get("Quantity", {})), | |
"unit": get_rich_text(props.get("Unit", {})), | |
"store": get_select(props.get("Store", {})), | |
"priority": get_select(props.get("Priority", {})), | |
"category": get_select(props.get("Category", {})), | |
"estimated_price": get_number(props.get("Estimated Price", {})), | |
"brand_preference": get_rich_text(props.get("Brand Preference", {})), | |
"size_package": get_rich_text(props.get("Size/Package", {})), | |
"notes": get_rich_text(props.get("Notes", {})), | |
"recipe_source": get_rich_text(props.get("Recipe Source", {})), | |
"status": get_select(props.get("Status", {})), | |
"date_added": get_date(props.get("Date Added", {})) | |
} | |