Format: text/x-python
⬇ Download File (7.0 KB)
   1  import re
   2  import typing as _t
   3  from time import time
   4  
   5  from cachelib.base import BaseCache
   6  
   7  
   8  _test_memcached_key = re.compile(r"[^\x00-\x21\xff]{1,250}$").match
   9  
  10  
  11  class MemcachedCache(BaseCache):
  12      """A cache that uses memcached as backend.
  13  
  14      The first argument can either be an object that resembles the API of a
  15      :class:`memcache.Client` or a tuple/list of server addresses. In the
  16      event that a tuple/list is passed, Werkzeug tries to import the best
  17      available memcache library.
  18  
  19      This cache looks into the following packages/modules to find bindings for
  20      memcached:
  21  
  22          - ``pylibmc``
  23          - ``google.appengine.api.memcached``
  24          - ``memcached``
  25          - ``libmc``
  26  
  27      Implementation notes:  This cache backend works around some limitations in
  28      memcached to simplify the interface.  For example unicode keys are encoded
  29      to utf-8 on the fly.  Methods such as :meth:`~BaseCache.get_dict` return
  30      the keys in the same format as passed.  Furthermore all get methods
  31      silently ignore key errors to not cause problems when untrusted user data
  32      is passed to the get methods which is often the case in web applications.
  33  
  34      :param servers: a list or tuple of server addresses or alternatively
  35                      a :class:`memcache.Client` or a compatible client.
  36      :param default_timeout: the default timeout that is used if no timeout is
  37                              specified on :meth:`~BaseCache.set`. A timeout of
  38                              0 indicates that the cache never expires.
  39      :param key_prefix: a prefix that is added before all keys.  This makes it
  40                         possible to use the same memcached server for different
  41                         applications.  Keep in mind that
  42                         :meth:`~BaseCache.clear` will also clear keys with a
  43                         different prefix.
  44      """
  45  
  46      def __init__(
  47          self,
  48          servers: _t.Any = None,
  49          default_timeout: int = 300,
  50          key_prefix: _t.Optional[str] = None,
  51      ):
  52          BaseCache.__init__(self, default_timeout)
  53          if servers is None or isinstance(servers, (list, tuple)):
  54              if servers is None:
  55                  servers = ["127.0.0.1:11211"]
  56              self._client = self.import_preferred_memcache_lib(servers)
  57              if self._client is None:
  58                  raise RuntimeError("no memcache module found")
  59          else:
  60              # NOTE: servers is actually an already initialized memcache
  61              # client.
  62              self._client = servers
  63  
  64          self.key_prefix = key_prefix
  65  
  66      def _normalize_key(self, key: str) -> str:
  67          if self.key_prefix:
  68              key = self.key_prefix + key
  69          return key
  70  
  71      def _normalize_timeout(self, timeout: _t.Optional[int]) -> int:
  72          timeout = BaseCache._normalize_timeout(self, timeout)
  73          if timeout > 0:
  74              timeout = int(time()) + timeout
  75          return timeout
  76  
  77      def get(self, key: str) -> _t.Any:
  78          key = self._normalize_key(key)
  79          # memcached doesn't support keys longer than that.  Because often
  80          # checks for so long keys can occur because it's tested from user
  81          # submitted data etc we fail silently for getting.
  82          if _test_memcached_key(key):
  83              return self._client.get(key)
  84  
  85      def get_dict(self, *keys: str) -> _t.Dict[str, _t.Any]:
  86          key_mapping = {}
  87          for key in keys:
  88              encoded_key = self._normalize_key(key)
  89              if _test_memcached_key(key):
  90                  key_mapping[encoded_key] = key
  91          _keys = list(key_mapping)
  92          d = rv = self._client.get_multi(_keys)  # type: _t.Dict[str, _t.Any]
  93          if self.key_prefix:
  94              rv = {}
  95              for key, value in d.items():
  96                  rv[key_mapping[key]] = value
  97          if len(rv) < len(keys):
  98              for key in keys:
  99                  if key not in rv:
 100                      rv[key] = None
 101          return rv
 102  
 103      def add(self, key: str, value: _t.Any, timeout: _t.Optional[int] = None) -> bool:
 104          key = self._normalize_key(key)
 105          timeout = self._normalize_timeout(timeout)
 106          return bool(self._client.add(key, value, timeout))
 107  
 108      def set(
 109          self, key: str, value: _t.Any, timeout: _t.Optional[int] = None
 110      ) -> _t.Optional[bool]:
 111          key = self._normalize_key(key)
 112          timeout = self._normalize_timeout(timeout)
 113          return bool(self._client.set(key, value, timeout))
 114  
 115      def get_many(self, *keys: str) -> _t.List[_t.Any]:
 116          d = self.get_dict(*keys)
 117          return [d[key] for key in keys]
 118  
 119      def set_many(
 120          self, mapping: _t.Dict[str, _t.Any], timeout: _t.Optional[int] = None
 121      ) -> _t.List[_t.Any]:
 122          new_mapping = {}
 123          for key, value in mapping.items():
 124              key = self._normalize_key(key)
 125              new_mapping[key] = value
 126  
 127          timeout = self._normalize_timeout(timeout)
 128          failed_keys = self._client.set_multi(
 129              new_mapping, timeout
 130          )  # type: _t.List[_t.Any]
 131          k_normkey = zip(mapping.keys(), new_mapping.keys())  # noqa: B905
 132          return [k for k, nkey in k_normkey if nkey not in failed_keys]
 133  
 134      def delete(self, key: str) -> bool:
 135          key = self._normalize_key(key)
 136          if _test_memcached_key(key):
 137              return bool(self._client.delete(key))
 138          return False
 139  
 140      def delete_many(self, *keys: str) -> _t.List[_t.Any]:
 141          new_keys = []
 142          for key in keys:
 143              key = self._normalize_key(key)
 144              if _test_memcached_key(key):
 145                  new_keys.append(key)
 146          self._client.delete_multi(new_keys)
 147          return [k for k in new_keys if not self.has(k)]
 148  
 149      def has(self, key: str) -> bool:
 150          key = self._normalize_key(key)
 151          if _test_memcached_key(key):
 152              return bool(self._client.append(key, ""))
 153          return False
 154  
 155      def clear(self) -> bool:
 156          return bool(self._client.flush_all())
 157  
 158      def inc(self, key: str, delta: int = 1) -> _t.Optional[int]:
 159          key = self._normalize_key(key)
 160          value = (self._client.get(key) or 0) + delta
 161          return value if self.set(key, value) else None
 162  
 163      def dec(self, key: str, delta: int = 1) -> _t.Optional[int]:
 164          key = self._normalize_key(key)
 165          value = (self._client.get(key) or 0) - delta
 166          return value if self.set(key, value) else None
 167  
 168      def import_preferred_memcache_lib(self, servers: _t.Any) -> _t.Any:
 169          """Returns an initialized memcache client.  Used by the constructor."""
 170          try:
 171              import pylibmc  # type: ignore
 172          except ImportError:
 173              pass
 174          else:
 175              return pylibmc.Client(servers)
 176  
 177          try:
 178              from google.appengine.api import memcache  # type: ignore
 179          except ImportError:
 180              pass
 181          else:
 182              return memcache.Client()
 183  
 184          try:
 185              import memcache  # type: ignore
 186          except ImportError:
 187              pass
 188          else:
 189              return memcache.Client(servers)
 190  
 191          try:
 192              import libmc  # type: ignore
 193          except ImportError:
 194              pass
 195          else:
 196              return libmc.Client(servers)
 197