class Factory(object):
    """
    Represent a cahce factory.
    """

    def store(self, name=None):
        """
        Get a cache store instance by name.

        :param name: The cache store name
        :type name: str

        :rtype: mixed
        """
        raise NotImplementedError() class Store(object):
    """
    Abstract class representing a cache store.
    """

    _serializer = PickleSerializer()

    def get(self, key):
        """
        Retrieve an item from the cache by key.

        :param key: The cache key
        :type key: str

        :return: The cache value
        """
        raise NotImplementedError()

    def put(self, key, value, minutes):
        """
        Store an item in the cache for a given number of minutes.

        :param key: The cache key
        :type key: str

        :param value: The cache value
        :type value: mixed

        :param minutes: The lifetime in minutes of the cached value
        :type minutes: int
        """
        raise NotImplementedError()

    def increment(self, key, value=1):
        """
        Increment the value of an item in the cache.

        :param key: The cache key
        :type key: str

        :param value: The increment value
        :type value: int

        :rtype: int or bool
        """
        raise NotImplementedError()

    def decrement(self, key, value=1):
        """
        Decrement the value of an item in the cache.

        :param key: The cache key
        :type key: str

        :param value: The decrement value
        :type value: int

        :rtype: int or bool
        """
        raise NotImplementedError()

    def forever(self, key, value):
        """
        Store an item in the cache indefinitely.

        :param key: The cache key
        :type key: str

        :param value: The value
        :type value: mixed
        """
        raise NotImplementedError()

    def forget(self, key):
        """
        Remove an item from the cache.

        :param key: The cache key
        :type key: str

        :rtype: bool
        """
        raise NotImplementedError()

    def flush(self):
        """
        Remove all items from the cache.
        """
        raise NotImplementedError()

    def get_prefix(self):
        """
        Get the cache key prefix.

        :rtype: str
        """
        raise NotImplementedError()

    def set_serializer(self, serializer):
        """
        Set the serializer.

        :param serializer: The serializer
        :type serializer: cachy.serializers.Serializer

        :rtype: Store
        """
        self._serializer = serializer

        return self

    def unserialize(self, data):
        return self._serializer.unserialize(data)

    def serialize(self, data):
        return self._serializer.serialize(data) class DictStore(TaggableStore):
    """
    A cache store using a dictionary as its backend.
    """

    def __init__(self):
        self._storage = {}

    def get(self, key):
        """
        Retrieve an item from the cache by key.

        :param key: The cache key
        :type key: str

        :return: The cache value
        """
        return self._get_payload(key)[0]

    def _get_payload(self, key):
        """
        Retrieve an item and expiry time from the cache by key.

        :param key: The cache key
        :type key: str

        :rtype: dict
        """
        payload = self._storage.get(key)

        # If the key does not exist, we return nothing
        if not payload:
            return (None, None)

        expire = payload[0]

        # If the current time is greater than expiration timestamps we will delete
        # the entry
        if round(time.time()) >= expire:
            self.forget(key)

            return (None, None)

        data = payload[1]

        # Next, we'll extract the number of minutes that are remaining for a cache
        # so that we can properly retain the time for things like the increment
        # operation that may be performed on the cache. We'll round this out.
        time_ = math.ceil((expire - round(time.time())) / 60.)

        return (data, time_)

    def put(self, key, value, minutes):
        """
        Store an item in the cache for a given number of minutes.

        :param key: The cache key
        :type key: str

        :param value: The cache value
        :type value: mixed

        :param minutes: The lifetime in minutes of the cached value
        :type minutes: int
        """
        self._storage[key] = (self._expiration(minutes), value)

    def increment(self, key, value=1):
        """
        Increment the value of an item in the cache.

        :param key: The cache key
        :type key: str

        :param value: The increment value
        :type value: int

        :rtype: int or bool
        """
        data, time_ = self._get_payload(key)

        integer = int(data) + value

        self.put(key, integer, int(time_))

        return integer

    def decrement(self, key, value=1):
        """
        Decrement the value of an item in the cache.

        :param key: The cache key
        :type key: str

        :param value: The decrement value
        :type value: int

        :rtype: int or bool
        """
        return self.increment(key, value * -1)

    def forever(self, key, value):
        """
        Store an item in the cache indefinitely.

        :param key: The cache key
        :type key: str

        :param value: The increment value
        :type value: int
        """
        self.put(key, value, 0)

    def forget(self, key):
        """
        Remove an item from the cache.

        :param key: The cache key
        :type key: str

        :rtype: bool
        """
        if key in self._storage:
            del self._storage[key]

            return True

        return False

    def flush(self):
        """
        Remove all items from the cache.
        """
        self._storage = {}

    def _expiration(self, minutes):
        """
        Get the expiration time based on the given minutes.

        :param minutes: The minutes
        :type minutes: int

        :rtype: int
        """
        if minutes == 0:
            return 9999999999

        return round(time.time()) + (minutes * 60)

    def get_prefix(self):
        """
        Get the cache key prefix.

        :rtype: str
        """
        return '' class FileStore(Store):
    """
    A cache store using the filesystem as its backend.
    """

    _HASHES = {
        'md5': (hashlib.md5, 2),
        'sha1': (hashlib.sha1, 4),
        'sha256': (hashlib.sha256, 8)
    }

    def __init__(self, directory, hash_type='sha256'):
        """
        :param directory: The cache directory
        :type directory: str
        """
        self._directory = directory

        if hash_type not in self._HASHES:
            raise ValueError('hash_type "{}" is not valid.'.format(hash_type))

        self._hash_type = hash_type

    def get(self, key):
        """
        Retrieve an item from the cache by key.

        :param key: The cache key
        :type key: str

        :return: The cache value
        """
        return self._get_payload(key).get('data')

    def _get_payload(self, key):
        """
        Retrieve an item and expiry time from the cache by key.

        :param key: The cache key
        :type key: str

        :rtype: dict
        """
        path = self._path(key)

        # If the file doesn't exists, we obviously can't return the cache so we will
        # just return null. Otherwise, we'll get the contents of the file and get
        # the expiration UNIX timestamps from the start of the file's contents.
        if not os.path.exists(path):
            return {'data': None, 'time': None}

        with open(path, 'rb') as fh:
            contents = fh.read()

        expire = int(contents[:10])

        # If the current time is greater than expiration timestamps we will delete
        # the file and return null. This helps clean up the old files and keeps
        # this directory much cleaner for us as old files aren't hanging out.
        if round(time.time()) >= expire:
            self.forget(key)

            return {'data': None, 'time': None}

        data = self.unserialize(contents[10:])

        # Next, we'll extract the number of minutes that are remaining for a cache
        # so that we can properly retain the time for things like the increment
        # operation that may be performed on the cache. We'll round this out.
        time_ = math.ceil((expire - round(time.time())) / 60.)

        return {'data': data, 'time': time_}

    def put(self, key, value, minutes):
        """
        Store an item in the cache for a given number of minutes.

        :param key: The cache key
        :type key: str

        :param value: The cache value
        :type value: mixed

        :param minutes: The lifetime in minutes of the cached value
        :type minutes: int
        """
        value = encode(str(self._expiration(minutes))) + encode(self.serialize(value))

        path = self._path(key)
        self._create_cache_directory(path)

        with open(path, 'wb') as fh:
            fh.write(value)

    def _create_cache_directory(self, path):
        """
        Create the file cache directory if necessary

        :param path: The cache path
        :type path: str
        """
        mkdir_p(os.path.dirname(path))

    def increment(self, key, value=1):
        """
        Increment the value of an item in the cache.

        :param key: The cache key
        :type key: str

        :param value: The increment value
        :type value: int

        :rtype: int or bool
        """
        raw = self._get_payload(key)

        integer = int(raw['data']) + value

        self.put(key, integer, int(raw['time']))

        return integer

    def decrement(self, key, value=1):
        """
        Decrement the value of an item in the cache.

        :param key: The cache key
        :type key: str

        :param value: The decrement value
        :type value: int

        :rtype: int or bool
        """
        return self.increment(key, value * -1)

    def forever(self, key, value):
        """
        Store an item in the cache indefinitely.

        :param key: The cache key
        :type key: str

        :param value: The increment value
        :type value: int
        """
        self.put(key, value, 0)

    def forget(self, key):
        """
        Remove an item from the cache.

        :param key: The cache key
        :type key: str

        :rtype: bool
        """
        path = self._path(key)

        if os.path.exists(path):
            os.remove(path)

            return True

        return False

    def flush(self):
        """
        Remove all items from the cache.
        """
        if os.path.isdir(self._directory):
            for root, dirs, files in os.walk(self._directory, topdown=False):
                for name in files:
                    os.remove(os.path.join(root, name))

                for name in dirs:
                    os.rmdir(os.path.join(root, name))

    def _path(self, key):
        """
        Get the full path for the given cache key.

        :param key: The cache key
        :type key: str

        :rtype: str
        """
        hash_type, parts_count = self._HASHES[self._hash_type]
        h = hash_type(encode(key)).hexdigest()

        parts = [h[i:i+2] for i in range(0, len(h), 2)][:parts_count]

        return os.path.join(self._directory, os.path.sep.join(parts), h)

    def _expiration(self, minutes):
        """
        Get the expiration time based on the given minutes.

        :param minutes: The minutes
        :type minutes: int

        :rtype: int
        """
        if minutes == 0:
            return 9999999999

        return int(round(time.time()) + (minutes * 60))

    def get_prefix(self):
        """
        Get the cache key prefix.

        :rtype: str
        """
        return ''
        return '' class MemcachedStore(TaggableStore):

    def __init__(self, servers, prefix='', **kwargs):
        # Removing potential "driver" key
        kwargs.pop('driver', None)

        self._prefix = prefix
        self._memcache = memcache.Client(servers, **kwargs)

    def get(self, key):
        """
        Retrieve an item from the cache by key.

        :param key: The cache key
        :type key: str

        :return: The cache value
        """
        return self._memcache.get(self._prefix + key)

    def put(self, key, value, minutes):
        """
        Store an item in the cache for a given number of minutes.

        :param key: The cache key
        :type key: str

        :param value: The cache value
        :type value: mixed

        :param minutes: The lifetime in minutes of the cached value
        :type minutes: int
        """
        self._memcache.set(self._prefix + key, value, minutes * 60)

    def add(self, key, val, minutes):
        """
        Store an item in the cache if it does not exist.

        :param key: The cache key
        :type key: str

        :param val: The cache value
        :type val: mixed

        :param minutes: The lifetime in minutes of the cached value
        :type minutes: int

        :rtype: bool
        """
        return self._memcache.add(self._prefix + key, val, minutes * 60)

    def increment(self, key, value=1):
        """
        Increment the value of an item in the cache.

        :param key: The cache key
        :type key: str

        :param value: The increment value
        :type value: int

        :rtype: int or bool
        """
        return self._memcache.incr(self._prefix + key, value)

    def decrement(self, key, value=1):
        """
        Decrement the value of an item in the cache.

        :param key: The cache key
        :type key: str

        :param value: The decrement value
        :type value: int

        :rtype: int or bool
        """
        return self._memcache.decr(self._prefix + key, value)

    def forever(self, key, value):
        """
        Store an item in the cache indefinitely.

        :param key: The cache key
        :type key: str

        :param value: The value
        :type value: mixed
        """
        self.put(key, value, 0)

    def forget(self, key):
        """
        Remove an item from the cache.

        :param key: The cache key
        :type key: str

        :rtype: bool
        """
        return self._memcache.delete(self._prefix + key)

    def flush(self):
        """
        Remove all items from the cache.
        """
        self._memcache.flush_all()

    def get_prefix(self):
        """
        Get the cache key prefix.

        :rtype: str
        """
        return self._prefix class RedisStore(TaggableStore):
    """
    A cache store using the Redis as its backend.
    """

    def __init__(self, host='localhost', port=6379, db=0, password=None,
                 prefix='', redis_class=StrictRedis, **kwargs):
        # Removing potential "driver" key
        kwargs.pop('driver', None)

        self._prefix = prefix
        self._redis = redis_class(host=host, port=port, db=db,
                                  password=password, **kwargs)

    def get(self, key):
        """
        Retrieve an item from the cache by key.

        :param key: The cache key
        :type key: str

        :return: The cache value
        """
        value = self._redis.get(self._prefix + key)

        if value is not None:
            return self.unserialize(value)

    def put(self, key, value, minutes):
        """
        Store an item in the cache for a given number of minutes.

        :param key: The cache key
        :type key: str

        :param value: The cache value
        :type value: mixed

        :param minutes: The lifetime in minutes of the cached value
        :type minutes: int
        """
        value = self.serialize(value)

        minutes = max(1, minutes)

        self._redis.setex(self._prefix + key, minutes * 60, value)

    def increment(self, key, value=1):
        """
        Increment the value of an item in the cache.

        :param key: The cache key
        :type key: str

        :param value: The increment value
        :type value: int

        :rtype: int or bool
        """
        return self._redis.incrby(self._prefix + key, value)

    def decrement(self, key, value=1):
        """
        Decrement the value of an item in the cache.

        :param key: The cache key
        :type key: str

        :param value: The decrement value
        :type value: int

        :rtype: int or bool
        """
        return self._redis.decr(self._prefix + key, value)

    def forever(self, key, value):
        """
        Store an item in the cache indefinitely.

        :param key: The cache key
        :type key: str

        :param value: The value to store
        :type value: mixed
        """
        value = self.serialize(value)

        self._redis.set(self._prefix + key, value)

    def forget(self, key):
        """
        Remove an item from the cache.

        :param key: The cache key
        :type key: str

        :rtype: bool
        """
        return bool(self._redis.delete(self._prefix + key))

    def flush(self):
        """
        Remove all items from the cache.
        """
        return self._redis.flushdb()

    def get_prefix(self):
        """
        Get the cache key prefix.

        :rtype: str
        """
        return self._prefix

    def connection(self):
        return self._redis

    def tags(self, *names):
        """
        Begin executing a new tags operation.

        :param names: The tags
        :type names: tuple

        :rtype: cachy.tagged_cache.TaggedCache
        """
        return RedisTaggedCache(self, TagSet(self, names)) class Repository(CacheContract):

    _default = 60

    def __init__(self, store):
        """
        :param store: The underlying cache store
        :type store: Store
        """
        self._store = store

    def has(self, key):
        """
        Determine if an item exists in the cache.

        :param key: The cache key
        :type key: str

        :rtype: bool
        """
        return self.get(key) is not None

    def get(self, key, default=None):
        """
        Retrieve an item from the cache by key.

        :param key: The cache key
        :type key: str

        :param default: The default value to return
        :type default: mixed

        :rtype: mixed
        """
        val = self._store.get(key)

        if val is None:
            return value(default)

        return val

    def pull(self, key, default=None):
        """
        Retrieve an item from the cache by key and delete ir.

        :param key: The cache key
        :type key: str

        :param default: The default value to return
        :type default: mixed

        :rtype: mixed
        """
        val = self.get(key, default)

        self.forget(key)

        return val

    def put(self, key, val, minutes):
        """
        Store an item in the cache.

        :param key: The cache key
        :type key: str

        :param val: The cache value
        :type val: mixed

        :param minutes: The lifetime in minutes of the cached value
        :type minutes: int|datetime
        """
        minutes = self._get_minutes(minutes)

        if minutes is not None:
            self._store.put(key, val, minutes)

    def add(self, key, val, minutes):
        """
        Store an item in the cache if it does not exist.

        :param key: The cache key
        :type key: str

        :param val: The cache value
        :type val: mixed

        :param minutes: The lifetime in minutes of the cached value
        :type minutes: int|datetime

        :rtype: bool
        """
        if hasattr(self._store, 'add'):
            return self._store.add(key, val, self._get_minutes(minutes))

        if not self.has(key):
            self.put(key, val, minutes)

            return True

        return False

    def forever(self, key, val):
        """
        Store an item in the cache indefinitely.

        :param key: The cache key
        :type key: str

        :param val: The cache value
        :type val: mixed
        """
        self._store.forever(key, val)

    def remember(self, key, minutes, callback):
        """
        Get an item from the cache, or store the default value.

        :param key: The cache key
        :type key: str

        :param minutes: The lifetime in minutes of the cached value
        :type minutes: int or datetime

        :param callback: The default function
        :type callback: mixed

        :rtype: mixed
        """
        # If the item exists in the cache we will just return this immediately
        # otherwise we will execute the given callback and cache the result
        # of that execution for the given number of minutes in storage.
        val = self.get(key)
        if val is not None:
            return val

        val = value(callback)

        self.put(key, val, minutes)

        return val

    def remember_forever(self, key, callback):
        """
        Get an item from the cache, or store the default value forever.

        :param key: The cache key
        :type key: str

        :param callback: The default function
        :type callback: mixed

        :rtype: mixed
        """
        # If the item exists in the cache we will just return this immediately
        # otherwise we will execute the given callback and cache the result
        # of that execution forever.
        val = self.get(key)
        if val is not None:
            return val

        val = value(callback)

        self.forever(key, val)

        return val

    def forget(self, key):
        """
        Remove an item from the cache.

        :param key: The cache key
        :type key: str

        :rtype: bool
        """
        success = self._store.forget(key)

        return success

    def get_default_cache_time(self):
        """
        Get the default cache time.

        :rtype: int
        """
        return self._default

    def set_default_cache_time(self, minutes):
        """
        Set the default cache time.

        :param minutes: The default cache time
        :type minutes: int

        :rtype: self
        """
        self._default = minutes

        return self

    def get_store(self):
        """
        Get the cache store implementation.

        :rtype: Store
        """
        return self._store

    def __getitem__(self, item):
        return self.get(item)

    def __setitem__(self, key, val):
        self.put(key, val, self._default)

    def __delitem__(self, key):
        self.forget(key)

    def _get_minutes(self, duration):
        """
        Calculate the number of minutes with the given duration.

        :param duration: The duration
        :type duration: int or datetime

        :rtype: int or None
        """
        if isinstance(duration, datetime.datetime):
            from_now = (duration - datetime.datetime.now()).total_seconds()
            from_now = math.ceil(from_now / 60)

            if from_now > 0:
                return from_now

            return

        return duration

    def _hash(self, value):
        """
        Calculate the hash given a value.

        :param value: The value to hash
        :type value: str or bytes

        :rtype: str
        """
        return hashlib.sha1(encode(value)).hexdigest()

    def _get_key(self, fn, args, kwargs):
        """
        Calculate a cache key given a function, args and kwargs.

        :param fn: The function
        :type fn: callable or str

        :param args: The function args
        :type args: tuple

        :param kwargs: The function kwargs
        :type kwargs: dict

        :rtype: str
        """
        if args:
            serialized_arguments = (
                self._store.serialize(args[1:])
                + self._store.serialize([(k, kwargs[k]) for k in sorted(kwargs.keys())])
            )
        else:
            serialized_arguments = self._store.serialize([(k, kwargs[k]) for k in sorted(kwargs.keys())])

        if isinstance(fn, types.MethodType):
            key = self._hash('%s.%s.%s'
                             % (fn.__self__.__class__.__name__,
                                args[0].__name__,
                                serialized_arguments))
        elif isinstance(fn, types.FunctionType):
            key = self._hash('%s.%s'
                             % (fn.__name__,
                                serialized_arguments))
        else:
            key = '%s:' % fn + self._hash(serialized_arguments)

        return key

    def __getattr__(self, item):
        try:
            return object.__getattribute__(self, item)
        except AttributeError:
            return getattr(self._store, item)

    def __call__(self, *args, **kwargs):
        if args and isinstance(args[0], (types.FunctionType, types.MethodType)):
            fn = args[0]

            @wraps(fn)
            def wrapper(*a, **kw):
                return self.remember(
                    self._get_key(fn, a, kw),
                    self._default,
                    lambda: fn(*a, **kw)
                )

            return wrapper
        else:
            k = kwargs.get('key')
            minutes = kwargs.get('minutes', self._default)

            def decorated(fn):
                key = k

                @wraps(fn)
                def wrapper(*a, **kw):
                    return self.remember(
                        self._get_key(key or fn, a, kw),
                        minutes,
                        lambda: fn(*a, **kw)
                    )

                return wrapper

            return decorated class Serializer(object):
    """
    Abstract serializer.
    """

    def serialize(self, data):
        """
        Serialize data.

        :param data: The data to serialize
        :type data: mixed

        :rtype: str
        """
        raise NotImplementedError()

    def unserialize(self, data):
        """
        Unserialize data.

        :param data: The data to unserialize
        :type data: mixed

        :rtype: str
        """
        raise NotImplementedError() class JsonSerializer(Serializer):
    """
    Serializer that uses JSON representations.
    """

    def serialize(self, data):
        """
        Serialize data.

        :param data: The data to serialize
        :type data: mixed

        :rtype: str
        """
        return json.dumps(data)

    def unserialize(self, data):
        """
        Unserialize data.

        :param data: The data to unserialize
        :type data: mixed

        :rtype: str
        """
        return json.loads(decode(data)) class MsgPackSerializer(Serializer):
    """
    Serializer that uses `msgpack <https://pypi.python.org/pypi/msgpack-python/>`_ representations.

    By default, this serializer does not support serializing custom objects.
    """

    def serialize(self, data):
        """
        Serialize data.

        :param data: The data to serialize
        :type data: mixed

        :rtype: str
        """
        return msgpack.packb(data, use_bin_type=True)

    def unserialize(self, data):
        """
        Unserialize data.

        :param data: The data to unserialize
        :type data: mixed

        :rtype: str
        """
        return msgpack.unpackb(data, encoding='utf-8') class PickleSerializer(Serializer):
    """
    Serializer that uses the pickle module.
    """

    def serialize(self, data):
        """
        Serialize data.

        :param data: The data to serialize
        :type data: mixed

        :rtype: str
        """
        return dumps(data)

    def unserialize(self, data):
        """
        Unserialize data.

        :param data: The data to unserialize
        :type data: mixed

        :rtype: str
        """
        return loads(data)
