"""
Household and HouseholdMember entities and proxies for the WiseFood API.
"""
from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING
if TYPE_CHECKING:
from ..api_client import Client
# ---------------------------------------------------------------------------
# Data Models
# ---------------------------------------------------------------------------
[docs]
class HouseholdMemberProfile:
"""
Profile information for a household member.
Setting properties automatically syncs to the API when sync=True.
Example:
>>> member.profile.dietary_groups = ["vegetarian", "gluten_free"]
>>> member.profile.nutritional_preferences = {"calories": 2000}
"""
def __init__(
self,
client: Optional["Client"] = None,
member_id: Optional[str] = None,
data: Optional[Dict[str, Any]] = None,
sync: bool = True,
):
object.__setattr__(self, "_client", client)
object.__setattr__(self, "_member_id", member_id)
object.__setattr__(self, "_data", data or {})
object.__setattr__(self, "_sync", sync)
object.__setattr__(self, "_dirty_fields", set())
[docs]
@classmethod
def from_dict(
cls,
data: Dict[str, Any],
client: Optional["Client"] = None,
member_id: Optional[str] = None,
) -> "HouseholdMemberProfile":
return cls(client=client, member_id=member_id, data=data)
def __getattr__(self, name: str) -> Any:
if name.startswith("_"):
return object.__getattribute__(self, name)
data = object.__getattribute__(self, "_data")
if name in data:
return data[name]
# Return empty defaults for known fields
if name in ("dietary_groups", "allergies"):
return []
if name in ("nutritional_preferences", "properties"):
return {}
raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'")
def __setattr__(self, name: str, value: Any) -> None:
if name.startswith("_"):
object.__setattr__(self, name, value)
return
data = object.__getattribute__(self, "_data")
dirty = object.__getattribute__(self, "_dirty_fields")
sync = object.__getattribute__(self, "_sync")
data[name] = value
dirty.add(name)
if sync:
self.save()
[docs]
def save(self) -> None:
"""Persist changes to the API."""
client = object.__getattribute__(self, "_client")
member_id = object.__getattribute__(self, "_member_id")
data = object.__getattribute__(self, "_data")
dirty = object.__getattribute__(self, "_dirty_fields")
if not client or not member_id:
return
if not dirty:
return
# Send only dirty fields
payload = {k: data[k] for k in dirty if k in data}
client.patch(f"members/{member_id}/profile", json=payload)
dirty.clear()
[docs]
def refresh(self) -> None:
"""Reload profile data from the API."""
client = object.__getattribute__(self, "_client")
member_id = object.__getattribute__(self, "_member_id")
if not client or not member_id:
raise RuntimeError("Profile not bound to a client")
resp = client.get(f"members/{member_id}/profile")
data = resp.json().get("result", resp.json())
object.__setattr__(self, "_data", data)
object.__getattribute__(self, "_dirty_fields").clear()
[docs]
def delete(self) -> None:
"""Delete this profile from the API."""
client = object.__getattribute__(self, "_client")
member_id = object.__getattribute__(self, "_member_id")
if not client or not member_id:
raise RuntimeError("Profile not bound to a client")
client.delete(f"members/{member_id}/profile")
object.__setattr__(self, "_data", {})
[docs]
def to_dict(self) -> Dict[str, Any]:
return dict(object.__getattribute__(self, "_data"))
def __repr__(self) -> str:
data = object.__getattribute__(self, "_data")
return f"HouseholdMemberProfile({data})"
[docs]
class HouseholdMember:
"""
A member of a household.
Access the profile via the `profile` property which auto-fetches if needed.
Example:
>>> member = client.members.get("member-id")
>>> member.profile.dietary_groups = ["vegan"]
"""
def __init__(
self,
client: Optional["Client"] = None,
data: Optional[Dict[str, Any]] = None,
):
self._client = client
self._data = data or {}
self._profile: Optional[HouseholdMemberProfile] = None
[docs]
@classmethod
def from_dict(cls, data: Dict[str, Any], client: Optional["Client"] = None) -> "HouseholdMember":
return cls(client=client, data=data)
@property
def id(self) -> str:
return self._data["id"]
@property
def name(self) -> str:
return self._data.get("name", "")
@name.setter
def name(self, value: str) -> None:
self._data["name"] = value
self._sync_field("name", value)
@property
def age_group(self) -> str:
return self._data.get("age_group", "")
@age_group.setter
def age_group(self, value: str) -> None:
self._data["age_group"] = value
self._sync_field("age_group", value)
@property
def household_id(self) -> str:
return self._data.get("household_id", "")
@property
def image_url(self) -> Optional[str]:
return self._data.get("image_url")
@image_url.setter
def image_url(self, value: Optional[str]) -> None:
self._data["image_url"] = value
self._sync_field("image_url", value)
@property
def created_at(self) -> Optional[str]:
return self._data.get("created_at")
@property
def updated_at(self) -> Optional[str]:
return self._data.get("updated_at")
@property
def profile(self) -> HouseholdMemberProfile:
"""
Get the member's profile, fetching from API if not loaded.
Returns a profile object where setting properties auto-syncs to API.
Example:
>>> member.profile.dietary_groups = ["vegetarian"]
>>> member.profile.nutritional_preferences = {"protein": 50}
"""
if self._profile is None:
if not self._client:
raise RuntimeError("Member not bound to a client")
try:
resp = self._client.get(f"members/{self.id}/profile")
data = resp.json().get("result", resp.json())
except Exception:
# Profile might not exist yet, return empty profile
data = {}
self._profile = HouseholdMemberProfile(
client=self._client,
member_id=self.id,
data=data,
)
return self._profile
def _sync_field(self, field: str, value: Any) -> None:
"""Sync a single field to the API."""
if self._client:
self._client.patch(f"members/{self.id}", json={field: value})
[docs]
def refresh(self) -> None:
"""Reload member data from the API."""
if not self._client:
raise RuntimeError("Member not bound to a client")
resp = self._client.get(f"members/{self.id}")
self._data = resp.json().get("result", resp.json())
self._profile = None # Reset profile to force re-fetch
[docs]
def delete(self) -> None:
"""Delete this member."""
if not self._client:
raise RuntimeError("Member not bound to a client")
self._client.delete(f"members/{self.id}")
[docs]
def to_dict(self) -> Dict[str, Any]:
return dict(self._data)
def __repr__(self) -> str:
return f"<HouseholdMember id='{self.id}' name='{self.name}'>"
[docs]
class Household:
"""
A household in the WiseFood system.
Example:
>>> household = client.households.me()
>>> household.name = "New Family Name" # auto-syncs
>>> members = household.members
"""
def __init__(
self,
client: Optional["Client"] = None,
data: Optional[Dict[str, Any]] = None,
):
self._client = client
self._data = data or {}
[docs]
@classmethod
def from_dict(cls, data: Dict[str, Any], client: Optional["Client"] = None) -> "Household":
return cls(client=client, data=data)
@property
def id(self) -> str:
return self._data["id"]
@property
def name(self) -> str:
return self._data.get("name", "")
@name.setter
def name(self, value: str) -> None:
self._data["name"] = value
self._sync_field("name", value)
@property
def owner_id(self) -> str:
return self._data.get("owner_id", "")
@property
def region(self) -> Optional[str]:
return self._data.get("region")
@region.setter
def region(self, value: Optional[str]) -> None:
self._data["region"] = value
self._sync_field("region", value)
@property
def metadata(self) -> Optional[Dict[str, Any]]:
return self._data.get("metadata")
@metadata.setter
def metadata(self, value: Optional[Dict[str, Any]]) -> None:
self._data["metadata"] = value
self._sync_field("metadata", value)
@property
def created_at(self) -> Optional[str]:
return self._data.get("created_at")
@property
def updated_at(self) -> Optional[str]:
return self._data.get("updated_at")
@property
def members(self) -> List["HouseholdMember"]:
"""Get all members of this household."""
if not self._client:
raise RuntimeError("Household not bound to a client")
resp = self._client.get("members", household_id=self.id)
data = resp.json().get("result", resp.json())
if isinstance(data, list):
return [HouseholdMember.from_dict(m, self._client) for m in data]
return []
def _sync_field(self, field: str, value: Any) -> None:
"""Sync a single field to the API."""
if self._client:
self._client.patch(f"households/{self.id}", json={field: value})
[docs]
def add_member(self, name: str, age_group: str, **kwargs) -> "HouseholdMember":
"""Add a new member to this household."""
if not self._client:
raise RuntimeError("Household not bound to a client")
payload = {"name": name, "age_group": age_group, "household_id": self.id, **kwargs}
resp = self._client.post("members", json=payload)
data = resp.json().get("result", resp.json())
return HouseholdMember.from_dict(data, self._client)
[docs]
def refresh(self) -> None:
"""Reload household data from the API."""
if not self._client:
raise RuntimeError("Household not bound to a client")
resp = self._client.get(f"households/{self.id}")
self._data = resp.json().get("result", resp.json())
[docs]
def delete(self) -> None:
"""Delete this household."""
if not self._client:
raise RuntimeError("Household not bound to a client")
self._client.delete(f"households/{self.id}")
[docs]
def to_dict(self) -> Dict[str, Any]:
return dict(self._data)
def __repr__(self) -> str:
return f"<Household id='{self.id}' name='{self.name}'>"
# ---------------------------------------------------------------------------
# Proxy Classes
# ---------------------------------------------------------------------------
[docs]
class HouseholdsProxy:
"""
Proxy for household operations.
Provides convenient access to household CRUD operations through the Client.
Example:
>>> client.households.me() # Get current user's household
>>> client.households.get("household-id-123")
>>> client.households.create(name="My Family")
"""
def __init__(self, client: "Client") -> None:
self._client = client
[docs]
def me(self) -> Household:
"""
Get the household owned by the authenticated user.
Returns:
Household object for the current user
Example:
>>> household = client.households.me()
>>> print(household.name)
"""
resp = self._client.get("households/me")
data = resp.json().get("result", resp.json())
return Household.from_dict(data, self._client)
[docs]
def get(self, household_id: str) -> Household:
"""
Get a household by ID.
Args:
household_id: The household's unique identifier
Returns:
Household object
Example:
>>> household = client.households.get("abc123")
"""
resp = self._client.get(f"households/{household_id}")
data = resp.json().get("result", resp.json())
return Household.from_dict(data, self._client)
[docs]
def list(self, limit: int = 100, offset: int = 0) -> List[Household]:
"""
List all households (admin only).
Args:
limit: Maximum number of results (default: 100)
offset: Number of results to skip (default: 0)
Returns:
List of Household objects
Example:
>>> households = client.households.list(limit=50)
"""
resp = self._client.get("households", limit=limit, offset=offset)
data = resp.json().get("result", resp.json())
if isinstance(data, list):
return [Household.from_dict(h, self._client) for h in data]
return []
[docs]
def create(self, name: str, region: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
members: Optional[List[Dict[str, Any]]] = None) -> Household:
"""
Create a new household.
Args:
name: Household name
region: Optional region identifier
metadata: Optional metadata dictionary
members: Optional list of initial members to create
Returns:
Created Household object
Example:
>>> household = client.households.create(
... name="My Family",
... region="US-CA",
... members=[{"name": "John", "age_group": "adult"}]
... )
"""
payload: Dict[str, Any] = {"name": name}
if region:
payload["region"] = region
if metadata:
payload["metadata"] = metadata
if members:
payload["members"] = members
resp = self._client.post("households", json=payload)
data = resp.json().get("result", resp.json())
return Household.from_dict(data, self._client)
[docs]
def update(self, household_id: str, **kwargs) -> Household:
"""
Update a household.
Args:
household_id: The household's unique identifier
**kwargs: Fields to update (name, region, metadata)
Returns:
Updated Household object
Example:
>>> household = client.households.update("abc123", name="New Name")
"""
resp = self._client.patch(f"households/{household_id}", json=kwargs)
data = resp.json().get("result", resp.json())
return Household.from_dict(data, self._client)
[docs]
def delete(self, household_id: str) -> None:
"""
Delete a household.
Args:
household_id: The household's unique identifier
Example:
>>> client.households.delete("abc123")
"""
self._client.delete(f"households/{household_id}")
[docs]
class MembersProxy:
"""
Proxy for household member operations.
Provides convenient access to member CRUD operations through the Client.
Example:
>>> member = client.members.get("member-id-123")
>>> member.profile.dietary_groups = ["vegetarian"] # auto-syncs
"""
def __init__(self, client: "Client") -> None:
self._client = client
[docs]
def get(self, member_id: str) -> HouseholdMember:
"""
Get a household member by ID.
Args:
member_id: The member's unique identifier
Returns:
HouseholdMember object with auto-syncing profile
Example:
>>> member = client.members.get("member123")
>>> member.profile.dietary_groups = ["vegan"]
"""
resp = self._client.get(f"members/{member_id}")
data = resp.json().get("result", resp.json())
return HouseholdMember.from_dict(data, self._client)
[docs]
def list(self, household_id: str, limit: int = 100, offset: int = 0) -> List[HouseholdMember]:
"""
List household members.
Args:
household_id: The household's unique identifier
limit: Maximum number of results (default: 100)
offset: Number of results to skip (default: 0)
Returns:
List of HouseholdMember objects
Example:
>>> members = client.members.list(household_id="abc123")
"""
resp = self._client.get("members", household_id=household_id, limit=limit, offset=offset)
data = resp.json().get("result", resp.json())
if isinstance(data, list):
return [HouseholdMember.from_dict(m, self._client) for m in data]
return []
[docs]
def create(self, household_id: str, name: str, age_group: str,
image_url: Optional[str] = None) -> HouseholdMember:
"""
Create a new household member.
Args:
household_id: The household's unique identifier
name: Member name
age_group: Age group (child, teen, adult, senior, etc.)
image_url: Optional URL to member's profile image
Returns:
Created HouseholdMember object
Example:
>>> member = client.members.create(
... household_id="abc123",
... name="John",
... age_group="adult"
... )
>>> member.profile.dietary_groups = ["vegetarian"]
"""
payload: Dict[str, Any] = {
"household_id": household_id,
"name": name,
"age_group": age_group,
}
if image_url:
payload["image_url"] = image_url
resp = self._client.post("members", json=payload)
data = resp.json().get("result", resp.json())
return HouseholdMember.from_dict(data, self._client)
[docs]
def delete(self, member_id: str) -> None:
"""
Delete a household member.
Args:
member_id: The member's unique identifier
Example:
>>> client.members.delete("member123")
"""
self._client.delete(f"members/{member_id}")