mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 20:07:18 +01:00
The three Python packages pinned ruff twice at different versions: requirements.txt at 0.16.7, .pre-commit-config.yaml at v0.15.2. Only the older one ever ran, so `select = ["ALL"]` was enforced two minor versions behind. Measured before aligning: v0.15.2 reports 0 findings, every 0.16.x reports CPY001 on each file -- 70 crdt-sync, 23 freedays, 71 gatelock, and no other rule. Resolved by adding the notice rather than ignoring the rule, so the gate keeps its teeth. Two gatelock modules sat at exactly the 250-line cap, so a one-line header tipped them to 251. Both are split along a real seam rather than shaved: _widget_fills.py colour arithmetic over LockConfig, no tkinter import _surfaces_focus.py selects among existing surfaces, never mutates them The focus split keeps _surfaces.py's documented containment (it stays the only module that creates, moves or destroys windows), and the static guard in test_recovery_invariants.py globs _surfaces*.py, so the new sibling is covered with no list to extend. widgets.py re-exports the fills helpers, which keeps gatelock.widgets the single public surface its tests import. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019kMvSMtsVMB3d2uoZ56pbp
72 lines
2.6 KiB
Python
72 lines
2.6 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki
|
|
"""Calendar-year accounting for the pool.
|
|
|
|
One global pool, shared by every app: a day is either free everywhere or
|
|
free nowhere. The budget resets on Jan 1 and unused days do not carry over,
|
|
which is the employer-leave model people already have in their heads --
|
|
deliberately *not* a rolling window, because "how many do I have left"
|
|
should be answerable without knowing what you did eleven months ago.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from freedays._constants import DEFAULT_ANNUAL_BUDGET
|
|
from freedays._model import is_consumed_record, is_free_record, to_free_day
|
|
|
|
if TYPE_CHECKING:
|
|
from datetime import date
|
|
|
|
from crdt_sync import Log
|
|
|
|
|
|
def free_days_in_year(log: Log, year: int) -> list[date]:
|
|
"""Return every currently-free date in ``year``, ascending."""
|
|
days: list[date] = []
|
|
for record in log.values():
|
|
if not is_free_record(record):
|
|
continue
|
|
free_day = to_free_day(record)
|
|
if free_day is not None and free_day.day.year == year:
|
|
days.append(free_day.day)
|
|
return sorted(days)
|
|
|
|
|
|
def spent_days_in_year(log: Log, year: int) -> list[date]:
|
|
"""Every date in ``year`` that counts against the budget, ascending.
|
|
|
|
Two kinds count: days still marked free, and days that were released
|
|
*after* they had already arrived. The second kind is why this is not
|
|
simply :func:`free_days_in_year` -- taking Monday off and clearing the
|
|
entry on Tuesday does not give Monday back.
|
|
"""
|
|
days: list[date] = []
|
|
for record in log.values():
|
|
free_day = to_free_day(record)
|
|
if free_day is None or free_day.day.year != year:
|
|
continue
|
|
if is_free_record(record) or is_consumed_record(record):
|
|
days.append(free_day.day)
|
|
return sorted(days)
|
|
|
|
|
|
def spent(log: Log, year: int) -> int:
|
|
"""How many of ``year``'s free days are gone -- claimed or already used."""
|
|
return len(spent_days_in_year(log, year))
|
|
|
|
|
|
def remaining(log: Log, year: int, *, budget: int = DEFAULT_ANNUAL_BUDGET) -> int:
|
|
"""How many free days ``year`` has left, never below zero.
|
|
|
|
Clamped at zero so a budget lowered after the fact -- or a log merged
|
|
from a device that had a larger one -- reads as "none left" rather than
|
|
as a negative number that some caller will eventually treat as a count.
|
|
"""
|
|
return max(0, budget - spent(log, year))
|
|
|
|
|
|
def is_exhausted(log: Log, year: int, *, budget: int = DEFAULT_ANNUAL_BUDGET) -> bool:
|
|
"""Whether ``year`` has no free days left to claim."""
|
|
return remaining(log, year, budget=budget) <= 0
|