Low Concurrency Counter

Sun, 30 October 2016

__init__.py
"""Low Concurrency Counters
A simple counter library
"""
from google.appengine.ext import ndb


class LowConcurrencyCounterService(object):

    @staticmethod
    def get(name):
        """Retrieve a single named counter
        :param str|unicode name:  Name of counter to be retrieved
        :rtype: LowConcurrencyCounterModel
        """
        return LowConcurrencyCounterService.__get_or_insert(name)

    @staticmethod
    def get_multi(names):
        """Retrieve multiple named counters
        :param list:  Names of counters to be retrieved
        :rtype: list
        """
        if len(names) < 1:
            return []
        return LowConcurrencyCounterService.__get_or_insert_multi(names)

    @staticmethod
    def delete(name):
        """Delete a single named counter
        :param str|unicode name:  Name of counter to be deleted
        :rtype: None
        """
        model = LowConcurrencyCounterService.__get_or_insert(name, False)
        model.key.delete()
        return None

    @staticmethod
    def reset(name):
        """Reset a given counter to 0. if the counter does not exist, a new counter will be created.
        :param str|unicode name: Name of counter to be reset.
        :rtype: LowConcurrencyCounterModel
        """
        model = LowConcurrencyCounterService.__get_or_insert(name)
        if model is None:
            return None
        if model.count == 0:
            return model
        model.count = 0
        model.put()
        return model

    @staticmethod
    def incr(name, delta=1):
        """Increment a named counter by the delta
        :param str name: Name of counter to be incremented.
        :param int delta: Non-negative integer value (int or long) to increment key by, defaulting to 1.
        :rtype: LowConcurrencyCounterModel
        Raises:
          ValueError: If number is negative.
          TypeError: If delta isn't an int or long.
        """
        if not isinstance(delta, (int, long)):
            raise TypeError('Delta must be an integer or long, received %r' % delta)
        if delta < 0:
            raise ValueError('Delta must not be negative.')

        model = LowConcurrencyCounterService.__get_or_insert(name)
        if model is None:
            return None
        model.count += delta
        model.put()
        return model

    @staticmethod
    def decr(name, delta=1):
        """Decrement a named counter by the delta.
        :param str name: Name of counter to be decremented.
        :param int delta: Non-negative integer value (int or long) to decrement key by, defaulting to 1.
        :rtype: LowConcurrencyCounterModel
        :raises: ValueError
        Raises:
          ValueError: If number is negative.
          TypeError: If delta isn't an int or long.
        """
        if not isinstance(delta, (int, long)):
            raise TypeError('Delta must be an integer or long, received %r' % delta)
        if delta < 0:
            raise ValueError('Delta must not be negative.')

        model = LowConcurrencyCounterService.__get_or_insert(name)
        if model is None:
            return None
        model.count -= delta
        model.put()
        return model

    @staticmethod
    def __get_or_insert(name, use_get_or_insert=True):
        """Get or create a named counter
        :param str name: Name of counter to be processed.
        :param bool use_get_or_insert: Optional, default True. Whether to create if not exists.
        :rtype: LowConcurrencyCounterModel
        """
        if use_get_or_insert is False:
            return LowConcurrencyCounterModel.get_by_id(unicode(name))

        return LowConcurrencyCounterModel.get_or_insert(
            unicode(name),
            count=0
        )

    @staticmethod
    def __get_or_insert_multi(names):
        """Get or create multiple named counters
        :param list names: Names of counters to be processed.
        :rtype: LowConcurrencyCounterModel
        """
        keys = []
        for n in names:
            keys.append(ndb.Key(LowConcurrencyCounterModel, unicode(n)))

        final = []
        records = ndb.get_multi(keys)  # type: list
        for record in records:
            if record is not None:
                names.remove(record.get_name())
                final.append(record)
        create_entities = []
        for n in names:
            create_entities.append(LowConcurrencyCounterModel(id=unicode(n)))
        if len(create_entities) > 0:
            ndb.put_multi(create_entities)
            final.extend(create_entities)
        return final


class LowConcurrencyCounterModel(ndb.Model):
    count = ndb.IntegerProperty(indexed=False, name=u'c', default=0)

    def get_count(self):
        """
        :rtype: unicode
        """
        return self.count

    def get_name(self):
        """
        :rtype: unicode
        """
        return unicode(self.key.id())
README.md
# Low Concurrency Counters

## About

A simple library to manage low concurrency counters by utilising a lightweight datastore model.

## Usage

### Code:
```
from low_concurrency_counters import LowConcurrencyCounterService
import logging

logging.info("Get counter")
counter = LowConcurrencyCounterService.get("foo")
logging.info(counter.get_name())
logging.info(counter.get_count())

logging.info("Increment counter by 1")
counter = LowConcurrencyCounterService.incr("foo")
logging.info(counter.get_count())

logging.info("Increment counter by 2")
counter = LowConcurrencyCounterService.incr("foo", 2)
logging.info(counter.get_count())

logging.info("Decrement counter by 1")
counter = LowConcurrencyCounterService.decr("foo")
logging.info(counter.get_count())

logging.info("Decrement counter by 2")
counter = LowConcurrencyCounterService.decr("foo", 2)
logging.info(counter.get_count())

logging.info("Increment counter by 2 again")
counter = LowConcurrencyCounterService.incr("foo", 2)
logging.info(counter.get_count())

logging.info("Get multiple counters")
counters = LowConcurrencyCounterService.get_multi(["foo", "bar"])
for counter in counters:
    logging.info(counter.get_name() + " : " + str(counter.get_count()))

logging.info("Reset counter to 0")
counter = LowConcurrencyCounterService.reset("foo")
logging.info(counter.get_count())

logging.info("Delete counter")
LowConcurrencyCounterService.delete("foo")
LowConcurrencyCounterService.delete("bar")
```

### Result:
```
Get counter
INFO] foo
INFO] 0
INFO] Increment counter by 1
INFO] 1
INFO] Increment counter by 2
INFO] 3
INFO] Decrement counter by 1
INFO] 2
INFO] Decrement counter by 2
INFO] 0
INFO] Increment counter by 2 again
INFO] 2
INFO] Get multiple counters
INFO] foo : 2
INFO] bar : 0
INFO] Reset counter to 0
INFO] 0
INFO] Delete counter # No return from this.
```