Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions src/huckleberry_api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand Down Expand Up @@ -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.

Expand All @@ -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")
Expand All @@ -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] = {}
Expand Down Expand Up @@ -1251,6 +1255,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"
Expand All @@ -1259,7 +1264,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)
Expand Down Expand Up @@ -1347,6 +1352,7 @@ async def log_diaper(
consistency: PooConsistency | None = None,
diaper_rash: bool = False,
notes: str | None = None,
start_time: datetime | None = None,
) -> None:
"""
Log a diaper change.
Expand All @@ -1360,6 +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
start_time: When the diaper change occurred; defaults to now
"""
await self._log_diaper_or_potty_event(
child_uid,
Expand All @@ -1371,6 +1378,7 @@ async def log_diaper(
consistency=consistency,
diaper_rash=diaper_rash,
notes=notes,
timestamp=start_time,
)

async def log_potty(
Expand All @@ -1383,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.

Expand All @@ -1395,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,
Expand All @@ -1407,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(
Expand All @@ -1416,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).
Expand All @@ -1426,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)

Expand All @@ -1435,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)
Expand Down
19 changes: 19 additions & 0 deletions tests/test_bottle_feeding.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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()
51 changes: 51 additions & 0 deletions tests/test_diaper.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Diaper tracking tests for Huckleberry API."""

import asyncio
import uuid
from datetime import datetime, timezone

from huckleberry_api import HuckleberryAPI

Expand Down Expand Up @@ -83,3 +85,52 @@ 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", start_time=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

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()
16 changes: 16 additions & 0 deletions tests/test_growth.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Growth tracking tests for Huckleberry API."""

import asyncio
import random
from datetime import datetime, timezone

from huckleberry_api import HuckleberryAPI

Expand Down Expand Up @@ -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()
24 changes: 24 additions & 0 deletions tests/test_solids.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import asyncio
import time
import uuid
from datetime import datetime, timezone

from google.cloud import firestore

Expand Down Expand Up @@ -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()