Skip to content

sdk: hide raw PK arrays from FoodLogEntry.to_dict(); add round-trip functional test#60

Merged
phitoduck merged 3 commits into
mainfrom
hide-entry-food-pk
Jun 12, 2026
Merged

sdk: hide raw PK arrays from FoodLogEntry.to_dict(); add round-trip functional test#60
phitoduck merged 3 commits into
mainfrom
hide-entry-food-pk

Conversation

@phitoduck

Copy link
Copy Markdown
Owner

Summary

  • FoodLogEntry.to_dict() was projecting the food and entry SimplePrimaryKey arrays (16 signed ints each) straight into the JSON/TOON output. After auditing the GWT RPC surface, none of those bytes need to cross the SDK boundary.
  • to_dict() now emits food_id (32-char lowercase hex, same shape as FoodSearchResult.food_id) and drops both raw entry_pk / food_pk byte arrays. The raw bytes stay on the dataclass for the envelope builders.
  • Added a live-API functional test that mirrors the README's SDK example, pinned to a 2018 date, that round-trips search → log → diary → delete → diary using only the documented LoseIt surface — proving the external identifier (food_id) is sufficient and no PK bytes ever have to leak.
  • Updated the README example to use food_id for the match-and-delete loop instead of a fragile name-substring match, so the docs and the new test stay aligned.

Why no entry_id either?

Both PKs are needed inside the SDK to build envelopes, but only the food PK has any external use case (round-tripping a food reference through LoseIt.{get_food,describe_food,log_food}). The entry PK has none:

  • No LoseIt RPC accepts an entry PK as standalone input.
  • deleteFoodLogEntry requires the full entry body — name, brand, day keys, day_num, hours_from_gmt, meal_ordinal, extra_ordinal, food_measure_ordinal, servings, food_identifier_code, the full nutrients_ordered list, and both PKs — which forces a fresh getDailyDetailsIncludingPendingForDate read; the entry PK comes along for free in that response.
  • There is no path where a caller can save just an entry id and act on it later.

So entry_id (hex) was added in an earlier commit and then removed: there is no consumer for it that doesn't also have the full FoodLogEntry dataclass already.

Test plan

  • uv run pytest -q — 165 passed (default hermetic suite, including updated test_cli.py shape assertion)
  • uv run ruff check && uv run ruff format --check — clean
  • uv run pytest -m requires_auth tests/functional/test_readme_example.py — run against a real Lose It! account (requires LOSEIT_* env vars + valid liauth JWT). This is the gating signal: if this passes, the externally visible PK arrays can stay hidden.

Notes

Please do not merge until the live-API round-trip test (test_readme_sdk_example_round_trip) has been run against a real account on the pinned 2018-03-15 diary day and shown to pass.

…unctional test

`FoodLogEntry.to_dict()` projected the food/entry SimplePrimaryKeys
verbatim as 16-int byte arrays — noise that no caller of the JSON/TOON
output actually consumed:

- Food PK: never crosses the SDK boundary as bytes. The only external
  use case is round-tripping a food reference, which `food_id` (hex)
  already serves via `LoseIt.{get_food,describe_food,log_food}`.
- Entry PK: no LoseIt RPC accepts it as input on its own. Even
  `deleteFoodLogEntry` requires the full entry body, which forces a
  fresh diary read; the entry PK comes along for the ride. There is
  no external workflow where a caller can act on an entry PK alone.

So `to_dict()` now emits `food_id` (32-char hex, same shape as
`FoodSearchResult.food_id`) and drops both raw `pk` arrays + the
entry-side hex entirely. The raw bytes stay on the dataclass where
the envelope builders need them.

Added a live-API functional test (`tests/functional/test_readme_example.py`)
mirroring the README's SDK example: pinned to 2018-03-15 for isolation,
asserts empty → log → present → delete → empty using only the documented
SDK surface (`search`/`log_food`/`diary`/`delete_entry`). Matching
diary entries by `food_id` proves that the externally visible identifier
is sufficient for the round-trip — no PK bytes ever escape the SDK.

Also extended the README example to use `food_id` for the match-and-delete
loop instead of a name substring, so the docs and the test stay aligned.
…-stored food

The README's SDK example logs `serving_amount=61, serving_unit=ServingUnit.g`
as the second log, but the first hit for `li.search("tortilla")` is the
Xtreme Wellness wrap — serving-stored with no per-serving-g cross-class
slot — so the gram path raises `PortionError` before the diary read-back
even runs.

The round-trip test isn't gating on the unit-conversion code (that's
covered by `test_entries_serving_unit.py` in the conformance suite). It's
gating on `search → log → diary → delete → diary` only needing `food_id`
to round-trip — no PK arrays. So the second log now uses `servings=2.0`
instead, which works regardless of the food's native unit.

