Client-Side Rate Limiting

The framework had RetryPolicy / CircuitBreaker (which recover from failures) and a FIFO work_queue, but nothing to shape the rate of calls — so a flow hammering an external API had no way to stay under a quota. This adds the two standard limiters plus a leading-edge throttle, all with an injectable clock so they are deterministic in tests (no real sleeping).

  • TokenBucket — a smooth rate with burst capacity (lazy refill).

  • SlidingWindowLimiter — a fixed call budget per rolling window (Cloudflare’s O(1) weighted-counter approximation).

  • throttle() — a decorator that fires a function at most once per interval.

Pure standard library (threading for the lock, time only as the default clock); imports no PySide6.

Headless API

from je_auto_control import TokenBucket, SlidingWindowLimiter, throttle

# 5 requests/second, bursts up to 10
bucket = TokenBucket(rate=5, capacity=10)
if bucket.try_acquire():
    call_api()                      # non-blocking: skip / queue if False
bucket.acquire()                    # or block until a token frees up

# at most 100 calls per 60s rolling window
window = SlidingWindowLimiter(limit=100, window_s=60)
if window.try_acquire():
    call_api()

@throttle(2.0)                      # fire at most once every 2 seconds
def on_event(payload):
    ...

TokenBucket.try_acquire takes tokens if available; acquire blocks (with an optional timeout); time_until_available reports the wait so a scheduler can pace itself. Every limiter accepts a clock= (and acquire a sleep=) so the whole thing is exercised in CI with a fake clock — no real delays.

Executor command

AC_rate_limit takes a limiter name plus rate / capacity / n and tries to take n tokens from that named token bucket (created on first use), returning {acquired, tokens, wait} so a flow can gate or defer an action. The same operation is exposed as the MCP tool ac_rate_limit and as a Script Builder command under Flow.