sdk: hide raw PK arrays from FoodLogEntry.to_dict(); add round-trip functional test#60
Merged
Conversation
…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
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
FoodLogEntry.to_dict()was projecting the food and entrySimplePrimaryKeyarrays (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 emitsfood_id(32-char lowercase hex, same shape asFoodSearchResult.food_id) and drops both rawentry_pk/food_pkbyte arrays. The raw bytes stay on the dataclass for the envelope builders.search → log → diary → delete → diaryusing only the documentedLoseItsurface — proving the external identifier (food_id) is sufficient and no PK bytes ever have to leak.food_idfor the match-and-delete loop instead of a fragile name-substring match, so the docs and the new test stay aligned.Why no
entry_ideither?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:deleteFoodLogEntryrequires 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 fullnutrients_orderedlist, and both PKs — which forces a freshgetDailyDetailsIncludingPendingForDateread; the entry PK comes along for free in that response.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 fullFoodLogEntrydataclass already.Test plan
uv run pytest -q— 165 passed (default hermetic suite, including updatedtest_cli.pyshape assertion)uv run ruff check && uv run ruff format --check— cleanuv run pytest -m requires_auth tests/functional/test_readme_example.py— run against a real Lose It! account (requiresLOSEIT_*env vars + validliauthJWT). 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 pinned2018-03-15diary day and shown to pass.