Skip to content

reservations

reservations #

Per-user time reservations — spread a crowd across candidate capture times.

Each user holds a distinct candidate time, stable while held and released once it lapses or a capture lands within grace of it.

DEFAULT_GRACE = timedelta(minutes=30) module-attribute #

How long a reservation outlives its time when no grace is passed.

ReservationBook #

Bases: ImpureState

The persistent book of reservations: each user's held instant.

held maps a user to the candidate time they currently hold. The struct IS the format: changing its shape starts a fresh book (no migration).

Source code in capturegraph-lib/capturegraph/scheduling/organize/reservations.py
class ReservationBook(cg.ImpureState):
    """The persistent book of reservations: each user's held instant.

    ``held`` maps a user to the candidate time they currently hold. The struct
    IS the format: changing its shape starts a fresh book (no migration).
    """

    held: cg.Map[cg.UserID, cg.Time]

    def reserve(
        self,
        user_id: str,
        among: Iterable[datetime],
        *,
        captured: datetime | None = None,
        now: datetime | None = None,
        grace: timedelta = DEFAULT_GRACE,
    ) -> datetime | None:
        """The time this user holds, leasing a fresh distinct one if they hold none.

        Args:
            user_id: The user asking for a time.
            among: The current candidate times (regenerated per call is fine).
            captured: The target's most recent capture (by any user), if known.
                Every reservation at or before ``captured + grace`` is released
                first, whoever holds it.
            now: The reference time; defaults to ``datetime.now()``. Pass it
                explicitly to make scheduling deterministic under test.
            grace: How long a reservation outlives its time. A time more than
                ``grace`` in the past is released, and a reservation snaps onto
                a candidate only when one lies within ``grace`` of it.

        Returns:
            The candidate this user holds (a member of ``among``), or ``None``
            when ``among`` is empty. When every candidate is already held the
            earliest is returned without recording a hold, so the answer in the
            oversubscribed case may drift between polls.
        """
        now = now if now is not None else datetime.now(UTC)
        held = _prune(_held(self), now=now, captured=captured, grace=grace)

        candidates = sorted(among)
        if not candidates:
            self._record(held)
            return None

        if user_id in held:
            snapped = _snap(candidates, held[user_id], grace=grace)
            if snapped is not None:
                self._record(held)
                return snapped
            held.pop(user_id, None)

        taken = {
            snapped
            for when in held.values()
            if (snapped := _snap(candidates, when, grace=grace)) is not None
        }
        for candidate in candidates:
            if candidate not in taken:
                held[user_id] = candidate
                self._record(held)
                return candidate

        self._record(held)
        return candidates[0]

    def _record(self, held: dict[str, datetime]) -> None:
        """Write ``held`` back as this book's map of reservations."""
        self.held = held_map({user_id: cg.Time(when) for user_id, when in held.items()})

reserve(user_id, among, *, captured=None, now=None, grace=DEFAULT_GRACE) #

The time this user holds, leasing a fresh distinct one if they hold none.

Parameters:

Name Type Description Default
user_id str

The user asking for a time.

required
among Iterable[datetime]

The current candidate times (regenerated per call is fine).

required
captured datetime | None

The target's most recent capture (by any user), if known. Every reservation at or before captured + grace is released first, whoever holds it.

None
now datetime | None

The reference time; defaults to datetime.now(). Pass it explicitly to make scheduling deterministic under test.

None
grace timedelta

How long a reservation outlives its time. A time more than grace in the past is released, and a reservation snaps onto a candidate only when one lies within grace of it.

DEFAULT_GRACE

Returns:

Type Description
datetime | None

The candidate this user holds (a member of among), or None

datetime | None

when among is empty. When every candidate is already held the

datetime | None

earliest is returned without recording a hold, so the answer in the

datetime | None

oversubscribed case may drift between polls.

Source code in capturegraph-lib/capturegraph/scheduling/organize/reservations.py
def reserve(
    self,
    user_id: str,
    among: Iterable[datetime],
    *,
    captured: datetime | None = None,
    now: datetime | None = None,
    grace: timedelta = DEFAULT_GRACE,
) -> datetime | None:
    """The time this user holds, leasing a fresh distinct one if they hold none.

    Args:
        user_id: The user asking for a time.
        among: The current candidate times (regenerated per call is fine).
        captured: The target's most recent capture (by any user), if known.
            Every reservation at or before ``captured + grace`` is released
            first, whoever holds it.
        now: The reference time; defaults to ``datetime.now()``. Pass it
            explicitly to make scheduling deterministic under test.
        grace: How long a reservation outlives its time. A time more than
            ``grace`` in the past is released, and a reservation snaps onto
            a candidate only when one lies within ``grace`` of it.

    Returns:
        The candidate this user holds (a member of ``among``), or ``None``
        when ``among`` is empty. When every candidate is already held the
        earliest is returned without recording a hold, so the answer in the
        oversubscribed case may drift between polls.
    """
    now = now if now is not None else datetime.now(UTC)
    held = _prune(_held(self), now=now, captured=captured, grace=grace)

    candidates = sorted(among)
    if not candidates:
        self._record(held)
        return None

    if user_id in held:
        snapped = _snap(candidates, held[user_id], grace=grace)
        if snapped is not None:
            self._record(held)
            return snapped
        held.pop(user_id, None)

    taken = {
        snapped
        for when in held.values()
        if (snapped := _snap(candidates, when, grace=grace)) is not None
    }
    for candidate in candidates:
        if candidate not in taken:
            held[user_id] = candidate
            self._record(held)
            return candidate

    self._record(held)
    return candidates[0]

held_entries(held) #

A book's held map as a plain {user_id: entry} dict (empty when unset).

Source code in capturegraph-lib/capturegraph/scheduling/organize/reservations.py
def held_entries[V](held: "cg.Map[cg.UserID, V]") -> dict[str, V]:
    """A book's ``held`` map as a plain ``{user_id: entry}`` dict (empty when unset)."""
    if cg.is_missing(held):
        return {}
    return held.to_dict()

held_map(entries) #

entries as the UserID-keyed map a book's state stores.

Source code in capturegraph-lib/capturegraph/scheduling/organize/reservations.py
def held_map[V](entries: dict[str, V]) -> "cg.Map[cg.UserID, V]":
    """``entries`` as the ``UserID``-keyed map a book's state stores."""
    return cast("cg.Map[cg.UserID, V]", cg.Map(entries))