From 3c4ecb1790004425723c9f14751eb8f589823b4b Mon Sep 17 00:00:00 2001 From: Benjamin Goldenberg Date: Mon, 9 Mar 2026 21:33:54 -0700 Subject: [PATCH 1/2] feat: add optional timestamp parameter to log_diaper Co-Authored-By: Claude Sonnet 4.6 --- src/huckleberry_api/api.py | 6 +++++- tests/test_diaper.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/huckleberry_api/api.py b/src/huckleberry_api/api.py index f7a3767..5440453 100644 --- a/src/huckleberry_api/api.py +++ b/src/huckleberry_api/api.py @@ -1251,6 +1251,7 @@ async def _log_diaper_or_potty_event( notes: str | None = None, is_potty: bool = False, how_it_happened: PottyResult | None = None, + timestamp: datetime | None = None, ) -> None: """Write a diaper-collection diaper or potty event and update the matching prefs summary.""" event_kind = "potty" if is_potty else "diaper" @@ -1259,7 +1260,7 @@ async def _log_diaper_or_potty_event( client = await self._get_firestore_client() diaper_ref = client.collection("diaper").document(child_uid) - current_time = time.time() + current_time = timestamp.timestamp() if timestamp is not None else time.time() current_offset = await self._get_timezone_offset_minutes() # Create interval ID (timestamp in ms + random suffix) @@ -1347,6 +1348,7 @@ async def log_diaper( consistency: PooConsistency | None = None, diaper_rash: bool = False, notes: str | None = None, + timestamp: datetime | None = None, ) -> None: """ Log a diaper change. @@ -1360,6 +1362,7 @@ async def log_diaper( consistency: Poo consistency - 'solid', 'loose', 'runny', 'mucousy', 'hard', 'pebbles', 'diarrhea' diaper_rash: Whether baby has diaper rash notes: Optional notes about this diaper change + timestamp: When the diaper change occurred; defaults to now """ await self._log_diaper_or_potty_event( child_uid, @@ -1371,6 +1374,7 @@ async def log_diaper( consistency=consistency, diaper_rash=diaper_rash, notes=notes, + timestamp=timestamp, ) async def log_potty( diff --git a/tests/test_diaper.py b/tests/test_diaper.py index 50bfc2e..638de29 100644 --- a/tests/test_diaper.py +++ b/tests/test_diaper.py @@ -1,6 +1,8 @@ """Diaper tracking tests for Huckleberry API.""" import asyncio +import uuid +from datetime import datetime, timezone from huckleberry_api import HuckleberryAPI @@ -83,3 +85,32 @@ async def test_log_potty(self, api: HuckleberryAPI, child_uid: str) -> None: assert latest is not None assert latest["isPotty"] is True assert latest["howItHappened"] == "wentPotty" + + async def test_log_diaper_with_timestamp(self, api: HuckleberryAPI, child_uid: str) -> None: + """Test that a custom timestamp is stored on the interval.""" + past_time = datetime(2024, 1, 15, 10, 30, 0, tzinfo=timezone.utc) + unique_note = str(uuid.uuid4()) + await api.log_diaper(child_uid, mode="pee", timestamp=past_time, notes=unique_note) + await asyncio.sleep(1) + + db = await api._get_firestore_client() + diaper_ref = db.collection("diaper").document(child_uid) + intervals = diaper_ref.collection("intervals").where("notes", "==", unique_note).stream() + docs = [doc async for doc in intervals] + assert len(docs) == 1, "Expected exactly one interval with the unique note" + assert docs[0].to_dict()["start"] == past_time.timestamp() + + async def test_log_diaper_without_timestamp_uses_current_time(self, api: HuckleberryAPI, child_uid: str) -> None: + """Test that omitting timestamp defaults to approximately now.""" + before = datetime.now(timezone.utc).timestamp() + await api.log_diaper(child_uid, mode="dry") + after = datetime.now(timezone.utc).timestamp() + await asyncio.sleep(1) + + db = await api._get_firestore_client() + diaper_ref = db.collection("diaper").document(child_uid) + intervals = diaper_ref.collection("intervals").order_by("start", direction="DESCENDING").limit(1).stream() + docs = [doc async for doc in intervals] + assert docs + start = docs[0].to_dict()["start"] + assert before <= start <= after From 0da213d588d44f274a4cfea6fadf4d13ec08e199 Mon Sep 17 00:00:00 2001 From: Benjamin Goldenberg Date: Mon, 9 Mar 2026 21:45:48 -0700 Subject: [PATCH 2/2] feat: add optional start_time/timestamp to log_bottle, log_solids, log_potty, log_growth Rename log_diaper's parameter from timestamp to start_time for consistency with the other event-logging methods. log_growth keeps the name "timestamp" since it records a measurement, not an event. Co-Authored-By: Claude Opus 4.6 --- src/huckleberry_api/api.py | 21 +++++++++++++++------ tests/test_bottle_feeding.py | 19 +++++++++++++++++++ tests/test_diaper.py | 22 +++++++++++++++++++++- tests/test_growth.py | 16 ++++++++++++++++ tests/test_solids.py | 24 ++++++++++++++++++++++++ 5 files changed, 95 insertions(+), 7 deletions(-) diff --git a/src/huckleberry_api/api.py b/src/huckleberry_api/api.py index 5440453..b1b674e 100644 --- a/src/huckleberry_api/api.py +++ b/src/huckleberry_api/api.py @@ -915,6 +915,7 @@ async def log_bottle( amount: float, bottle_type: BottleType = "Formula", units: VolumeUnits = "ml", + start_time: datetime | None = None, ) -> None: """Log bottle feeding as instant event. @@ -923,13 +924,14 @@ async def log_bottle( bottle_type: Type of bottle contents ("Breast Milk", "Formula", "Cow Milk", etc.) amount: Amount fed in specified units units: Volume units ("ml" or "oz") + start_time: When the bottle feeding occurred; defaults to now """ _LOGGER.info("Logging bottle feeding for child %s: %s %s of %s", child_uid, amount, units, bottle_type) client = await self._get_firestore_client() feed_ref = client.collection("feed").document(child_uid) - now_time = time.time() + now_time = start_time.timestamp() if start_time is not None else time.time() interval_id = f"{int(now_time * 1000)}-{uuid.uuid4().hex[:20]}" # Create interval document for bottle feeding @@ -1069,6 +1071,7 @@ async def log_solids( notes: str = "", reaction: SolidsReaction | None = None, food_note_image: str | None = None, + start_time: datetime | None = None, ) -> None: """Log solid food feeding. @@ -1078,6 +1081,7 @@ async def log_solids( notes: Optional notes about the meal reaction: Optional reaction - "LOVED", "MEH", "HATED", or "ALLERGIC" food_note_image: Optional Firebase Storage image filename + start_time: When the solids feeding occurred; defaults to now """ if not foods: raise ValueError("At least one food is required") @@ -1087,7 +1091,7 @@ async def log_solids( client = await self._get_firestore_client() feed_ref = client.collection("feed").document(child_uid) - now_time = time.time() + now_time = start_time.timestamp() if start_time is not None else time.time() interval_id = f"{int(now_time * 1000)}-{uuid.uuid4().hex[:20]}" foods_dict: dict[str, SolidsFoodEntry] = {} @@ -1348,7 +1352,7 @@ async def log_diaper( consistency: PooConsistency | None = None, diaper_rash: bool = False, notes: str | None = None, - timestamp: datetime | None = None, + start_time: datetime | None = None, ) -> None: """ Log a diaper change. @@ -1362,7 +1366,7 @@ async def log_diaper( consistency: Poo consistency - 'solid', 'loose', 'runny', 'mucousy', 'hard', 'pebbles', 'diarrhea' diaper_rash: Whether baby has diaper rash notes: Optional notes about this diaper change - timestamp: When the diaper change occurred; defaults to now + start_time: When the diaper change occurred; defaults to now """ await self._log_diaper_or_potty_event( child_uid, @@ -1374,7 +1378,7 @@ async def log_diaper( consistency=consistency, diaper_rash=diaper_rash, notes=notes, - timestamp=timestamp, + timestamp=start_time, ) async def log_potty( @@ -1387,6 +1391,7 @@ async def log_potty( color: PooColor | None = None, consistency: PooConsistency | None = None, notes: str | None = None, + start_time: datetime | None = None, ) -> None: """Log a potty event in the shared diaper tracker. @@ -1399,6 +1404,7 @@ async def log_potty( color: Poo color - 'yellow', 'brown', 'black', 'green', 'red', 'gray' consistency: Poo consistency - 'solid', 'loose', 'runny', 'mucousy', 'hard', 'pebbles', 'diarrhea' notes: Optional notes about this potty event + start_time: When the potty event occurred; defaults to now """ await self._log_diaper_or_potty_event( child_uid, @@ -1411,6 +1417,7 @@ async def log_potty( notes=notes, is_potty=True, how_it_happened=how_it_happened, + timestamp=start_time, ) async def log_growth( @@ -1420,6 +1427,7 @@ async def log_growth( height: float | None = None, head: float | None = None, units: Literal["metric", "imperial"] = "metric", + timestamp: datetime | None = None, ) -> None: """ Log growth measurements (weight, height, head circumference). @@ -1430,6 +1438,7 @@ async def log_growth( height: Height measurement (cm for metric, inches for imperial) head: Head circumference (cm for metric, inches for imperial) units: 'metric' or 'imperial' + timestamp: When the measurement was taken; defaults to now """ _LOGGER.info("Logging growth data for child %s", child_uid) @@ -1439,7 +1448,7 @@ async def log_growth( client = await self._get_firestore_client() health_ref = client.collection("health").document(child_uid) - current_time = time.time() + current_time = timestamp.timestamp() if timestamp is not None else time.time() # Create interval ID (timestamp in ms + random suffix) interval_timestamp_ms = int(current_time * 1000) diff --git a/tests/test_bottle_feeding.py b/tests/test_bottle_feeding.py index ed6b6fa..4395b0b 100644 --- a/tests/test_bottle_feeding.py +++ b/tests/test_bottle_feeding.py @@ -1,7 +1,9 @@ """Bottle feeding tests for Huckleberry API.""" import asyncio +import random import time +from datetime import datetime, timezone from google.cloud import firestore @@ -191,3 +193,20 @@ async def test_bottle_feeding_updates_prefs(self, api: HuckleberryAPI, child_uid assert prefs["lastBottle"]["bottleType"] == "Breast Milk" assert prefs["lastBottle"]["bottleAmount"] == 110.0 assert prefs["lastBottle"]["bottleUnits"] == "oz" + + async def test_log_bottle_with_start_time(self, api: HuckleberryAPI, child_uid: str) -> None: + """Test that a custom start_time is stored on the bottle interval.""" + past_time = datetime(2024, 6, 1, 14, 0, 0, tzinfo=timezone.utc) + unique_amount = round(random.uniform(0.1, 200.0), 2) + await api.log_bottle(child_uid, amount=unique_amount, bottle_type="Formula", units="ml", start_time=past_time) + await asyncio.sleep(2) + + interval_data = await self._find_recent_bottle_interval( + api, + child_uid, + created_after=0, + bottle_type="Formula", + amount=unique_amount, + units="ml", + ) + assert interval_data["start"] == past_time.timestamp() diff --git a/tests/test_diaper.py b/tests/test_diaper.py index 638de29..d6ff8eb 100644 --- a/tests/test_diaper.py +++ b/tests/test_diaper.py @@ -90,7 +90,7 @@ async def test_log_diaper_with_timestamp(self, api: HuckleberryAPI, child_uid: s """Test that a custom timestamp is stored on the interval.""" past_time = datetime(2024, 1, 15, 10, 30, 0, tzinfo=timezone.utc) unique_note = str(uuid.uuid4()) - await api.log_diaper(child_uid, mode="pee", timestamp=past_time, notes=unique_note) + await api.log_diaper(child_uid, mode="pee", start_time=past_time, notes=unique_note) await asyncio.sleep(1) db = await api._get_firestore_client() @@ -114,3 +114,23 @@ async def test_log_diaper_without_timestamp_uses_current_time(self, api: Huckleb assert docs start = docs[0].to_dict()["start"] assert before <= start <= after + + async def test_log_potty_with_start_time(self, api: HuckleberryAPI, child_uid: str) -> None: + """Test that a custom start_time is stored on the potty interval.""" + past_time = datetime(2024, 5, 20, 9, 0, 0, tzinfo=timezone.utc) + unique_note = str(uuid.uuid4()) + await api.log_potty( + child_uid, + mode="pee", + how_it_happened="wentPotty", + notes=unique_note, + start_time=past_time, + ) + await asyncio.sleep(1) + + db = await api._get_firestore_client() + diaper_ref = db.collection("diaper").document(child_uid) + intervals = diaper_ref.collection("intervals").where("notes", "==", unique_note).stream() + docs = [doc async for doc in intervals] + assert len(docs) == 1, "Expected exactly one interval with the unique note" + assert docs[0].to_dict()["start"] == past_time.timestamp() diff --git a/tests/test_growth.py b/tests/test_growth.py index 7c5ff87..6cafbc2 100644 --- a/tests/test_growth.py +++ b/tests/test_growth.py @@ -1,6 +1,8 @@ """Growth tracking tests for Huckleberry API.""" import asyncio +import random +from datetime import datetime, timezone from huckleberry_api import HuckleberryAPI @@ -51,3 +53,17 @@ async def test_get_latest_growth(self, api: HuckleberryAPI, child_uid: str) -> N assert growth_data.heightUnits in ("cm", "ft.in") if growth_data.headUnits is not None: assert growth_data.headUnits in ("hcm", "hin") + + async def test_log_growth_with_timestamp(self, api: HuckleberryAPI, child_uid: str) -> None: + """Test that a custom timestamp is stored on the growth entry.""" + past_time = datetime(2024, 2, 14, 8, 0, 0, tzinfo=timezone.utc) + unique_weight = round(random.uniform(0.1, 30.0), 4) + await api.log_growth(child_uid, weight=unique_weight, units="metric", timestamp=past_time) + await asyncio.sleep(1) + + db = await api._get_firestore_client() + health_ref = db.collection("health").document(child_uid) + data_stream = health_ref.collection("data").where("weight", "==", unique_weight).stream() + docs = [doc async for doc in data_stream] + assert len(docs) >= 1, "Expected at least one growth entry with the unique weight" + assert docs[0].to_dict()["start"] == past_time.timestamp() diff --git a/tests/test_solids.py b/tests/test_solids.py index 1c38103..63a3bde 100644 --- a/tests/test_solids.py +++ b/tests/test_solids.py @@ -2,6 +2,8 @@ import asyncio import time +import uuid +from datetime import datetime, timezone from google.cloud import firestore @@ -153,3 +155,25 @@ async def test_solids_in_feed_interval_events(self, api: HuckleberryAPI, child_u feed_entries = await api.list_feed_intervals(child_uid, start_ts, end_ts) assert any(entry.mode == "solids" for entry in feed_entries) + + async def test_log_solids_with_start_time(self, api: HuckleberryAPI, child_uid: str) -> None: + """Test that a custom start_time is stored on the solids interval.""" + curated = await api.list_solids_curated_foods() + assert curated + + past_time = datetime(2024, 3, 10, 12, 0, 0, tzinfo=timezone.utc) + unique_note = str(uuid.uuid4()) + await api.log_solids( + child_uid, + foods=[SolidsFoodReference(id=curated[0].id, source="curated", name=curated[0].name, amount="small")], + notes=unique_note, + start_time=past_time, + ) + await asyncio.sleep(2) + + db = await api._get_firestore_client() + intervals_ref = db.collection("feed").document(child_uid).collection("intervals") + intervals = intervals_ref.where("notes", "==", unique_note).stream() + docs = [doc async for doc in intervals] + assert len(docs) == 1, "Expected exactly one interval with the unique note" + assert docs[0].to_dict()["start"] == past_time.timestamp()