Skip to content

popoto.counters

popoto.counters

Durable named counters: the counter primitive recipes use (#630).

A counter is one Redis string holding an integer. increment is INCRBY and returns the running total; read is GET and reports 0 for a key that has never been touched. Both are atomic on the server, so concurrent writers from several processes always converge on the true sum.

The key string is the caller's contract. Recipes compose it themselves (f"{EVICTION_COUNTER_PREFIX}:{agent_id}:evicted" in :mod:popoto.recipes.default_memory) so the layout that MemoryService._read_counters() scans stays exactly where the recipe declares it. This module adds no prefix and rewrites no key.

Import by path (from popoto import counters); it is deliberately absent from the popoto package namespace.

increment(key, delta=1, *, model=None)

Add delta to the counter at key and return the new total.

Creates the key at delta when it does not exist (INCRBY semantics). delta may be 0 to read-through atomically.

model names the model whose state the counter reports (#759 M4): on a Postgres-bound model the counter is a row of that backend's popoto_counter table, so recording it needs no Redis. Without it, or on a Redis-bound model, it is the Redis string, as before.

Source code in src/popoto/counters.py
def increment(key: str, delta: int = 1, *, model: Optional[Any] = None) -> int:
    """Add ``delta`` to the counter at ``key`` and return the new total.

    Creates the key at ``delta`` when it does not exist (``INCRBY``
    semantics). ``delta`` may be ``0`` to read-through atomically.

    ``model`` names the model whose state the counter reports (#759 M4): on a
    Postgres-bound model the counter is a row of that backend's
    ``popoto_counter`` table, so recording it needs no Redis. Without it, or
    on a Redis-bound model, it is the Redis string, as before.
    """
    backend = _backend(model)
    if backend is not None:
        spec = cast(Any, model)._meta.spec
        return int(backend.field_call(spec, "_counter", "increment", key, delta))
    return int(get_REDIS_DB().incrby(key, delta))

read(key, *, model=None)

Current value of the counter at key, or 0 when absent (from model's backend when it is not Redis, as :func:increment).

Source code in src/popoto/counters.py
def read(key: str, *, model: Optional[Any] = None) -> int:
    """Current value of the counter at ``key``, or ``0`` when absent (from
    ``model``'s backend when it is not Redis, as :func:`increment`)."""
    backend = _backend(model)
    if backend is not None:
        spec = cast(Any, model)._meta.spec
        return int(backend.field_call(spec, "_counter", "read", key))
    raw = get_REDIS_DB().get(key)
    return int(raw) if raw is not None else 0