mirror of
https://github.com/thekiwismarthome/shopping-list-manager.git
synced 2026-05-01 11:46:30 +00:00
Update models.py
This commit is contained in:
@@ -1,104 +1,106 @@
|
|||||||
"""Data models for Shopping List Manager."""
|
"""Data models for Shopping List Manager."""
|
||||||
from dataclasses import dataclass, asdict
|
from dataclasses import dataclass, field, asdict
|
||||||
from typing import Dict
|
from datetime import datetime
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
def generate_id() -> str:
|
||||||
|
"""Generate a unique ID."""
|
||||||
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
|
|
||||||
|
def current_timestamp() -> str:
|
||||||
|
"""Get current ISO timestamp."""
|
||||||
|
return datetime.utcnow().isoformat() + "Z"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Category:
|
||||||
|
"""Category model."""
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
icon: str
|
||||||
|
color: str
|
||||||
|
sort_order: int
|
||||||
|
system: bool = True
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
"""Convert to dictionary."""
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Product:
|
class Product:
|
||||||
"""
|
"""Product model."""
|
||||||
Product catalog entry - authoritative product definition.
|
id: str
|
||||||
|
|
||||||
Products exist independently of the shopping list.
|
|
||||||
They define WHAT can be shopped, not HOW MUCH is needed.
|
|
||||||
"""
|
|
||||||
key: str
|
|
||||||
name: str
|
name: str
|
||||||
category: str = "other"
|
category_id: str
|
||||||
unit: str = "pcs"
|
aliases: List[str] = field(default_factory=list)
|
||||||
image: str = ""
|
default_unit: str = "units"
|
||||||
|
default_quantity: float = 1
|
||||||
|
price: Optional[float] = None
|
||||||
|
currency: Optional[str] = None
|
||||||
|
price_per_unit: Optional[float] = None
|
||||||
|
price_updated: Optional[str] = None
|
||||||
|
image_url: Optional[str] = None
|
||||||
|
image_source: Optional[str] = None
|
||||||
|
barcode: Optional[str] = None
|
||||||
|
brands: List[str] = field(default_factory=list)
|
||||||
|
nutrition: Optional[Dict[str, Any]] = None
|
||||||
|
user_frequency: int = 0
|
||||||
|
last_used: Optional[str] = None
|
||||||
|
custom: bool = False
|
||||||
|
source: str = "user"
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
"""Convert to dictionary for storage/transmission."""
|
"""Convert to dictionary."""
|
||||||
return {
|
return asdict(self)
|
||||||
"key": self.key,
|
|
||||||
"name": self.name,
|
|
||||||
"category": self.category,
|
|
||||||
"unit": self.unit,
|
|
||||||
"image": self.image
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_dict(data: dict) -> 'Product':
|
|
||||||
"""Create Product from dictionary."""
|
|
||||||
return Product(
|
|
||||||
key=data["key"],
|
|
||||||
name=data["name"],
|
|
||||||
category=data.get("category", "other"),
|
|
||||||
unit=data.get("unit", "pcs"),
|
|
||||||
image=data.get("image", "")
|
|
||||||
)
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
"""Validate product data."""
|
|
||||||
if not self.key:
|
|
||||||
raise ValueError("Product key cannot be empty")
|
|
||||||
if not self.name:
|
|
||||||
raise ValueError("Product name cannot be empty")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ActiveItem:
|
class Item:
|
||||||
"""
|
"""Shopping list item model."""
|
||||||
Shopping list state - quantity only.
|
id: str
|
||||||
|
list_id: str
|
||||||
|
name: str
|
||||||
|
category_id: str
|
||||||
|
product_id: Optional[str] = None
|
||||||
|
quantity: float = 1
|
||||||
|
unit: str = "units"
|
||||||
|
note: Optional[str] = None
|
||||||
|
checked: bool = False
|
||||||
|
checked_at: Optional[str] = None
|
||||||
|
created_at: str = field(default_factory=current_timestamp)
|
||||||
|
updated_at: str = field(default_factory=current_timestamp)
|
||||||
|
image_url: Optional[str] = None
|
||||||
|
order_index: int = 0
|
||||||
|
price: Optional[float] = None
|
||||||
|
estimated_total: Optional[float] = None
|
||||||
|
barcode: Optional[str] = None
|
||||||
|
|
||||||
Contains NO product metadata, only references products by key.
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
qty > 0 means "on the list"
|
"""Convert to dictionary."""
|
||||||
qty == 0 means "not on the list" (should be removed)
|
return asdict(self)
|
||||||
"""
|
|
||||||
qty: int
|
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def calculate_total(self) -> None:
|
||||||
"""Convert to dictionary for storage/transmission."""
|
"""Calculate estimated total from quantity and price."""
|
||||||
return {"qty": self.qty}
|
if self.price is not None:
|
||||||
|
self.estimated_total = self.quantity * self.price
|
||||||
@staticmethod
|
|
||||||
def from_dict(data: dict) -> 'ActiveItem':
|
|
||||||
"""Create ActiveItem from dictionary."""
|
|
||||||
return ActiveItem(qty=data["qty"])
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
"""Validate quantity."""
|
|
||||||
if self.qty < 0:
|
|
||||||
raise ValueError("Quantity cannot be negative")
|
|
||||||
|
|
||||||
|
|
||||||
class InvariantError(Exception):
|
@dataclass
|
||||||
"""
|
class ShoppingList:
|
||||||
Raised when the core data model invariant is violated.
|
"""Shopping list model."""
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
icon: str = "mdi:cart"
|
||||||
|
created_at: str = field(default_factory=current_timestamp)
|
||||||
|
updated_at: str = field(default_factory=current_timestamp)
|
||||||
|
item_order: List[str] = field(default_factory=list)
|
||||||
|
category_order: List[str] = field(default_factory=list)
|
||||||
|
active: bool = False
|
||||||
|
|
||||||
Invariant: Every key in active_list MUST exist in products.
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
"""Convert to dictionary."""
|
||||||
If this exception is raised, the system is in an inconsistent state
|
return asdict(self)
|
||||||
and must be repaired before continuing.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def validate_invariant(products: Dict[str, Product],
|
|
||||||
active_list: Dict[str, ActiveItem]) -> None:
|
|
||||||
"""
|
|
||||||
Validate the core data model invariant.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
products: Product catalog dictionary
|
|
||||||
active_list: Active shopping list dictionary
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
InvariantError: If any key in active_list doesn't exist in products
|
|
||||||
"""
|
|
||||||
for key in active_list:
|
|
||||||
if key not in products:
|
|
||||||
raise InvariantError(
|
|
||||||
f"Invariant violated: active_list contains unknown product key '{key}'. "
|
|
||||||
f"This product must be added to the catalog first."
|
|
||||||
)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user