Metadata-Version: 2.5
Name: healthlog
Version: 0.3.4
Summary: Read Google Health data, and log explicit nutrient data to it.
Project-URL: Homepage, https://github.com/owahltinez/healthlog
Project-URL: Repository, https://github.com/owahltinez/healthlog
Author: Healthlog Contributors
License-Expression: MIT
License-File: LICENSE
Keywords: calories,cli,fitbit,google-health,health,macros,nutrition,sleep,weight
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: System :: Monitoring
Classifier: Topic :: Utilities
Requires-Python: >=3.12
Requires-Dist: click-agentcli>=0.4.1
Requires-Dist: click>=8.1
Requires-Dist: google-auth-oauthlib>=1.2.0
Requires-Dist: google-auth>=2.28.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: mealtime-nutrients>=0.3.0
Description-Content-Type: text/markdown

# Healthlog

Read every Google Health data type, and record food, weight and height.

```console
healthlog auth login
healthlog food log "Bean salad" --grams 350 --kcal 420 --protein 25 --fat 12 --carbs 48
healthlog food log --input meal.json
cat meal.json | healthlog food log --input -
healthlog food history
healthlog food history yesterday
healthlog food history 2026-08-17 2026-08-23
healthlog food duplicate POINT_ID --protein 0
healthlog food delete POINT_ID

healthlog weight log 82.4 --unit kg
healthlog weight log 181 --unit lb
healthlog weight history 2026-08-01 2026-08-27 --unit lb
healthlog weight delete POINT_ID
healthlog weight latest
healthlog height log 195 --unit cm
healthlog height latest
healthlog sleep history yesterday
healthlog types
```

Every data type is a noun and every noun reads the same way, so a caller that
can read one can read all of them. `healthlog types` lists them, and says which
accept a write. Food, weight and height are the three this version writes.

Every noun also takes `latest`, which reports the most recent reading however
old it is. A `history` answers a range, so a height recorded once in 2017 and
unchanged since reads as nothing at all when today is the range; `latest` asks
what the value is rather than what changed.

## Food

JSON input is a flat item carrying `name`, `meal_type`, optional `time`,
optional `grams`, and nutrient fields. Omit `time` to use the device's current
local time.
Every new entry needs `kcal`, `protein`, `fat`, and `carbs`; explicit zero is a
valid value. Use `--nutrient NAME=GRAMS` for another nutrient; names come from
`mealtime-nutrients`, the list the mealtime tools share, which holds exactly
one per nutrient. Dietary fibre is `fiber`, and carbohydrate has its own field,
so it is `carbs` and never `carbohydrates`. Explicit flags override the input.
Piped tool output keeps its `{"ok":true,"data":...}` envelope, and a field this
version has not heard of is dropped, so the other tools stay free to add one.
A bare JSON object is read as hand-written instead: an unrecognised key there
is an error, rather than a nutrient quietly left out of the entry.
`grams` is written to Google as a gram serving and survives reads. When
it is absent, the shared format treats the nutrients as a 100 g fallback.

An item's nutrients describe the weight that item states, so `--grams` may not
contradict it: piping a 100 g product and asking for 250 g is refused, because
it would relabel the nutrients rather than convert them. Ask the source for the
weight you mean, as with `pantry lookup --grams 250`, or state every nutrient
here yourself. Restating the four core macros is not an escape, and is not
accepted as one: an item carrying fibre or sugar would keep those at the old
weight. An item stating no weight has nothing to contradict, so `--grams`
records what was eaten; an Eatout meal is the usual case.

`food duplicate` always keeps the source and accepts the same overrides as
`food log`. To correct an entry, duplicate it with the correction, inspect the
result, then delete the source explicitly. JSON overrides may use `null` to
remove a value.

A duplicate reports the source it kept, and says it still counts. Two entries
for one meal are totalled twice, so a correction that stops after the copy
inflates the day; the delete is not optional caution, it is the second half of
the edit. Google Health does document a patch method for updating a point in
place, which would avoid the two steps, but it answers `500` for a nutrition
log, so correcting really is copy-then-delete.

Output carries `kcal`, `protein`, `fat` and `carbs` always, plus only the
nutrients the entry states; an absent key and a `null` mean the same, while an
explicit zero survives. Missing legacy Google core macros render as zero in
Healthlog output. Unstated nutrients are omitted from writes. `--dry-run
--json` shows the record without authenticating or writing.

`food history` totals the core macros always. Every other nutrient is totalled
only when an entry states it, over the entries that state it, so a total may
cover part of the range.

## Weight and height

`weight log` requires `--unit`, which takes `kg` or `lb`, the standard
symbols. There is no default, because a defaulted unit records 181 kg for
someone who meant 181 lb, and 181 kg is a weight a person can have, so nothing
downstream can catch it. Google Health stores grams either way, so the unit
chooses only how the figure is read, never what is kept.

`weight history` takes `--unit` too, defaulting to `kg`. A display unit may
default safely, because choosing one cannot change what is stored.

A reading outside 20-500 kg is refused. That guard is for a slipped digit and
nothing more: it cannot tell kilograms from pounds, which is why the unit is
required rather than guessed.

`height log` works the same way, taking `cm`, `m` or `in`, and refusing
anything outside 50-250 cm. Google Health states height in whole millimetres as
a string rather than a number, so the record it stores is `mm`. Height is the
clearest case for `latest`: it is recorded once and left, so a range read
covering only recent days reports none.

## Reading a range

Every `history` reads today by default. Pass one date for that day or two dates
for an inclusive range; dates may be ISO dates, `today`, or `yesterday`. Dates
become UTC bounds using the device's local timezone. Offset-aware ISO datetimes
are exact bounds; the end datetime is exclusive. Points are then compared only
in UTC.

Google Health spells its server-side time filter differently for nearly every
data type and rejects a wrong spelling outright, so the range is applied here
instead. Points arrive newest first, so a read stops at the first page holding
nothing new enough rather than walking a whole history.

Types other than food report each point as the API stated it, under `data`,
with only the time lifted out — samples state a `sampleTime`, intervals and
sessions an `interval`, and daily summaries a civil `date`. `--limit` caps a
dense type at 500 points by default; a capped read always says `truncated`, and
`--limit 0` reads every point in the range.

## Authentication

`healthlog auth login` asks for the read scope of every type in
`healthlog types`, so no future data type needs a second login to be read. It
also asks for four write scopes. Two are used now: one by food, one shared by
weight and height. The other two cover exercise and sleep, the only families
left that this tool could plausibly write, so growing into them costs no
re-login either. It asks for
nothing touching reproductive health, symptoms, mood, location, profile or
settings, because it handles none of that. Google refuses to refresh a
token for a scope it never granted, so a token from an earlier version keeps
working for what it does cover: `healthlog auth status` reports the scopes it
lacks, and a read it cannot do fails with a 403 naming the re-login.

OAuth tokens remain in `~/.config/healthlog/tokens.json` with mode `0600`.
