mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-10 19:07:15 +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
108 lines
3.7 KiB
Python
108 lines
3.7 KiB
Python
"""CLI handler for the ``averages`` subcommand.
|
|
|
|
Split out from :mod:`diet_guard._cli` for the same reason
|
|
:mod:`diet_guard._cli_sync` is: that module is already close to the repo's
|
|
500-line cap.
|
|
|
|
The daily budget is printed here on purpose. It is "hidden" only in the sense
|
|
of never leaving this machine (see :mod:`diet_guard._constants`), and an
|
|
average intake with no yardstick beside it is unreadable -- "2310 kcal/day,
|
|
slightly over" invites the obvious "over *what*?". The MCP server's
|
|
equivalent tool deliberately makes the opposite trade and returns the band
|
|
alone; see :mod:`diet_guard._mcp`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from diet_guard._averages import band_label
|
|
from diet_guard._averages_report import monthly_average, weekly_average
|
|
from diet_guard._budget import (
|
|
BudgetFileCorruptError,
|
|
BudgetNotInitializedError,
|
|
current_schedule,
|
|
daily_budget,
|
|
)
|
|
from diet_guard._state import load_log
|
|
|
|
if TYPE_CHECKING:
|
|
import argparse
|
|
from collections.abc import Callable
|
|
|
|
from diet_guard._averages import PeriodAverage
|
|
from diet_guard._budget_history import BudgetSchedule
|
|
|
|
# Width of the leading period label, so the numbers line up in a column.
|
|
_LABEL_WIDTH = 11
|
|
|
|
|
|
def register_averages_subparser(sub: argparse._SubParsersAction) -> None:
|
|
"""Register the ``averages`` subcommand on ``sub``."""
|
|
sub.add_parser(
|
|
"averages",
|
|
help="Average kcal/day this and last week, and this and last month.",
|
|
)
|
|
|
|
|
|
def _format_period(label: str, period: PeriodAverage) -> str:
|
|
"""Render one period as a single aligned line.
|
|
|
|
Args:
|
|
label: The period's name, e.g. ``"this week"``.
|
|
period: The computed average.
|
|
|
|
Returns:
|
|
A line ending in the coverage count, or a short "nothing yet" line
|
|
when the period has no logged day to average.
|
|
"""
|
|
head = f"{label:<{_LABEL_WIDTH}} {period.start}..{period.end}"
|
|
if period.avg_kcal is None or period.avg_budget is None:
|
|
return f"{head} no logged days yet"
|
|
return (
|
|
f"{head} {period.avg_kcal:.0f} kcal/day - "
|
|
f"{band_label(period.band)} (budget {period.avg_budget:.0f}) "
|
|
f"[{period.logged_days}/{period.elapsed_days} days logged]"
|
|
)
|
|
|
|
|
|
def _emit_periods(emit: Callable[[str], None], schedule: BudgetSchedule) -> None:
|
|
"""Print the four period lines against ``schedule``."""
|
|
log = load_log()
|
|
rows = (
|
|
("this week", weekly_average(log, schedule=schedule, weeks_ago=0)),
|
|
("last week", weekly_average(log, schedule=schedule, weeks_ago=1)),
|
|
("this month", monthly_average(log, schedule=schedule, months_ago=0)),
|
|
("last month", monthly_average(log, schedule=schedule, months_ago=1)),
|
|
)
|
|
for label, period in rows:
|
|
emit(_format_period(label, period))
|
|
|
|
|
|
def cmd_averages(emit: Callable[[str], None]) -> int:
|
|
"""Print weekly and monthly average intake and its budget band.
|
|
|
|
Every period ends at *yesterday*: today is still being logged, and half a
|
|
day of entries would drag the mean far enough to flip the band (see
|
|
:mod:`diet_guard._averages`).
|
|
|
|
Args:
|
|
emit: A one-line output sink (``_cli._emit``, passed in rather than
|
|
imported so this module has no reach-in dependency on ``_cli``).
|
|
|
|
Returns:
|
|
0 once the periods are printed, 1 when no usable budget exists to
|
|
judge them against.
|
|
"""
|
|
try:
|
|
budget = daily_budget()
|
|
except BudgetNotInitializedError:
|
|
emit("budget not set - run: python -m diet_guard init")
|
|
return 1
|
|
except BudgetFileCorruptError:
|
|
emit("budget file corrupt - re-run init")
|
|
return 1
|
|
emit("averages exclude today, which is still being logged.")
|
|
_emit_periods(emit, current_schedule(default=budget))
|
|
return 0
|