mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-10 19:27:14 +01:00
Ninth batch of the 250-line sweep. Every Python file in the repo is now under cap (over-cap count 25 -> 18; the 18 remaining are all Dart). _foodbank.py 365 -> 210 (+ _foodbank_search, _foodbank_rebuild) test_averages.py 351 -> 222 (+ test_averages_report.py) _estimator.py 325 -> 137 (+ _estimator_off.py) _budget.py 323 -> 227 (+ _budget_biometrics, _budget_derived) test_foodbank.py 311 -> 117 (+ test_foodbank_rebuild.py) _averages.py 298 -> 174 (+ _averages_periods, _averages_report) test_budget_history.py 288 -> 162 (+ test_budget_history_io.py) The split boundaries are constrained by which module owns each conftest-patched state path, not by line count. BUDGET_FILE stays in _budget, FOOD_BANK_FILE in _foodbank: the derived/search/rebuild halves reach the file only through their parent's private helpers. Two of the new modules had copied FOOD_BANK_FILE into their headers as an incidental import, which is exactly the leak the guard is for -- both cleaned. test_state_redirect.py now covers all five redirected paths (FOOD_LOG_FILE, BUDGET_FILE, BUDGET_HISTORY_FILE, FOOD_BANK_FILE, MANUAL_BANK_FILE) rather than just the food log, in both directions: only the owning module may name each constant, and each redirect demonstrably moves a real write off the live path. Two rewires fail-closed rather than silently: `_budget.budget_weight` and `_estimator.requests` were module-attribute references no import rewrite touches, and both raised AttributeError immediately. The latter appeared only after ruff removed the now-unused `requests` import from _estimator -- the import was load-bearing solely as a patch target. Verified: 707 Python tests / 100% branch coverage, 5 layout-fits tests under xvfb, check_patch_targets 0, pre-commit clean, ~/.local/share/diet_guard byte-identical. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RA9ynjqXPnexRQnR5oAuCC
81 lines
2.5 KiB
Python
81 lines
2.5 KiB
Python
"""Biometrics and the one-time budget computation seeded from them.
|
|
|
|
Split out of :mod:`._budget` to keep both files under the repo's 250-line
|
|
limit. This half is pure arithmetic -- Mifflin-St Jeor BMR, the activity and
|
|
goal multipliers -- and touches no file, which is why ``BUDGET_FILE`` stays
|
|
named by :mod:`._budget` alone.
|
|
|
|
The biometrics themselves are used once and discarded: only the computed
|
|
budget is ever persisted.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
# The floor a computed target can never go below, however aggressive the
|
|
# deficit. Defined here with the computation it guards rather than imported
|
|
# from _budget, which would make the dependency circular.
|
|
_MIN_SANE_BUDGET = 1200
|
|
|
|
__all__ = ["Biometrics", "compute_target_budget", "mifflin_st_jeor_bmr"]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Biometrics:
|
|
"""Body metrics that feed the Mifflin-St Jeor budget formula.
|
|
|
|
Grouped into one value object so the budget calculation stays under the
|
|
repo's five-argument lint ceiling and so the inputs travel together.
|
|
|
|
Attributes:
|
|
weight_kg: Body mass in kilograms.
|
|
height_cm: Height in centimetres.
|
|
age_years: Age in years.
|
|
is_male: True for the male BMR constant (+5), False for female (-161).
|
|
"""
|
|
|
|
weight_kg: float
|
|
height_cm: float
|
|
age_years: float
|
|
is_male: bool
|
|
|
|
|
|
def mifflin_st_jeor_bmr(bio: Biometrics) -> float:
|
|
"""Return resting metabolic rate via the Mifflin-St Jeor equation.
|
|
|
|
Args:
|
|
bio: The person's body metrics.
|
|
|
|
Returns:
|
|
Basal metabolic rate in kcal/day.
|
|
"""
|
|
base = 10.0 * bio.weight_kg + 6.25 * bio.height_cm - 5.0 * bio.age_years
|
|
return base + 5.0 if bio.is_male else base - 161.0
|
|
|
|
|
|
def compute_target_budget(
|
|
bio: Biometrics,
|
|
*,
|
|
activity_factor: float,
|
|
deficit_kcal: float,
|
|
) -> int:
|
|
"""Return the daily kcal target: TDEE minus a deficit, floored for safety.
|
|
|
|
TDEE (total daily energy expenditure) is the BMR scaled by an activity
|
|
factor; subtracting a deficit yields a target that drives gradual loss.
|
|
|
|
Args:
|
|
bio: The person's body metrics.
|
|
activity_factor: Multiplier for daily activity (e.g. 1.2 sedentary,
|
|
1.375 light, 1.55 moderate, 1.725 very active).
|
|
deficit_kcal: Calories subtracted from TDEE for weight loss.
|
|
|
|
Returns:
|
|
The target budget in kcal, never below ``_MIN_SANE_BUDGET``.
|
|
"""
|
|
bmr = mifflin_st_jeor_bmr(bio)
|
|
tdee = bmr * activity_factor
|
|
target = round(tdee - deficit_kcal)
|
|
return max(target, _MIN_SANE_BUDGET)
|