"""Read the workday-stick penalty wake-alarm signed for today. A missed Tuesday/Wednesday/Thursday wake-alarm ring costs tomorrow's relaxed-day leniency (:func:`screen_locker._weekly_check.is_relaxed_day`) instead of just resetting to normal. This module never re-derives that policy: the file carries a signed ``penalty_date``, and the only decision here is *HMAC ok, dated today*. Anything else -- missing file, stale date, bad signature -- is "no penalty", i.e. today's ordinary relaxed-day rule. The file is void when EITHER its ``issued_date`` (the day of the missed ring) belongs to a sick day whose ``alarm`` switch is off, OR its ``penalty_date`` belongs to a sick day whose ``workout`` lock is off (``sickdays``). Each date is judged by what it owns: the ring is the alarm's, and the penalty it costs -- the relaxed-day leniency -- is the workout lock's. A sick day that keeps that locker leaves its half of the penalty in force. Wake-alarm's own retraction of the file can lag its sync by ~15 min, so it is not relied on here. """ from __future__ import annotations from datetime import date import json import logging from typing import TYPE_CHECKING from gatelock.log_integrity import verify_entry_hmac import sickdays from screen_locker._constants import WORKDAY_PENALTY_FILE from screen_locker._day import today_str from screen_locker._sick import stands_down if TYPE_CHECKING: from datetime import datetime _logger = logging.getLogger(__name__) def _read_verified() -> dict[str, object] | None: """The file's entry when it exists, parses and verifies; else None.""" if not WORKDAY_PENALTY_FILE.exists(): _logger.debug("No workday penalty file at %s", WORKDAY_PENALTY_FILE) return None try: entry = json.loads(WORKDAY_PENALTY_FILE.read_text()) except (OSError, json.JSONDecodeError) as exc: _logger.warning("Cannot read %s: %s", WORKDAY_PENALTY_FILE, exc) return None if not isinstance(entry, dict) or not verify_entry_hmac(entry): _logger.warning("Workday penalty file is not a signed object") return None return entry def workday_penalty_today(now: datetime | None = None) -> bool: """Whether today lost its relaxed-day leniency to a missed prior ring.""" entry = _read_verified() if entry is None or entry.get("penalty_date") != today_str(now): return False for field, locker in ( ("issued_date", sickdays.LOCKER_ALARM), ("penalty_date", sickdays.LOCKER_WORKOUT), ): if _stands_down_iso(entry.get(field), locker): _logger.warning( "Workday penalty void: its %s is a sick day with %s off", field, locker ) return False return True def _stands_down_iso(value: object, locker: str) -> bool: """Whether ``value`` is a ``YYYY-MM-DD`` on which ``locker`` is switched off.""" if value is None: return False # an older file without issued_date: penalty_date rules try: day = date.fromisoformat(str(value)) except ValueError: _logger.warning("Workday penalty: %r is not a date; not a sick day", value) return False return stands_down(locker, day)