mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 22:47:14 +01:00
sickdays: per-date pool, no cap or reason, add/remove today or later only, import_legacy for the migration. LWW state flip per date (no tombstones), Firebase RTDB sickdays-sync/devices/<uuid>/sick_days.json, HMAC audit trail. Readers fail closed (unreadable pool = not sick, logged); writers refuse a corrupt pool. Free-day cross-check via a lazily imported freedays predicate. earned_time: resolve(..., sick=True) floors the final shutdown at 20:00 (capped at the ceiling); gaming, terms and base unchanged. Result dataclasses moved to _resolution.py for the line cap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xfj1sxKH9VBPqpcAni8Jry
85 lines
2.4 KiB
Python
85 lines
2.4 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki
|
|
"""What :func:`earned_time.resolve` returns: the base, each term, the sum.
|
|
|
|
Split from :mod:`earned_time._resolve` (0.11.0) to keep both under the line
|
|
cap; ``earned_time._resolve`` still re-exports all three.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from datetime import date
|
|
|
|
from earned_time._policy import Earner
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Base:
|
|
"""The floor for one day, after every penalty in force on it.
|
|
|
|
Attributes:
|
|
gaming_minutes: Gaming time a day that earned nothing gets.
|
|
shutdown_minutes: Shutdown time of such a day, minutes after midnight.
|
|
"""
|
|
|
|
gaming_minutes: int
|
|
shutdown_minutes: int
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Term:
|
|
"""What one earner contributed.
|
|
|
|
Attributes:
|
|
earner: The earner.
|
|
answer: The consumer's answer: a unit count, or ``None`` for
|
|
"could not check" (and for no answer at all).
|
|
gaming_minutes: Gaming minutes earned.
|
|
shutdown_minutes: Shutdown minutes earned.
|
|
"""
|
|
|
|
earner: Earner
|
|
answer: int | None
|
|
gaming_minutes: int
|
|
shutdown_minutes: int
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Resolution:
|
|
"""One day's budget and shutdown, and what earned them.
|
|
|
|
Attributes:
|
|
day: The day resolved.
|
|
base: Its floor.
|
|
terms: One entry per registered earner, in registry order.
|
|
gaming_minutes: Base plus terms, capped at the gaming ceiling.
|
|
shutdown_minutes: Base plus terms, capped at the shutdown ceiling;
|
|
on a sick day raised to the sick floor (:mod:`earned_time._sick`).
|
|
sick: Whether the day was resolved as a sick day (0.11.0).
|
|
sick_lift_minutes: How far the sick floor raised ``shutdown_minutes``
|
|
above base plus terms (``0`` unless sick): a consumer adding live
|
|
credits on top pays them out of this first, as with a grace lift.
|
|
"""
|
|
|
|
day: date
|
|
base: Base
|
|
terms: tuple[Term, ...]
|
|
gaming_minutes: int
|
|
shutdown_minutes: int
|
|
sick: bool = False
|
|
sick_lift_minutes: int = 0
|
|
|
|
def term(self, name: str) -> Term:
|
|
"""The term of the earner called ``name``.
|
|
|
|
Raises:
|
|
KeyError: No such earner was resolved.
|
|
"""
|
|
for candidate in self.terms:
|
|
if candidate.earner.name == name:
|
|
return candidate
|
|
raise KeyError(name)
|