Thread configured horizon_days (Feature 07 integration fix) (#86)
This commit was merged in pull request #86.
This commit is contained in:
@@ -44,7 +44,8 @@ definition edits / reassignment (Features 06/07); :func:`regenerate_for_child`
|
||||
covers a single child's presence change (Feature 09). Both delete the
|
||||
affected open instances at or after today — never a completed instance,
|
||||
never the past — and re-run the walk over the rolling horizon
|
||||
``[today, today + DEFAULT_HORIZON_DAYS]``.
|
||||
``[today, today + horizon_days]`` (``horizon_days`` defaults to
|
||||
``DEFAULT_HORIZON_DAYS``).
|
||||
|
||||
HOOK (Feature 09 presence services): the presence business layer that
|
||||
lands in Feature 09 MUST call :func:`regenerate_for_child` after EVERY
|
||||
@@ -83,6 +84,31 @@ def _now_stamp() -> str:
|
||||
)
|
||||
|
||||
|
||||
def _resolve_horizon_days(horizon_days: int | None) -> int:
|
||||
"""Validate a regeneration horizon and return the resolved day count.
|
||||
|
||||
``None`` resolves to :data:`~.const.DEFAULT_HORIZON_DAYS`. A bool
|
||||
(SQLite would happily bind ``True``/``False`` onto an int), a non-int,
|
||||
or a value below 1 raises ValueError naming the field — the validation
|
||||
runs BEFORE any future-instance delete so a malformed horizon can never
|
||||
delete rows it then fails to rebuild. A negative horizon would
|
||||
otherwise delete first and only then be rejected by
|
||||
:func:`materialize`'s inverted-range check; ``0``/``False`` would
|
||||
silently shrink the window to a single day.
|
||||
"""
|
||||
if horizon_days is None:
|
||||
return DEFAULT_HORIZON_DAYS
|
||||
if (
|
||||
isinstance(horizon_days, bool)
|
||||
or not isinstance(horizon_days, int)
|
||||
or horizon_days < 1
|
||||
):
|
||||
raise ValueError(
|
||||
f"horizon_days must be a positive integer, got {horizon_days!r}"
|
||||
)
|
||||
return horizon_days
|
||||
|
||||
|
||||
def _decode_rule(snapshot) -> ScheduleRule:
|
||||
"""Decode a snapshot's rule record back into a ScheduleRule.
|
||||
|
||||
@@ -232,6 +258,7 @@ async def regenerate_for_definition(
|
||||
definition_id: int,
|
||||
*,
|
||||
today: datetime.date | None = None,
|
||||
horizon_days: int | None = None,
|
||||
) -> int:
|
||||
"""Regenerate a definition's future instances after a config change.
|
||||
|
||||
@@ -241,7 +268,7 @@ async def regenerate_for_definition(
|
||||
completed instance, never the past) via
|
||||
:meth:`~.dao_instances.QuestInstancesDao.delete_future_uncompleted`,
|
||||
then the materialization walk re-runs over the rolling horizon
|
||||
``[today, today + DEFAULT_HORIZON_DAYS]`` so the (changed) rule,
|
||||
``[today, today + horizon_days]`` so the (changed) rule,
|
||||
assignee set and windows re-materialize against today's state.
|
||||
|
||||
"today" is computed the same way the walk computes it —
|
||||
@@ -251,14 +278,18 @@ async def regenerate_for_definition(
|
||||
range. ``today`` optionally pins a caller-resolved HA-local date
|
||||
(threaded into both the delete cutoff and the re-materialization)
|
||||
so a household time zone behind the host around midnight stays
|
||||
consistent. Returns the number of instances the re-materialization
|
||||
consistent. ``horizon_days`` sizes the re-materialization window;
|
||||
when omitted it falls back to :data:`~.const.DEFAULT_HORIZON_DAYS`.
|
||||
A bool, non-int, or sub-1 value raises ValueError BEFORE any delete.
|
||||
Returns the number of instances the re-materialization
|
||||
upserted across the whole snapshot (idempotent — other active
|
||||
definitions' tuples refresh in place, never duplicate).
|
||||
"""
|
||||
horizon = _resolve_horizon_days(horizon_days)
|
||||
today_date = _resolve_today(today)
|
||||
start_date = today_date.isoformat()
|
||||
end_date = (
|
||||
today_date + datetime.timedelta(days=DEFAULT_HORIZON_DAYS)
|
||||
today_date + datetime.timedelta(days=horizon)
|
||||
).isoformat()
|
||||
|
||||
instances = QuestInstancesDao(database)
|
||||
@@ -286,6 +317,7 @@ async def regenerate_for_child(
|
||||
child_id: int,
|
||||
*,
|
||||
today: datetime.date | None = None,
|
||||
horizon_days: int | None = None,
|
||||
) -> int:
|
||||
"""Regenerate a child's future instances after a presence change.
|
||||
|
||||
@@ -299,7 +331,7 @@ async def regenerate_for_child(
|
||||
(never a completed instance, never the past) via
|
||||
:meth:`~.dao_instances.QuestInstancesDao.delete_future_uncompleted_for_child`,
|
||||
then the materialization walk re-runs over the rolling horizon
|
||||
``[today, today + DEFAULT_HORIZON_DAYS]`` scoped to that child only,
|
||||
``[today, today + horizon_days]`` scoped to that child only,
|
||||
so the child's now-absent/present dates re-materialize against
|
||||
today's presence WITHOUT touching any other child's rows.
|
||||
|
||||
@@ -315,13 +347,17 @@ async def regenerate_for_child(
|
||||
execution-day anchor even when the call is queued across midnight.
|
||||
``today`` optionally pins a caller-resolved HA-local date, threaded
|
||||
into the delete cutoff and the re-materialization for the same
|
||||
host-vs-HA time-zone consistency. Returns the number of instances
|
||||
host-vs-HA time-zone consistency. ``horizon_days`` sizes the
|
||||
re-materialization window; when omitted it falls back to
|
||||
:data:`~.const.DEFAULT_HORIZON_DAYS`. A bool, non-int, or sub-1 value
|
||||
raises ValueError BEFORE any delete. Returns the number of instances
|
||||
the re-materialization upserted for the child across the whole
|
||||
snapshot.
|
||||
"""
|
||||
if isinstance(child_id, bool) or not isinstance(child_id, int):
|
||||
raise ValueError(f"child_id must be an integer, got {child_id!r}")
|
||||
|
||||
horizon = _resolve_horizon_days(horizon_days)
|
||||
today_date = _resolve_today(today)
|
||||
|
||||
instances = QuestInstancesDao(database)
|
||||
@@ -335,7 +371,7 @@ async def regenerate_for_child(
|
||||
child_id, today_date.isoformat(), today=today
|
||||
)
|
||||
start = datetime.date.fromisoformat(effective_cutoff)
|
||||
end_date = (start + datetime.timedelta(days=DEFAULT_HORIZON_DAYS)).isoformat()
|
||||
end_date = (start + datetime.timedelta(days=horizon)).isoformat()
|
||||
return await materialize(
|
||||
database, start.isoformat(), end_date, child_ids=[child_id], today=today
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user