Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/db/cache.py: 76%
106 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-25 15:02 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-25 15:02 +0000
1import datetime
2import logging
3import sys
4import typing as t
6from viur.core.config import conf
7from viur.core import utils
8from .types import Entity, Key
10MEMCACHE_MAX_BATCH_SIZE = 30
11MEMCACHE_NAMESPACE = "viur-datastore"
12MEMCACHE_TIMEOUT: int | datetime.timedelta = datetime.timedelta(days=1)
13MEMCACHE_MAX_SIZE: t.Final[int] = 1_000_000
14TESTBED = None
15"""
17 This Module controls the Interaction with the Memcache from Google
18 To activate the cache copy this code in your main.py
19 .. code-block:: python
20 # Example
21 from viur.core import conf
22 from google.appengine.api.memcache import Client
23 conf.db.memcache_client = Client()
24"""
26__all__ = [
27 "MEMCACHE_MAX_BATCH_SIZE",
28 "MEMCACHE_NAMESPACE",
29 "MEMCACHE_TIMEOUT",
30 "MEMCACHE_MAX_SIZE",
31 "get",
32 "put",
33 "delete",
34 "flush",
35]
38def get(keys: t.Union[Key, t.Iterable[Key]], namespace: t.Optional[str] = None) -> list[Entity]:
39 """
40 Reads data form the memcache.
41 :param keys: Unique identifier(s) for one or more entry(s).
42 :param namespace: Optional namespace to use.
43 :return: The entities that were found, in arbitrary order. Always a list, even for a single key,
44 because an Entity is dict-like and callers must not have to tell a hit from an iterable.
45 """
46 # Inside a transaction reads must go straight to the datastore, so the cache
47 # cannot serve a (potentially stale) value. Lazy import to avoid a cycle.
48 from .utils import is_in_transaction
49 if is_in_transaction():
50 return []
52 if not check_for_memcache():
53 return []
55 namespace = namespace or MEMCACHE_NAMESPACE
56 keys = utils.ensure_iterable(keys)
57 keys = [str(key) for key in keys] # Enforce that all keys are strings
58 cached_data_result = {}
59 result = []
60 try:
61 while keys:
62 if cached_data := conf.db.memcache_client.get_multi(keys[:MEMCACHE_MAX_BATCH_SIZE], namespace=namespace):
63 cached_data_result |= cached_data
64 keys = keys[MEMCACHE_MAX_BATCH_SIZE:]
65 except Exception as e:
66 logging.error(f"""Failed to get keys form the memcache with {e=}""")
67 for key, value in cached_data_result.items():
68 entity = Entity(Key.from_legacy_urlsafe(key))
69 entity |= value
70 result.append(entity)
72 return result
75def put(
76 data: t.Union[Entity, t.Dict[Key, Entity], t.Iterable[Entity]],
77 namespace: t.Optional[str] = None,
78 timeout: t.Optional[t.Union[int, datetime.timedelta]] = None
79) -> bool:
80 """
81 Writes Data to the memcache.
82 :param data: Data to write
83 :param namespace: Optional namespace to use.
84 :param timeout: Optional timeout in seconds or a timedelta object.
85 :return: A boolean indicating success.
86 """
87 # Inside a transaction the write is not committed yet; caching it now would
88 # serve values that may be rolled back. Lazy import to avoid a cycle.
89 from .utils import is_in_transaction
90 if is_in_transaction():
91 return False
93 if not check_for_memcache(): 93 ↛ 94line 93 didn't jump to line 94 because the condition on line 93 was never true
94 return False
95 if not data: 95 ↛ 96line 95 didn't jump to line 96 because the condition on line 95 was never true
96 return False
97 namespace = namespace or MEMCACHE_NAMESPACE
98 timeout = timeout or MEMCACHE_TIMEOUT
99 if isinstance(timeout, datetime.timedelta): 99 ↛ 101line 99 didn't jump to line 101 because the condition on line 99 was always true
100 timeout = timeout.total_seconds()
101 if isinstance(data, (list, tuple, set)):
102 data = {item.key: item for item in data}
103 elif isinstance(data, Entity): 103 ↛ 105line 103 didn't jump to line 105 because the condition on line 103 was always true
104 data = {data.key: data}
105 elif not isinstance(data, dict):
106 raise TypeError(f"Invalid type {type(data)}. Expected a db.Entity, list or dict.")
108 # Add only values to cache <= MEMMAX_SIZE (1.000.000)
109 data = {str(key): value for key, value in data.items() if get_size(value) <= MEMCACHE_MAX_SIZE}
111 keys = list(data.keys())
112 try:
113 while keys:
114 data_batch = {key: data[key] for key in keys[:MEMCACHE_MAX_BATCH_SIZE]}
115 conf.db.memcache_client.set_multi(data_batch, namespace=namespace, time=timeout)
116 keys = keys[MEMCACHE_MAX_BATCH_SIZE:]
117 return True
118 except Exception as e:
119 logging.error(f"""Failed to put data to the memcache with {e=}""")
120 return False
123def delete(keys: t.Union[Key, t.Iterable[Key]], namespace: t.Optional[str] = None) -> None:
124 """
125 Deletes an Entry form memcache.
126 :param keys: Unique identifier(s) for one or more entry(s).
127 :param namespace: Optional namespace to use.
128 """
129 if not check_for_memcache(): 129 ↛ 130line 129 didn't jump to line 130 because the condition on line 129 was never true
130 return None
131 if not keys: 131 ↛ 132line 131 didn't jump to line 132 because the condition on line 131 was never true
132 return None
133 namespace = namespace or MEMCACHE_NAMESPACE
134 keys = utils.ensure_iterable(keys)
135 keys = [str(key) for key in keys] # Enforce that all keys are strings
136 try:
137 while keys:
138 conf.db.memcache_client.delete_multi(keys[:MEMCACHE_MAX_BATCH_SIZE], namespace=namespace)
139 keys = keys[MEMCACHE_MAX_BATCH_SIZE:]
140 except Exception as e:
141 logging.error(f"""Failed to delete keys form the memcache with {e=}""")
144def flush() -> bool:
145 """
146 Deletes everything in memcache.
147 :return: A boolean indicating success.
148 """
149 if not check_for_memcache():
150 return False
151 try:
152 conf.db.memcache_client.flush_all()
153 except Exception as e:
154 logging.error(f"""Failed to flush the memcache with {e=}""")
155 return False
156 return True
159def get_size(obj: t.Any) -> int:
160 """
161 Utility function that counts the size of an object in bytes.
162 """
163 if isinstance(obj, dict):
164 return sum(get_size([k, v]) for k, v in obj.items())
165 elif isinstance(obj, list):
166 return sum(get_size(x) for x in obj)
168 return sys.getsizeof(obj)
171def check_for_memcache() -> bool:
172 if conf.db.memcache_client is None:
173 # logging.warning(f"""conf.db.memcache_client is 'None'. It can not be used.""")
174 return False
176 init_testbed()
177 return True
180def init_testbed() -> None:
181 global TESTBED
182 if TESTBED is None and conf.instance.is_dev_server and conf.db.memcache_client: 182 ↛ 183line 182 didn't jump to line 183 because the condition on line 182 was never true
183 from google.appengine.ext.testbed import Testbed
184 TESTBED = Testbed()
185 TESTBED.activate()
186 TESTBED.init_memcache_stub()