Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/db/utils.py: 17%
118 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-07 12:23 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-07 12:23 +0000
1import datetime
2import fnmatch
3import sys
4import typing as t
6from deprecated.sphinx import deprecated
7from google.cloud.datastore.transaction import Transaction
9from viur.core import current
10from viur.core.config import conf
11from .transport import __client__, get, put, run_in_transaction
12from .types import Entity, Key, current_db_access_log
15def fix_unindexable_properties(entry: Entity, *, keep_exclusions: bool = True) -> Entity:
16 """
17 Recursively walk the given Entity and add all properties to the list of unindexed properties if they contain
18 a string longer than 1500 bytes (which is maximum size of a string that can be indexed). The datastore would
19 return an error otherwise.
20 https://cloud.google.com/datastore/docs/concepts/limits?hl=en#limits
22 :param entry: The entity to fix (inplace)
23 :param keep_exclusions: If true, keep the properties already included in ``exclude_from_indexes``.
24 Otherwise, ignore them and exclude only non-indexable properties.
25 :return: The fixed entity
26 """
28 def has_unindexable_property(prop):
29 if isinstance(prop, dict):
30 return any(has_unindexable_property(x) for x in prop.values())
31 elif isinstance(prop, list):
32 return any(has_unindexable_property(x) for x in prop)
33 elif isinstance(prop, (str, bytes)):
34 return sys.getsizeof(prop) >= 1500
35 else:
36 return False
38 unindexable_properties = set()
39 for key, value in entry.items():
40 if not has_unindexable_property(value):
41 continue
42 if isinstance(value, dict):
43 inner_entity = Entity()
44 inner_entity.update(value)
45 entry[key] = fix_unindexable_properties(inner_entity)
46 if isinstance(value, Entity):
47 inner_entity.key = value.key
48 else:
49 unindexable_properties.add(key)
50 if keep_exclusions:
51 entry.exclude_from_indexes.update(unindexable_properties) # type:ignore
52 else:
53 entry.exclude_from_indexes = unindexable_properties
54 return entry
57def normalize_key(key: t.Union[None, Key, str]) -> t.Union[None, Key]:
58 """
59 Normalizes a datastore key (replacing the key's project with conf.instance.project_id)
61 The key's project is only allowed to be normalized when it matches one of the patterns
62 configured in `conf.valid_application_ids`; otherwise a ValueError is raised.
64 :param key: Key to be normalized.
65 :return: Normalized key in string representation.
66 """
67 if key is None:
68 return None
70 if isinstance(key, str):
71 key = Key.from_legacy_urlsafe(key)
73 if key.project != conf.instance.project_id and not any(
74 fnmatch.fnmatch(key.project, application_id) for application_id in conf.valid_application_ids
75 ):
76 raise ValueError(f"{key=} cannot be normalized; Only keys from conf.valid_application_ids can be provided.")
78 if key.parent:
79 parent = normalize_key(key.parent)
80 else:
81 parent = None
83 return Key(key.kind, key.id_or_name, parent=parent)
86@deprecated(version="3.8.0", reason="Use 'db.normalize_key' instead")
87def normalizeKey(key: t.Union[None, Key]) -> t.Union[None, Key]:
88 return normalize_key(key)
91def key_helper(
92 in_key: t.Union[Key, str, int],
93 target_kind: str,
94 additional_allowed_kinds: t.Union[t.List[str], t.Tuple[str]] = (),
95 adjust_kind: bool = False,
96) -> Key:
97 if isinstance(in_key, Key):
98 if in_key.kind != target_kind and in_key.kind not in additional_allowed_kinds:
99 if not adjust_kind:
100 raise ValueError(
101 f"Kind mismatch: {in_key.kind!r} != {target_kind!r} (or in {additional_allowed_kinds!r})")
102 in_key = Key(target_kind, in_key.id_or_name, parent=in_key.parent)
103 return in_key
104 elif isinstance(in_key, str):
105 # Try to parse key from str
106 try:
107 decoded_key = normalize_key(in_key)
108 except Exception:
109 decoded_key = None
111 # If it did decode, recall keyHelper with Key object
112 if decoded_key:
113 return key_helper(
114 decoded_key,
115 target_kind=target_kind,
116 additional_allowed_kinds=additional_allowed_kinds,
117 adjust_kind=adjust_kind
118 )
120 # otherwise, construct key from str or int
121 if in_key.isdigit():
122 in_key = int(in_key)
124 return Key(target_kind, in_key)
125 elif isinstance(in_key, int):
126 return Key(target_kind, in_key)
128 raise NotImplementedError(f"Unsupported key type {type(in_key)}")
131def keyHelper(
132 inKey: t.Union[Key, str, int],
133 targetKind: str,
134 additionalAllowedKinds: t.Union[t.List[str], t.Tuple[str]] = (),
135 adjust_kind: bool = False,
136) -> Key:
137 return key_helper(
138 in_key=inKey,
139 target_kind=targetKind,
140 additional_allowed_kinds=additionalAllowedKinds,
141 adjust_kind=adjust_kind
142 )
145def is_in_transaction() -> bool:
146 return __client__.current_transaction is not None
149@deprecated(version="3.8.0", reason="Use 'db.utils.is_in_transaction' instead")
150def IsInTransaction() -> bool:
151 return is_in_transaction()
154def get_or_insert(key: Key, **kwargs) -> Entity:
155 """
156 Either creates a new entity with the given key, or returns the existing one.
158 Its guaranteed that there is no race-condition here; it will never overwrite a
159 previously created entity. Extra keyword arguments passed to this function will be
160 used to populate the entity if it has to be created; otherwise they are ignored.
162 :param key: The key which will be fetched or created.
163 :returns: Returns the fetched or newly created Entity.
164 """
166 def txn(key, kwargs):
167 obj = get(key)
168 if not obj:
169 obj = Entity(key)
170 for k, v in kwargs.items():
171 obj[k] = v
172 put(obj)
173 return obj
175 if is_in_transaction():
176 return txn(key, kwargs)
177 return run_in_transaction(txn, key, kwargs)
180@deprecated(version="3.8.0", reason="Use 'db.get_or_insert' instead")
181def GetOrInsert(key: Key, **kwargs: t.Any) -> Entity:
182 return get_or_insert(key, **kwargs)
185@deprecated(version="3.8.0", reason="Use 'str(key)' instead")
186def encodeKey(key: Key) -> str:
187 """
188 Return the given key encoded as string (mimicking the old str() behaviour of keys)
189 """
190 return str(key)
193def acquire_transaction_success_marker() -> str:
194 """
195 Generates a token that will be written to the datastore (under "viur-transactionmarker") if the transaction
196 completes successfully. Currently only used by deferredTasks to check if the task should actually execute
197 or if the transaction it was created in failed.
198 :return: Name of the entry in viur-transactionmarker
199 """
200 txn: Transaction | None = __client__.current_transaction
201 assert txn, "acquire_transaction_success_marker cannot be called outside an transaction"
202 marker = str(txn.id)
203 request_data = current.request_data.get()
204 if not request_data.get("__viur-transactionmarker__"):
205 db_obj = Entity(Key("viur-transactionmarker", marker))
206 db_obj["creationdate"] = datetime.datetime.now(datetime.timezone.utc)
207 put(db_obj)
208 request_data["__viur-transactionmarker__"] = True
209 return marker
212def start_data_access_log() -> t.Set[t.Union[Key, str]]:
213 """
214 Clears our internal access log (which keeps track of which entries have been accessed in the current
215 request). The old set of accessed entries is returned so that it can be restored with
216 :func:`server.db.popAccessData` in case of nested caching. You must call popAccessData afterwards, otherwise
217 we'll continue to log all entries accessed in subsequent request on the same thread!
218 :return: t.Set of old accessed entries
219 """
220 old = current_db_access_log.get(set())
221 current_db_access_log.set(set())
222 return old
225def startDataAccessLog() -> t.Set[t.Union[Key, str]]:
226 return start_data_access_log()
229def end_data_access_log(
230 outer_access_log: t.Optional[t.Set[t.Union[Key, str]]] = None,
231) -> t.Optional[t.Set[t.Union[Key, str]]]:
232 """
233 Retrieves the set of entries accessed so far.
235 To clean up and restart the log, call :func:`viur.datastore.startAccessDataLog`.
237 If you called :func:`server.db.startAccessDataLog` before, you can re-apply the old log using
238 the outerAccessLog param. Otherwise, it will disable the access log.
240 :param outerAccessLog: State of your log returned by :func:`server.db.startAccessDataLog`
241 :return: t.Set of entries accessed
242 """
243 res = current_db_access_log.get()
244 if isinstance(outer_access_log, set):
245 current_db_access_log.set((outer_access_log or set()).union(res))
246 else:
247 current_db_access_log.set(None)
248 return res
251def endDataAccessLog(
252 outerAccessLog: t.Optional[t.Set[t.Union[Key, str]]] = None,
253) -> t.Optional[t.Set[t.Union[Key, str]]]:
254 return end_data_access_log(outer_access_log=outerAccessLog)