diet-guard/diet_guard/_cli_init.py
Krzysztof kuhy Rudnicki e761926df4
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
file-length / file length <= 250 lines (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Has been cancelled
Tests / test (push) Has been cancelled
release-apk / release (APK) (push) Has been cancelled
Body tab: weight/body-fat logs, BMI, ideal weight, goal-based kcal target, workouts + Health Connect steps, week planner
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-28 09:03:03 +02:00

99 lines
3.2 KiB
Python

"""CLI handler for the ``init`` subcommand.
Split out of :mod:`diet_guard._cli` to hold the repo's 250-line cap, following
the same thin-per-subcommand-handler shape as ``_cli_gate.py`` (``gate``),
``_cli_log.py`` (``ate``) and ``_cli_sync.py`` (``sync``).
The biometrics prompted for here seed the Body profile too (height, sex, a
birth date estimated from the typed age, and today's weigh-in), so BMI and
ideal weight work straight after ``init``. Refine the birth date with
``diet_guard profile --birth``. See ``docs/DOCS-body.md``.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from diet_guard._body_store import set_profile, set_weight
from diet_guard._budget import write_budget
from diet_guard._budget_biometrics import Biometrics, compute_target_budget
from diet_guard._state import now_local
if TYPE_CHECKING:
from collections.abc import Callable
# Accepted answers for the sex prompt that map to the male BMR constant.
MALE_ANSWERS = {"m", "male"}
FEMALE_ANSWERS = {"f", "female"}
def read_init_inputs(
emit: Callable[[str], None],
ask: Callable[[str], str],
) -> tuple[Biometrics, float, float] | None:
"""Prompt for biometrics on stdin; return (bio, activity, deficit) or None.
Returns None (after printing why) on any unparsable or out-of-range input,
so a typo never sets a wrong budget.
"""
try:
weight = float(ask("weight in kg:"))
height = float(ask("height in cm:"))
age = float(ask("age in years:"))
sex_raw = ask("sex (m/f):").lower()
activity = float(
ask(
"activity factor "
"(1.2 sedentary / 1.375 light / 1.55 moderate / 1.725 active):",
),
)
deficit = float(ask("daily deficit in kcal (e.g. 200):"))
except ValueError:
emit("that was not a number; nothing was set.")
return None
if sex_raw in MALE_ANSWERS:
is_male = True
elif sex_raw in FEMALE_ANSWERS:
is_male = False
else:
emit('sex must be "m" or "f"; nothing was set.')
return None
bio = Biometrics(
weight_kg=weight,
height_cm=height,
age_years=age,
is_male=is_male,
)
return bio, activity, deficit
def cmd_init(
emit: Callable[[str], None],
ask: Callable[[str], str],
) -> int:
"""Compute the starting budget from biometrics and write it."""
inputs = read_init_inputs(emit, ask)
if inputs is None:
return 2
bio, activity, deficit = inputs
budget = compute_target_budget(
bio,
activity_factor=activity,
deficit_kcal=deficit,
)
write_budget(budget, weight_kg=bio.weight_kg)
today = now_local().date()
# Same month/day as today, so the estimated age is exact right now.
birth = today.replace(year=today.year - int(bio.age_years), day=min(today.day, 28))
set_profile(
birth=birth.isoformat(),
height_cm=bio.height_cm,
sex="m" if bio.is_male else "f",
)
set_weight(today.isoformat(), bio.weight_kg, "init")
emit(f"budget computed from your biometrics: {budget:g} kcal/day.")
emit("edit it any time from the gate's calendar tab or the phone app.")
return 0