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

73 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-13 14:41 +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 itertools 

9import typing as t 

10from contextvars import ContextVar 

11from dataclasses import dataclass, field 

12from ..config import conf 

13 

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

15 

16KEY_SPECIAL_PROPERTY = "__key__" 

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

18 

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

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

21 

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

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

24 

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

26 

27 

28class SortOrder(enum.Enum): 

29 Ascending = 1 

30 """Sort A->Z""" 

31 Descending = 2 

32 """Sort Z->A""" 

33 InvertedAscending = 3 

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

35 InvertedDescending = 4 

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

37 

38 

39class Key(Datastore_key): 

40 """ 

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

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

43 does not support accessing data in multiple projects. 

44 """ 

45 

46 def __init__(self, *path_args, project: str | None = None, **kwargs): 

47 # Convert digit-only id_or_name attributes to int 

48 # See https://github.com/viur-framework/viur-core/issues/1636 

49 new_path_args = [] 

50 for pair in itertools.batched(path_args, 2): 

51 try: 

52 kind, id_or_name = pair 

53 except ValueError: # it's a incomplete key 

54 new_path_args.append(pair[0]) 

55 continue 

56 if isinstance(id_or_name, str) and id_or_name.isdigit(): 

57 id_or_name = int(id_or_name) 

58 new_path_args.extend((kind, id_or_name)) 

59 

60 if project is None: 60 ↛ 64line 60 didn't jump to line 64 because the condition on line 60 was always true

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

62 project = __client__.project 

63 

64 super().__init__(*new_path_args, project=project, **kwargs) 

65 

66 def __str__(self): 

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

68 

69 ''' 

70 def __repr__(self): 

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

72 

73 def __hash__(self): 

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

75 

76 def __eq__(self, other): 

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

78 and self.parent == other.parent 

79 

80 @staticmethod 

81 def _parse_path(path_args): 

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

83 

84 :type path_args: tuple 

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

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

87 parts (int or string). 

88 

89 :rtype: :class:`list` of :class:`dict` 

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

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

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

93 a string or an integer. 

94 """ 

95 if len(path_args) == 0: 

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

97 

98 kind_list = path_args[::2] 

99 id_or_name_list = path_args[1::2] 

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

101 partial_ending = object() 

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

103 id_or_name_list += (partial_ending,) 

104 

105 result = [] 

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

107 curr_key_part = {} 

108 if isinstance(kind, str): 

109 curr_key_part["kind"] = kind 

110 else: 

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

112 

113 if isinstance(id_or_name, str): 

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

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

116 else: 

117 curr_key_part["name"] = id_or_name 

118 

119 elif isinstance(id_or_name, int): 

120 curr_key_part["id"] = id_or_name 

121 elif id_or_name is not partial_ending: 

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

123 

124 result.append(curr_key_part) 

125 return result 

126 

127 @classmethod 

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

129 """ 

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

131 :param strKey: The string key to parse 

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

133 """ 

134 urlsafe = strKey.encode("ASCII") 

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

136 urlsafe += padding 

137 raw_bytes = base64.urlsafe_b64decode(urlsafe) 

138 reference = _app_engine_key_pb2.Reference() 

139 reference.ParseFromString(raw_bytes) 

140 resultKey = None 

141 for elem in reference.path.element: 

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

143 return resultKey 

144 ''' 

145 

146 

147class Entity(Datastore_entity): 

148 """ 

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

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

151 """ 

152 

153 def __init__( 

154 self, 

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

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

157 ) -> None: 

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

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

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

161 

162 

163KeyType: t.TypeAlias = Key | str | int 

164""" 

165Alias that describes a key-type. 

166""" 

167 

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

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

170 

171 

172@dataclass 

173class QueryDefinition: 

174 """ 

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

176 """ 

177 

178 kind: t.Optional[str] 

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

180 

181 filters: TFilters 

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

183 

184 orders: t.Optional[TOrders] 

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

186 

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

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

189 

190 limit: int = field(init=False) 

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

192 

193 startCursor: t.Optional[str] = None 

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

195 

196 endCursor: t.Optional[str] = None 

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

198 

199 currentCursor: t.Optional[str] = None 

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

201 

202 def __post_init__(self): 

203 self.limit = conf.db.query_default_limit