Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/db/types.py: 85%
99 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
1"""
2The constants, global variables and container classes used in the datastore api
3"""
4from __future__ import annotations
6import copy
7import datetime
8import enum
9import itertools
10import typing as t
11from contextvars import ContextVar
12from dataclasses import dataclass, field
13from ..config import conf
15from google.cloud.datastore import Entity as Datastore_entity, Key as Datastore_key
17KEY_SPECIAL_PROPERTY = "__key__"
18"""The property name pointing to an entities key in a query"""
20DATASTORE_BASE_TYPES = t.Union[None, str, int, float, bool, datetime.datetime, datetime.date, datetime.time, "Key"]
21"""Types that can be used in a datastore query"""
23current_db_access_log: ContextVar[t.Optional[set[t.Union[Key, str]]]] = ContextVar("Database-Accesslog", default=None)
24"""If set to a set for the current thread/request, we'll log all entities / kinds accessed"""
26"""The current projectID, which can't be imported from transport.py"""
29class SortOrder(enum.Enum):
30 """
31 Defines possible types of sort orders for queries.
32 """
34 Ascending = 1
35 """Sort A->Z"""
36 Descending = 2
37 """Sort Z->A"""
38 InvertedAscending = 3
39 """Fetch Z->A, then flip the results (useful in pagination to go from a start cursor backwards)"""
40 InvertedDescending = 4
41 """Fetch A->Z, then flip the results (useful in pagination)"""
43 @classmethod
44 def from_str(cls, ident: str | int) -> SortOrder:
45 """
46 Parses a string defining a sort order into a db.SortOrder.
47 """
48 match str(ident or "").lower():
49 case "desc" | "descending" | "1":
50 return SortOrder.Descending
51 case "inverted_asc" | "inverted_ascending" | "2":
52 return SortOrder.InvertedAscending
53 case "inverted_desc" | "inverted_descending" | "3":
54 return SortOrder.InvertedDescending
55 case _: # everything else
56 return SortOrder.Ascending
59class QueryOrder(t.NamedTuple):
60 """A named tuple describing a single sort order for a datastore query."""
61 name: str
62 order: SortOrder = SortOrder.Ascending
65class Key(Datastore_key):
66 """
67 The python representation of one datastore key. Unlike the original implementation, we don't store a
68 reference to the project the key lives in. This is always expected to be the current project as ViUR
69 does not support accessing data in multiple projects.
70 """
72 def __init__(self, *path_args, project: str | None = None, **kwargs):
73 # Convert digit-only id_or_name attributes to int
74 # See https://github.com/viur-framework/viur-core/issues/1636
75 new_path_args = []
76 for pair in itertools.batched(path_args, 2):
77 try:
78 kind, id_or_name = pair
79 except ValueError: # it's a incomplete key
80 new_path_args.append(pair[0])
81 continue
82 if isinstance(id_or_name, str) and id_or_name.isdigit():
83 id_or_name = int(id_or_name)
84 new_path_args.extend((kind, id_or_name))
86 from .transport import __client__ # noqa: E402 # import works only here because circular imports
88 if project is None:
89 project = __client__.project
91 # Keys must match the client's db/namespace or Datastore rejects the
92 # request as cross-database. Default from the client; caller wins.
93 if __client__.database:
94 kwargs.setdefault("database", __client__.database)
95 if __client__.namespace:
96 kwargs.setdefault("namespace", __client__.namespace)
98 super().__init__(*new_path_args, project=project, **kwargs)
100 def __str__(self):
101 return self.to_legacy_urlsafe().decode("ASCII")
103 def to_legacy_urlsafe(self, location_prefix=None):
104 # Upstream to_legacy_urlsafe() rejects keys carrying a database, but
105 # str(key)/session paths hit it constantly. Encode a database-less copy —
106 # unambiguous to restore since the process talks to a single database.
107 # A copy keeps this thread-safe: mutating self._database in place would
108 # let concurrent encodes of the same Key clobber each other's state.
109 clone = copy.copy(self)
110 clone._database = None
111 return super(Key, clone).to_legacy_urlsafe(location_prefix=location_prefix)
113 '''
114 def __repr__(self):
115 return "<viur.datastore.Key %s/%s, parent=%s>" % (self.kind, self.id_or_name, self.parent)
117 def __hash__(self):
118 return hash("%s.%s.%s" % (self.kind, self.id, self.name))
120 def __eq__(self, other):
121 return isinstance(other, Key) and self.kind == other.kind and self.id == other.id and self.name == other.name \
122 and self.parent == other.parent
124 @staticmethod
125 def _parse_path(path_args):
126 """Parses positional arguments into key path with kinds and IDs.
128 :type path_args: tuple
129 :param path_args: A tuple from positional arguments. Should be
130 alternating list of kinds (string) and ID/name
131 parts (int or string).
133 :rtype: :class:`list` of :class:`dict`
134 :returns: A list of key parts with kind and ID or name set.
135 :raises: :class:`ValueError` if there are no ``path_args``, if one of
136 the kinds is not a string or if one of the IDs/names is not
137 a string or an integer.
138 """
139 if len(path_args) == 0:
140 raise ValueError("Key path must not be empty.")
142 kind_list = path_args[::2]
143 id_or_name_list = path_args[1::2]
144 # Dummy sentinel value to pad incomplete key to even length path.
145 partial_ending = object()
146 if len(path_args) % 2 == 1:
147 id_or_name_list += (partial_ending,)
149 result = []
150 for kind, id_or_name in zip(kind_list, id_or_name_list):
151 curr_key_part = {}
152 if isinstance(kind, str):
153 curr_key_part["kind"] = kind
154 else:
155 raise ValueError(kind, "Kind was not a string.")
157 if isinstance(id_or_name, str):
158 if (id_or_name.isdigit()): # !!! VIUR
159 curr_key_part["id"] = int(id_or_name)
160 else:
161 curr_key_part["name"] = id_or_name
163 elif isinstance(id_or_name, int):
164 curr_key_part["id"] = id_or_name
165 elif id_or_name is not partial_ending:
166 raise ValueError(id_or_name, "ID/name was not a string or integer.")
168 result.append(curr_key_part)
169 return result
171 @classmethod
172 def from_legacy_urlsafe(cls, strKey: str) -> Key:
173 """
174 Parses the string representation generated by :meth:to_legacy_urlsafe into a new Key object
175 :param strKey: The string key to parse
176 :return: The new Key object constructed from the string key
177 """
178 urlsafe = strKey.encode("ASCII")
179 padding = b"=" * (-len(urlsafe) % 4)
180 urlsafe += padding
181 raw_bytes = base64.urlsafe_b64decode(urlsafe)
182 reference = _app_engine_key_pb2.Reference()
183 reference.ParseFromString(raw_bytes)
184 resultKey = None
185 for elem in reference.path.element:
186 resultKey = Key(elem.type, elem.id or elem.name, parent=resultKey)
187 return resultKey
188 '''
191class Entity(Datastore_entity):
192 """
193 The python representation of one datastore entity. The values of this entity are stored inside this dictionary,
194 while the meta-data (it's key, the list of properties excluded from indexing and our version) as property values.
195 """
197 def __init__(
198 self,
199 key: t.Optional[Key] = None,
200 exclude_from_indexes: t.Optional[list[str]] = None,
201 ) -> None:
202 super().__init__(key, exclude_from_indexes or [])
203 if not (key is None or isinstance(key, Key)): 203 ↛ 204line 203 didn't jump to line 204 because the condition on line 203 was never true
204 raise ValueError(f"key must be a Key-Object (or None for an embedded entity). Got {key!r} ({type(key)})")
207KeyType: t.TypeAlias = Key | str | int
208"""
209Alias that describes a key-type.
210"""
212TOrders: t.TypeAlias = list[QueryOrder]
213TFilters: t.TypeAlias = dict[str, DATASTORE_BASE_TYPES | list[DATASTORE_BASE_TYPES]]
214TOrFilters: t.TypeAlias = list[list[tuple[str, DATASTORE_BASE_TYPES | list[DATASTORE_BASE_TYPES]]]]
217@dataclass
218class QueryDefinition:
219 """
220 A single Query that will be run against the datastore.
221 """
223 kind: t.Optional[str]
224 """The datastore kind to run the query on. Can be None for kindles queries."""
226 filters: TFilters
227 """A dictionary of constrains to apply to the query."""
229 orders: t.Optional[TOrders]
230 """The list of fields to sort the results by."""
232 distinct: t.Optional[list[str]] = None
233 """If set, a list of fields that we should return distinct values of"""
235 or_filters: "TOrFilters" = field(default_factory=list)
236 """Each entry is a list of (filterStr, value) pairs that are OR-ed together.
237 Multiple entries are AND-ed with each other and with the AND filters."""
239 limit: int = field(init=False)
240 """The maximum amount of entities that should be returned"""
242 startCursor: t.Optional[str] = None
243 """If set, we'll only return entities that appear after this cursor in the index."""
245 endCursor: t.Optional[str] = None
246 """If set, we'll only return entities up to this cursor in the index."""
248 currentCursor: t.Optional[str] = None
249 """Will be set after this query has been run, pointing after the last entity returned"""
251 def __post_init__(self):
252 self.limit = conf.db.query_default_limit