✅ fresh
last synced 2026-10-24T20:09:49.281395+00:00 · coverage 100% (
absence)Validation by Beadloom
doc_sync— same source assync-check.
Absence
The owner's absences (vacation, sick leave, day off) as day ranges, and the decisions over a set of them: which covers a day, which comes next, the id a new one gets, whether two share a day; and the book the store keeps them in.
Source
src/horizon_tracker/domain/absence.py
Specification
Purpose
The production calendar does not know that the owner is on leave. An absence is the second kind of day the service does not work on: the owner enters it through the bot, and a run on a day it covers does not go ahead. This module holds the absence value and the four decisions the store, the bot and the runs take over a set of absences. It knows neither the file the absences are kept in nor today's date: every function takes the day as an argument.
How it works
AbsenceKindnames what the owner is absent for:VACATION,SICK_LEAVE,DAY_OFF.Absence(id, kind, first_day, last_day)is one range of whole days, both ends included; a one-day absence hasfirst_day == last_day.covers(day)answers whether a day falls inside it.covering(absences, day)returns the absence that covers the day, orNone. When several cover it, the lowest id wins, so the answer does not depend on the order the store returned them in.next_after(absences, day)returns the absence that starts soonest after the day, not on it, orNone. A tie on the first day goes to the lowest id. An absence that has started already is not "next":coveringanswers for the day itself.allocate_id(absences, *, last_issued=0)returns the id of a new absence: one above both the highest id held andlast_issued, the highest id ever handed out. The ids stay small integers the owner can type in a chat. Withlast_issuedkept beside the absences, an id whose absence was deleted never comes back under a new meaning. With the default0, the answer is the highest id held plus one, which reuses the id of a deleted absence that held the highest one.overlaps(first, second)is true when the two share at least one day, and the order of the arguments does not matter. Overlapping absences are refused by the caller, not merged.AbsenceBook(absences, last_issued)is what theAbsenceStoreport loads and saves: the absences in the order they were saved, and the highest id ever issued. The empty book,AbsenceBook(), holds nothing and has issued nothing.AbsenceBook.of(absences)builds a book whoselast_issuedis the highest id held, which is how a file written without the field reads.next_id()isallocate_id(absences, last_issued=last_issued), the id the next absence entered gets.
Invariants
- An absence's
last_dayis never before itsfirst_day, and its id is at leastFIRST_ID(1); the constructor raisesValueErrorotherwise. coveringandnext_afterare deterministic over a set: the lowest id breaks every tie.allocate_idnever returns an id at or belowlast_issuedor any id held, and refuses a negativelast_issuedwithValueError.overlaps(a, b) == overlaps(b, a), and an absence overlaps itself.- An
AbsenceBookholds no id twice, and itslast_issuedis neither negative nor below an id it holds; the constructor raisesValueErrorotherwise, so a book that could reissue an id cannot be built. - No function reads the clock, a file or the network.
API
Module src/horizon_tracker/domain/absence.py:
FIRST_ID = 1— the id the first absence gets.AbsenceKind—StrEnum:VACATION("vacation"),SICK_LEAVE("sick_leave"),DAY_OFF("day_off").Absence(id: int, kind: AbsenceKind, first_day: date, last_day: date)— frozen;covers(day: date) -> bool.covering(absences: Iterable[Absence], day: date) -> Absence | None.next_after(absences: Iterable[Absence], day: date) -> Absence | None.allocate_id(absences: Iterable[Absence], *, last_issued: int = 0) -> int.overlaps(first: Absence, second: Absence) -> bool.AbsenceBook(absences: tuple[Absence, ...] = (), last_issued: int = 0)— frozen; classmethodof(absences: Iterable[Absence]) -> AbsenceBook;next_id() -> int.
Testing
Tests: tests/unit/domain/test_absence.py. The module is inside the mutation gate.
Scenarios: tests/acceptance/features/absences.feature holds the PRD HT-012 scenarios of the bot and the runs, every one of them real. Those of the runs read this module through covering:
A morning run on a day of vacation is skipped with the kind namedAn evening run on a skipped day sends nothingA day after the absence ends is a working day again
Dependencies
Derived from the imports on reindex.
- Depends on: (none)
- Used by:
absence-service,absence-store,bot-commands,ports,probe,run-report,windowed-run
Parent
domain — domain README