screen-locker/screen_locker/_late_credit.py
Krzysztof kuhy Rudnicki 46393fa866
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
late credits: a credit at/after tonight's shutdown sets it to min(23:00, its own time + reward); grace floor removed (also on sick days)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 18:14:04 +02:00

104 lines
4.0 KiB
Python

"""A credit at or after tonight's shutdown counts from its own time.
Shutting down must never make a finished task impossible to submit (kuhy,
2026-10-17). The config is read-add-write, so a credit landing after the
shutdown used to add its minutes to a time already past: LeetCode at 19:00
after an 18:00 shutdown made 19:00, and the machine stayed locked. Now such
a credit moves tonight to ``its own time + its reward``, capped at the
ceiling -- only the ceiling (23:00) is hard.
The anchor is the credit's own time (a run's end, a ledger row's), never the
moment a sync applied it, so a late upload does not buy extra time. Past the
shutdown the grace and sick lifts are spent: the caller clears the lift.
"""
from __future__ import annotations
from datetime import date, datetime
import logging
import time
from typing import TYPE_CHECKING
from screen_locker._day import today_str
from screen_locker._earned import ceiling, hhmm
from screen_locker._grace_floor import save_lift, tonight_minutes
_logger = logging.getLogger(__name__)
def _now() -> float:
"""The clock a credit without its own time is stamped with."""
return time.time()
def late_tonight(
config: tuple[int, int, int], day: date, at: float, minutes: int
) -> int | None:
"""Tonight's new shutdown for a late credit, or ``None`` if it is not late.
Late means timed on ``day`` at or after tonight's shutdown: the check
script locks at ``now >= shutdown``, so a credit exactly on it is late.
"""
moment = datetime.fromtimestamp(at).astimezone()
if moment.date() != day:
return None
done = moment.hour * 60 + moment.minute
if done < tonight_minutes(config, day):
return None
return min(ceiling(day), done + minutes)
class LateCreditMixin:
"""Late credits and sick-day credits; composed into ShutdownMixin."""
if TYPE_CHECKING:
def _read_shutdown_config(self) -> tuple[int, int, int] | None: ...
def _write_shutdown_config(
self, mon_wed: int, thu_sun: int, morning_end: int, *, restore: bool = ...
) -> bool: ...
def _apply_sick_floor(self) -> bool: ...
def _raise_on_sick_day(self, minutes: int, at: float | None) -> bool:
"""A sick day's credit: raise to the derived target, never add.
The credit's row is already in the target (``_sick_floor``), so adding
its minutes on top would pay it twice; a late one still counts from
its own time. True even when nothing moved (capped, or already there):
the credit is accounted for, and a failed write is retried by the next
tick's sick pass.
"""
self._apply_sick_floor()
late = self._late_write(minutes, at)
return True if late is None else late
def _late_write(self, minutes: int, at: float | None) -> bool | None:
"""Write a late credit's shutdown; ``None`` when it is not late."""
day = date.fromisoformat(today_str())
try:
config = self._read_shutdown_config()
except (OSError, ValueError) as exc:
# The add path reads it again inside its own guard and fails there.
_logger.warning("Late credit: cannot read shutdown config (%s)", exc)
return None
if config is None:
return None # the add path reads it again and reports it
target = late_tonight(config, day, _now() if at is None else at, minutes)
if target is None:
return None
mon_wed, thu_sun, morning = config
if not self._write_shutdown_config(
max(mon_wed, target), max(thu_sun, target), morning, restore=True
):
_logger.warning("Late credit: failed to write shutdown config")
return False
save_lift(0) # past the shutdown, the floors' lift is spent
_logger.info(
"Late credit: shutdown %s -> %s (earned at or after tonight's).",
hhmm(tonight_minutes(config, day)),
hhmm(target),
)
return True