Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/db/types.py: 80%

60 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-13 11:04 +0000

1""" 

2The constants, global variables and container classes used in the datastore api 

3""" 

4from __future__ import annotations 

5 

6import datetime 

7import enum 

8import typing as t 

9from contextvars import ContextVar 

10from dataclasses import dataclass, field 

11from ..config import conf 

12 

13from google.cloud.datastore import Entity as Datastore_entity, Key as Datastore_key 

14 

15KEY_SPECIAL_PROPERTY = "__key__" 

16"""The property name pointing to an entities key in a query""" 

17 

18DATASTORE_BASE_TYPES = t.Union[None, str, int, float, bool, datetime.datetime, datetime.date, datetime.time, "Key"] 

19"""Types that can be used in a datastore query""" 

20 

21current_db_access_log: ContextVar[t.Optional[set[t.Union[Key, str]]]] = ContextVar("Database-Accesslog", default=None) 

22"""If set to a set for the current thread/request, we'll log all entities / kinds accessed""" 

23 

24"""The current projectID, which can't be imported from transport.py""" 

25 

26 

27class SortOrder(enum.Enum): 

28 Ascending = 1 

29 """Sort A->Z""" 

30 Descending = 2 

31 """Sort Z->A""" 

32 InvertedAscending = 3 

33 """Fetch Z->A, then flip the results (useful in pagination to go from a start cursor backwards)""" 

34 InvertedDescending = 4 

35 """Fetch A->Z, then flip the results (useful in pagination)""" 

36 

37 

38class Key(Datastore_key): 

39 """ 

40 The python representation of one datastore key. Unlike the original implementation, we don't store a 

41 reference to the project the key lives in. This is always expected to be the current project as ViUR 

42 does not support accessing data in multiple projects. 

43 """ 

44 

45 def __init__(self, *args, project=None, **kwargs): 

46 if project is None: 

47 from .transport import __client__ # noqa: E402 # import works only here because circular imports 

48 project = __client__.project 

49 

50 super().__init__(*args, project=project, **kwargs) 

51 

52 def __str__(self): 

53 return self.to_legacy_urlsafe().decode("ASCII") 

54 

55 ''' 

56 def __repr__(self): 

57 return "<viur.datastore.Key %s/%s, parent=%s>" % (self.kind, self.id_or_name, self.parent) 

58 

59 def __hash__(self): 

60 return hash("%s.%s.%s" % (self.kind, self.id, self.name)) 

61 

62 def __eq__(self, other): 

63 return isinstance(other, Key) and self.kind == other.kind and self.id == other.id and self.name == other.name \ 

64 and self.parent == other.parent 

65 

66 @staticmethod 

67 def _parse_path(path_args): 

68 """Parses positional arguments into key path with kinds and IDs. 

69 

70 :type path_args: tuple 

71 :param path_args: A tuple from positional arguments. Should be 

72 alternating list of kinds (string) and ID/name 

73 parts (int or string). 

74 

75 :rtype: :class:`list` of :class:`dict` 

76 :returns: A list of key parts with kind and ID or name set. 

77 :raises: :class:`ValueError` if there are no ``path_args``, if one of 

78 the kinds is not a string or if one of the IDs/names is not 

79 a string or an integer. 

80 """ 

81 if len(path_args) == 0: 

82 raise ValueError("Key path must not be empty.") 

83 

84 kind_list = path_args[::2] 

85 id_or_name_list = path_args[1::2] 

86 # Dummy sentinel value to pad incomplete key to even length path. 

87 partial_ending = object() 

88 if len(path_args) % 2 == 1: 

89 id_or_name_list += (partial_ending,) 

90 

91 result = [] 

92 for kind, id_or_name in zip(kind_list, id_or_name_list): 

93 curr_key_part = {} 

94 if isinstance(kind, str): 

95 curr_key_part["kind"] = kind 

96 else: 

97 raise ValueError(kind, "Kind was not a string.") 

98 

99 if isinstance(id_or_name, str): 

100 if (id_or_name.isdigit()): # !!! VIUR 

101 curr_key_part["id"] = int(id_or_name) 

102 else: 

103 curr_key_part["name"] = id_or_name 

104 

105 elif isinstance(id_or_name, int): 

106 curr_key_part["id"] = id_or_name 

107 elif id_or_name is not partial_ending: 

108 raise ValueError(id_or_name, "ID/name was not a string or integer.") 

109 

110 result.append(curr_key_part) 

111 return result 

112 

113 @classmethod 

114 def from_legacy_urlsafe(cls, strKey: str) -> Key: 

115 """ 

116 Parses the string representation generated by :meth:to_legacy_urlsafe into a new Key object 

117 :param strKey: The string key to parse 

118 :return: The new Key object constructed from the string key 

119 """ 

120 urlsafe = strKey.encode("ASCII") 

121 padding = b"=" * (-len(urlsafe) % 4) 

122 urlsafe += padding 

123 raw_bytes = base64.urlsafe_b64decode(urlsafe) 

124 reference = _app_engine_key_pb2.Reference() 

125 reference.ParseFromString(raw_bytes) 

126 resultKey = None 

127 for elem in reference.path.element: 

128 resultKey = Key(elem.type, elem.id or elem.name, parent=resultKey) 

129 return resultKey 

130 ''' 

131 

132 

133class Entity(Datastore_entity): 

134 """ 

135 The python representation of one datastore entity. The values of this entity are stored inside this dictionary, 

136 while the meta-data (it's key, the list of properties excluded from indexing and our version) as property values. 

137 """ 

138 

139 def __init__( 

140 self, 

141 key: t.Optional[Key] = None, 

142 exclude_from_indexes: t.Optional[list[str]] = None, 

143 ) -> None: 

144 super().__init__(key, exclude_from_indexes or []) 

145 if not (key is None or isinstance(key, Key)): 

146 raise ValueError(f"key must be a Key-Object (or None for an embedded entity). Got {key!r} ({type(key)})") 

147 

148 

149TOrders: t.TypeAlias = list[tuple[str, SortOrder]] 

150TFilters: t.TypeAlias = dict[str, DATASTORE_BASE_TYPES | list[DATASTORE_BASE_TYPES]] 

151 

152 

153@dataclass 

154class QueryDefinition: 

155 """ 

156 A single Query that will be run against the datastore. 

157 """ 

158 

159 kind: t.Optional[str] 

160 """The datastore kind to run the query on. Can be None for kindles queries.""" 

161 

162 filters: TFilters 

163 """A dictionary of constrains to apply to the query.""" 

164 

165 orders: t.Optional[TOrders] 

166 """The list of fields to sort the results by.""" 

167 

168 distinct: t.Optional[list[str]] = None 

169 """If set, a list of fields that we should return distinct values of""" 

170 

171 limit: int = field(init=False) 

172 """The maximum amount of entities that should be returned""" 

173 

174 startCursor: t.Optional[str] = None 

175 """If set, we'll only return entities that appear after this cursor in the index.""" 

176 

177 endCursor: t.Optional[str] = None 

178 """If set, we'll only return entities up to this cursor in the index.""" 

179 

180 currentCursor: t.Optional[str] = None 

181 """Will be set after this query has been run, pointing after the last entity returned""" 

182 

183 def __post_init__(self): 

184 self.limit = conf.db.query_default_limit