Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from typing import Sequence
from .dates_add_days import add_days, days_between, parse_iso_date ← from dates.add-days ^1.0.0 · built alongside by fune
from .dates_add_months import add_months ← from dates.add-months ^1.0.0 · built alongside by fune
from .education_cpd_hours_types import CpdEntry, CpdProgress, CpdRequirement
def _is_int(value: object) -> bool:
return isinstance(value, int) and not isinstance(value, bool)
def cpd_hours(entries: Sequence[CpdEntry], requirement: CpdRequirement, on_date: str) -> CpdProgress:
"""CPD minutes logged in the period that applies on on_date, against the
requirement. Fixed period boundaries are anchor + k x period_months, each
worked out from the anchor so month-end clamping never accumulates."""
on = parse_iso_date(on_date)
required = requirement.required_minutes
months = requirement.period_months
anchor = requirement.anchor
if not _is_int(required) or required < 0:
raise ValueError("requiredMinutes must be a whole number of 0 or more, received %s" % (required,))
if not _is_int(months) or months < 1:
raise ValueError("periodMonths must be a whole number of 1 or more, received %s" % (months,))
if requirement.rolling:
if anchor is not None:
raise ValueError("a rolling period takes no anchor")
period_end = on_date
period_start = add_days(add_months(on_date, -months), 1)
else:
if anchor is None:
raise ValueError("a fixed period needs an anchor date")
a = parse_iso_date(anchor)
months_apart = (on.year - a.year) * 12 + (on.month - a.month)
k = months_apart // months
while add_months(anchor, k * months) > on_date:
k -= 1
while add_months(anchor, (k + 1) * months) <= on_date:
k += 1
period_start = add_months(anchor, k * months)
period_end = add_days(add_months(anchor, (k + 1) * months), -1)
logged = 0
counted = 0
for e in entries:
parse_iso_date(e.date)
if not _is_int(e.minutes) or e.minutes < 0:
raise ValueError("minutes must be a whole number of 0 or more, received %s on %s" % (e.minutes, e.date))
if period_start <= e.date <= period_end and e.date <= on_date:
logged += e.minutes
counted += 1
shortfall = max(0, required - logged)
return CpdProgress(
period_start=period_start,
period_end=period_end,
logged_minutes=logged,
entries_counted=counted,
required_minutes=required,
shortfall_minutes=shortfall,
met=shortfall == 0,
days_remaining=days_between(on_date, period_end),
)