| import re |
| import typing as _t |
| from time import time |
|
|
| from cachelib.base import BaseCache |
|
|
|
|
| _test_memcached_key = re.compile(r"[^\x00-\x21\xff]{1,250}$").match |
|
|
|
|
| class MemcachedCache(BaseCache): |
|
|
| """A cache that uses memcached as backend. |
| |
| The first argument can either be an object that resembles the API of a |
| :class:`memcache.Client` or a tuple/list of server addresses. In the |
| event that a tuple/list is passed, Werkzeug tries to import the best |
| available memcache library. |
| |
| This cache looks into the following packages/modules to find bindings for |
| memcached: |
| |
| - ``pylibmc`` |
| - ``google.appengine.api.memcached`` |
| - ``memcached`` |
| - ``libmc`` |
| |
| Implementation notes: This cache backend works around some limitations in |
| memcached to simplify the interface. For example unicode keys are encoded |
| to utf-8 on the fly. Methods such as :meth:`~BaseCache.get_dict` return |
| the keys in the same format as passed. Furthermore all get methods |
| silently ignore key errors to not cause problems when untrusted user data |
| is passed to the get methods which is often the case in web applications. |
| |
| :param servers: a list or tuple of server addresses or alternatively |
| a :class:`memcache.Client` or a compatible client. |
| :param default_timeout: the default timeout that is used if no timeout is |
| specified on :meth:`~BaseCache.set`. A timeout of |
| 0 indicates that the cache never expires. |
| :param key_prefix: a prefix that is added before all keys. This makes it |
| possible to use the same memcached server for different |
| applications. Keep in mind that |
| :meth:`~BaseCache.clear` will also clear keys with a |
| different prefix. |
| """ |
|
|
| def __init__( |
| self, |
| servers: _t.Any = None, |
| default_timeout: int = 300, |
| key_prefix: _t.Optional[str] = None, |
| ): |
| BaseCache.__init__(self, default_timeout) |
| if servers is None or isinstance(servers, (list, tuple)): |
| if servers is None: |
| servers = ["127.0.0.1:11211"] |
| self._client = self.import_preferred_memcache_lib(servers) |
| if self._client is None: |
| raise RuntimeError("no memcache module found") |
| else: |
| |
| |
| self._client = servers |
|
|
| self.key_prefix = key_prefix |
|
|
| def _normalize_key(self, key: str) -> str: |
| if self.key_prefix: |
| key = self.key_prefix + key |
| return key |
|
|
| def _normalize_timeout(self, timeout: _t.Optional[int]) -> int: |
| timeout = BaseCache._normalize_timeout(self, timeout) |
| if timeout > 0: |
| timeout = int(time()) + timeout |
| return timeout |
|
|
| def get(self, key: str) -> _t.Any: |
| key = self._normalize_key(key) |
| |
| |
| |
| if _test_memcached_key(key): |
| return self._client.get(key) |
|
|
| def get_dict(self, *keys: str) -> _t.Dict[str, _t.Any]: |
| key_mapping = {} |
| for key in keys: |
| encoded_key = self._normalize_key(key) |
| if _test_memcached_key(key): |
| key_mapping[encoded_key] = key |
| _keys = list(key_mapping) |
| d = rv = self._client.get_multi(_keys) |
| if self.key_prefix: |
| rv = {} |
| for key, value in d.items(): |
| rv[key_mapping[key]] = value |
| if len(rv) < len(keys): |
| for key in keys: |
| if key not in rv: |
| rv[key] = None |
| return rv |
|
|
| def add(self, key: str, value: _t.Any, timeout: _t.Optional[int] = None) -> bool: |
| key = self._normalize_key(key) |
| timeout = self._normalize_timeout(timeout) |
| return bool(self._client.add(key, value, timeout)) |
|
|
| def set( |
| self, key: str, value: _t.Any, timeout: _t.Optional[int] = None |
| ) -> _t.Optional[bool]: |
| key = self._normalize_key(key) |
| timeout = self._normalize_timeout(timeout) |
| return bool(self._client.set(key, value, timeout)) |
|
|
| def get_many(self, *keys: str) -> _t.List[_t.Any]: |
| d = self.get_dict(*keys) |
| return [d[key] for key in keys] |
|
|
| def set_many( |
| self, mapping: _t.Dict[str, _t.Any], timeout: _t.Optional[int] = None |
| ) -> _t.List[_t.Any]: |
| new_mapping = {} |
| for key, value in mapping.items(): |
| key = self._normalize_key(key) |
| new_mapping[key] = value |
|
|
| timeout = self._normalize_timeout(timeout) |
| failed_keys = self._client.set_multi( |
| new_mapping, timeout |
| ) |
| k_normkey = zip(mapping.keys(), new_mapping.keys()) |
| return [k for k, nkey in k_normkey if nkey not in failed_keys] |
|
|
| def delete(self, key: str) -> bool: |
| key = self._normalize_key(key) |
| if _test_memcached_key(key): |
| return bool(self._client.delete(key)) |
| return False |
|
|
| def delete_many(self, *keys: str) -> _t.List[_t.Any]: |
| new_keys = [] |
| for key in keys: |
| key = self._normalize_key(key) |
| if _test_memcached_key(key): |
| new_keys.append(key) |
| self._client.delete_multi(new_keys) |
| return [k for k in new_keys if not self.has(k)] |
|
|
| def has(self, key: str) -> bool: |
| key = self._normalize_key(key) |
| if _test_memcached_key(key): |
| return bool(self._client.append(key, "")) |
| return False |
|
|
| def clear(self) -> bool: |
| return bool(self._client.flush_all()) |
|
|
| def inc(self, key: str, delta: int = 1) -> _t.Optional[int]: |
| key = self._normalize_key(key) |
| value = (self._client.get(key) or 0) + delta |
| return value if self.set(key, value) else None |
|
|
| def dec(self, key: str, delta: int = 1) -> _t.Optional[int]: |
| key = self._normalize_key(key) |
| value = (self._client.get(key) or 0) - delta |
| return value if self.set(key, value) else None |
|
|
| def import_preferred_memcache_lib(self, servers: _t.Any) -> _t.Any: |
| """Returns an initialized memcache client. Used by the constructor.""" |
| try: |
| import pylibmc |
| except ImportError: |
| pass |
| else: |
| return pylibmc.Client(servers) |
|
|
| try: |
| from google.appengine.api import memcache |
| except ImportError: |
| pass |
| else: |
| return memcache.Client() |
|
|
| try: |
| import memcache |
| except ImportError: |
| pass |
| else: |
| return memcache.Client(servers) |
|
|
| try: |
| import libmc |
| except ImportError: |
| pass |
| else: |
| return libmc.Client(servers) |
|
|