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
- judgevet.adapters.outbound.spend: Wraps adapter attempts in the cap.
- judgevet.testing: Offline fakes that accept
spend_cap=. - judgevet.domain.errors.JevBudgetExceededError: The refusal error.
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 |
required |