import threading
import types
from .contracts.factory import Factory
from .contracts.store import Store
from .stores import (
    DictStore,
    FileStore,
    RedisStore,
    MemcachedStore
)
from .repository import Repository
from .serializers import (
    Serializer,
    JsonSerializer,
    MsgPackSerializer,
    PickleSerializer
)
# -*- coding: utf-8 -*-





class CacheManager(Factory, threading.local):
    """
    A CacheManager is a pool of cache stores.
    """

    _serializers = {
        'json': JsonSerializer(),
        'msgpack': MsgPackSerializer(),
        'pickle': PickleSerializer()
    }

    def __init__(self, config):
        super(CacheManager, self).__init__()

        self._config = config
        self._stores = {}
        self._custom_creators = {}
        self._serializer = self._resolve_serializer(config.get('serializer', 'pickle'))

    def store(self, name=None):
        """
        Get a cache store instance by name.

        :param name: The cache store name
        :type name: str

        :rtype: Repository
        """
        if name is None:
            name = self.get_default_driver()

        self._stores[name] = self._get(name)

        return self._stores[name]

    def driver(self, name=None):
        """
        Get a cache store instance by name.

        :param name: The cache store name
        :type name: str

        :rtype: Repository
        """
        return self.store(name)

    def _get(self, name):
        """
        Attempt to get the store from the local cache.

        :param name: The store name
        :type name: str

        :rtype: Repository
        """
        return self._stores.get(name, self._resolve(name))

    def _resolve(self, name):
        """
        Resolve the given store

        :param name: The store to resolve
        :type name: str

        :rtype: Repository
        """
        config = self._get_config(name)

        if not config:
            raise RuntimeError('Cache store [%s] is not defined.' % name)

        if config['driver'] in self._custom_creators:
            repository = self._call_custom_creator(config)
        else:
            repository = getattr(self, '_create_%s_driver' % config['driver'])(config)

        if 'serializer' in config:
            serializer = self._resolve_serializer(config['serializer'])
        else:
            serializer = self._serializer

        repository.get_store().set_serializer(serializer)

        return repository

    def _call_custom_creator(self, config):
        """
        Call a custom driver creator.

        :param config: The driver configuration
        :type config: dict

        :rtype: Repository
        """
        creator = self._custom_creators[config['driver']](config)

        if isinstance(creator, Store):
            creator = self.repository(creator)


        if not isinstance(creator, Repository):