mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 20:27:13 +01:00
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
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
102 lines
3.6 KiB
Python
102 lines
3.6 KiB
Python
"""Live shutdown credit for counted gate earners: paid per unit, as it lands.
|
|
|
|
A flat earner (LeetCode, reading) pays once a day, stamped
|
|
``<name>_bonus_date``. A counted gate earner -- the Automation tutor, one
|
|
signed ledger row per verified 15-minute block -- pays per unit, so its stamp
|
|
is ``<name>_bonus_units``: the day, and how many units the config already
|
|
holds. The live pass pays only the difference, priced by
|
|
``Earner.shutdown_for`` so the uneven rung split (13/13/12/12) and the
|
|
``max_units`` cap come from the registry, never from here.
|
|
|
|
The workout is counted too, but screen-locker owns its log and
|
|
``WorkoutCreditMixin`` applies it; it is never in this pass.
|
|
|
|
State I/O stays in :mod:`screen_locker._shutdown_base`; these functions read
|
|
and update the state dict it hands over.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date
|
|
import logging
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from screen_locker._earned import earned_units, is_gate, span
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable, Iterable
|
|
|
|
import earned_time
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
def date_stamp(earner: earned_time.Earner) -> str:
|
|
"""The once-per-day key of a flat earner's live pass in the state file."""
|
|
return f"{earner.name}_bonus_date"
|
|
|
|
|
|
def units_stamp(earner: earned_time.Earner) -> str:
|
|
"""The per-day key of ``earner``'s applied units in the state file."""
|
|
return f"{earner.name}_bonus_units"
|
|
|
|
|
|
def applied_units(state: dict[str, Any], earner: earned_time.Earner, today: str) -> int:
|
|
"""Units of ``earner`` the config already holds today (0 if none stamped)."""
|
|
raw = state.get(units_stamp(earner))
|
|
if not isinstance(raw, dict) or raw.get("date") != today:
|
|
return 0
|
|
units = raw.get("units")
|
|
return units if isinstance(units, int) and units > 0 else 0
|
|
|
|
|
|
def reset_stamps(terms: Iterable[earned_time.Term], today: str) -> dict[str, object]:
|
|
"""The stamps for what a daily reset already wrote into the config.
|
|
|
|
A flat earner that paid is stamped with the date, a counted gate with
|
|
its units, so the live pass does not add either a second time today.
|
|
"""
|
|
stamps: dict[str, object] = {}
|
|
for t in terms:
|
|
if t.earner.kind == "flat" and t.shutdown_minutes:
|
|
stamps[date_stamp(t.earner)] = today
|
|
elif t.earner.kind == "counted" and is_gate(t.earner) and t.answer:
|
|
stamps[units_stamp(t.earner)] = {"date": today, "units": t.answer}
|
|
return stamps
|
|
|
|
|
|
def apply_counted_bonus(
|
|
state: dict[str, Any],
|
|
adjust: Callable[[int], bool],
|
|
earner: earned_time.Earner,
|
|
today: str,
|
|
) -> bool:
|
|
"""Push shutdown later by ``earner``'s units credited since the last pass.
|
|
|
|
``adjust`` is the mixin's ``_adjust_shutdown_time_by``. Updates ``state``
|
|
(the caller saves it) and returns True when it changed. An unreadable
|
|
ledger or key earns nothing and is logged.
|
|
"""
|
|
day = date.fromisoformat(today)
|
|
units = earned_units(earner, day)
|
|
if units is None:
|
|
_logger.warning(
|
|
"%s state could not be checked; no shutdown time for it", earner.label
|
|
)
|
|
return False
|
|
done = applied_units(state, earner, today)
|
|
if units <= done:
|
|
return False
|
|
minutes = earner.shutdown_for(units, day) - earner.shutdown_for(done, day)
|
|
if minutes > 0 and not adjust(minutes):
|
|
_logger.warning("%s bonus: failed to write shutdown config.", earner.label)
|
|
return False
|
|
state[units_stamp(earner)] = {"date": today, "units": units}
|
|
_logger.info(
|
|
"%s bonus: %d unit(s) today, +%s shutdown time.",
|
|
earner.label,
|
|
units,
|
|
span(minutes),
|
|
)
|
|
return True
|