Three decorator factories, the shape decorators,
unwrapped just walked ring by ring. @retry and @memo in particular are code
you will write for real around an LLM API call — retrying a flaky call with backoff, and not paying for
the same prompt twice. Eight checks grade whether your rings behave honestly: whether they retry the right
number of times, whether they still look like the function they wrap, and whether the cache actually
evicts in the order it claims to.
retry(times, backoff, on=(Exception,), sleep=time.sleep) — call the wrapped function; on any
exception in on, retry up to times more times (so at most times + 1
attempts total), sleeping backoff * 2 ** attempt between attempts. An exception NOT in
on propagates immediately, unretried. After the final attempt fails, re-raise it.
sleep exists purely so a test can inject a fake one instead of actually waiting.timed(sink) — call sink(fn.__name__, elapsed_seconds) exactly once after each
call to the wrapped function — a non-negative float — whether it returned or raised.memo(maxsize) — LRU-memoise the wrapped function. Keyword arguments given in a different
order must hit the SAME cache entry. When a new key would push the cache past maxsize
entries, evict the LEAST RECENTLY USED one — any access, a hit or a fresh insert, counts as "used".The checks are ordinary Python and ship with the page like everything else on a static site — you could read them in devtools if you wanted. They assert what your decorators do on real calls (an always-failing function, an injected fake clock, kwargs given two different ways), not how you write the code to do it.
while True: loop with an attempt counter, starting at 0. Wrap
the call in try/except on as exc:; if attempt >= times, do a bare
raise to keep the original traceback; otherwise call sleep(backoff * 2 ** attempt)
and increment attempt before looping again. Don't forget
@functools.wraps(fn) on the innermost wrapper.start = time.perf_counter() before the call, then a
try/finally so sink still fires when the call raises; the elapsed time is
time.perf_counter() - start either way.collections.OrderedDict as the cache, closed over by the wrapper. Build
the key as (args, tuple(sorted(kwargs.items()))) — sorting is what makes kwarg order not
matter. On a hit, cache.move_to_end(key) before returning. On a miss, compute, store, move the
new entry to the end too, then cache.popitem(last=False) if the cache is now over
maxsize — that pops the LEAST recently touched entry, because every touch (hit or insert)
just moved something else to the end.