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