judgevet.domain.spend

Status: draft.

judgevet.domain.spend

Hold the opt-in cap on attempts and settled input tokens a caller may spend.

A SpendCap is opt-in. A port without one sends every attempt its retry policy permits. A port with one claims a slot before each physical attempt, including every retry, and refuses with JevBudgetExceededError when a limit is already reached. Nothing is sent on a refusal. The HTTP adapters and the offline fakes in judgevet.testing accept the same object.

Each attempt counts, failed or not. The AWS SDK retry quota also charges failed retries. Source: https://docs.aws.amazon.com/sdkref/latest/guide/feature-retry-behavior.html. Input tokens settle from the response usage.input_tokens after the attempt. A failed attempt, or a response without that count, settles zero. Whether the service bills a failed attempt is an open question on #56.

The counters use threading.Lock, because both the sync and async adapters share them and an asyncio.Lock guards one event loop only. The lock is held for the counter update alone, never across an await. Source: https://docs.python.org/3/library/asyncio-sync.html.

The attempt limit is hard. The token limit is checked before sending, so attempts already in flight can settle past it.

Examples:

from judgevet.domain.errors import JevBudgetExceededError
from judgevet.domain.spend import SpendCap

cap = SpendCap(max_attempts=1)
cap.claim()
try:
    cap.claim()
except JevBudgetExceededError as error:
    assert (error.limit, error.cap, error.spent) == ("attempts", 1, 1)
See Also

SpendCap

Hold lock-guarded attempt and input-token counters that never reset.

Every adapter given the same object draws from the same counters. A limit left as None does not bound that counter.

Attributes:

Name Type Description
max_attempts int | None

Physical attempts allowed, retries included.

max_input_tokens int | None

Settled input tokens after which the cap refuses further attempts.

attempts int

Attempts claimed so far.

input_tokens int

Input tokens settled so far.

Examples:

cap = SpendCap(max_input_tokens=100)
cap.claim()
cap.settle(60)
assert (cap.attempts, cap.input_tokens) == (1, 60)

attempts property

Return the attempts claimed so far.

Returns:

Type Description
int

The attempt counter.

input_tokens property

Return the input tokens settled so far.

Returns:

Type Description
int

The input-token counter.

__init__(max_attempts=None, max_input_tokens=None)

Validate the limits and start both counters at zero.

Parameters:

Name Type Description Default
max_attempts int | None

Physical attempts allowed, or None for no bound.

None
max_input_tokens int | None

Input tokens allowed, or None for no bound.

None

Raises:

Type Description
ValueError

If a limit is not None and not a positive integer.

claim()

Claim one attempt slot, or refuse when a limit is already reached.

Raises:

Type Description
JevBudgetExceededError

If attempts or settled input tokens have reached their cap.

settle(input_tokens)

Add the input tokens one attempt reported.

Parameters:

Name Type Description Default
input_tokens int | None

The response usage.input_tokens; None adds zero.

required