Verified live against the real API on 2018-03-15: passes.
@phitoduck phitoduck merged commit 5d34c7c into main Jun 12, 2026
4 checks passed
@phitoduck phitoduck deleted the hide-entry-food-pk branch June 12, 2026 22:10
phitoduck added a commit that referenced this pull request Jun 13, 2026
main shipped v0.3.0 via PR #60 while delete-safeguards was being
integrated; bumping to 0.4.0 keeps the tag-on-merge CI happy when
the eventual delete-safeguards -> main PR lands.
phitoduck added a commit that referenced this pull request Jun 13, 2026
…store (#71)

* sdk: hide raw PK arrays from FoodLogEntry.to_dict(); add round-trip functional test (#60)

* sdk: hide raw PK arrays from FoodLogEntry.to_dict(); add round-trip functional test

`FoodLogEntry.to_dict()` projected the food/entry SimplePrimaryKeys
verbatim as 16-int byte arrays — noise that no caller of the JSON/TOON
output actually consumed:

- Food PK: never crosses the SDK boundary as bytes. The only external
  use case is round-tripping a food reference, which `food_id` (hex)
  already serves via `LoseIt.{get_food,describe_food,log_food}`.
- Entry PK: no LoseIt RPC accepts it as input on its own. Even
  `deleteFoodLogEntry` requires the full entry body, which forces a
  fresh diary read; the entry PK comes along for the ride. There is
  no external workflow where a caller can act on an entry PK alone.

So `to_dict()` now emits `food_id` (32-char hex, same shape as
`FoodSearchResult.food_id`) and drops both raw `pk` arrays + the
entry-side hex entirely. The raw bytes stay on the dataclass where
the envelope builders need them.

Added a live-API functional test (`tests/functional/test_readme_example.py`)
mirroring the README's SDK example: pinned to 2018-03-15 for isolation,
asserts empty → log → present → delete → empty using only the documented
SDK surface (`search`/`log_food`/`diary`/`delete_entry`). Matching
diary entries by `food_id` proves that the externally visible identifier
is sufficient for the round-trip — no PK bytes ever escape the SDK.

Also extended the README example to use `food_id` for the match-and-delete
loop instead of a name substring, so the docs and the test stay aligned.

* test(functional): use servings-based second log; gram path needs gram-stored food

The README's SDK example logs `serving_amount=61, serving_unit=ServingUnit.g`
as the second log, but the first hit for `li.search("tortilla")` is the
Xtreme Wellness wrap — serving-stored with no per-serving-g cross-class
slot — so the gram path raises `PortionError` before the diary read-back
even runs.

The round-trip test isn't gating on the unit-conversion code (that's
covered by `test_entries_serving_unit.py` in the conformance suite). It's
gating on `search → log → diary → delete → diary` only needing `food_id`
to round-trip — no PK arrays. So the second log now uses `servings=2.0`
instead, which works regardless of the food's native unit.

Verified live against the real API on 2018-03-15: passes.

* chore: bump version to 0.3.0

* feat(cli+sdk): backup / restore-backup CLI + safe-mode upsert restore

Wires T7's pure plan_day function into the orchestrator as
restore_backup_safe, exposes it through LoseIt.restore_backup (replacing
the prior NotImplementedError stub), and adds the loseit backup and
loseit restore-backup CLI commands per spec §3.

* src/lose_it/backup/_orchestrator.py: new restore_backup_safe + a
  SafeRestoreGrainReport report shape; RestoreSummary gains per-day
  counters that safe mode populates.
* src/lose_it/client.py: restore_backup default routes through safe
  mode, with skip_restore_on_nonempty_grain_time_ranges=True falling
  back to cheap mode. New upsert_window kwarg surfaces the ±10m fuzz.
* src/lose_it/cli.py: adds backup, restore-backup commands. Renders
  per-grain rows + summary block per spec §3.1 / §3.2; supports
  --dry-run, --quiet-skips, -o text|json|toon.
* tests/conformance/test_backup_restore_safe.py: 7 unit tests for
  safe-mode (missing → log, idempotent re-run, ±10m window, additive
  vs. server-only, dry-run, strict_account, pk round-trip).
* tests/conformance/test_cli_backup.py: 7 CLI integration tests
  hermetic via monkeypatched _open_loseit.
* version.txt: 0.6.0 → 0.7.0.
phitoduck added a commit that referenced this pull request Jun 13, 2026
Main shipped PR #60 (0.3.0) during the 8-track integration. delete-
safeguards bumped through 0.4.0, 0.5.0, 0.6.0, 0.7.0 in successive
track merges. Resolving the version.txt conflict in favor of 0.7.0
since CI tags v{version} on merge to main, and v0.3.0 is already
taken.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant