Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/google/cloud/firestore_v1/field_path.py: 28%

141 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-09 06:27 +0000

1# Copyright 2018 Google LLC 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14 

15"""Utilities for managing / converting field paths to / from strings.""" 

16 

17from collections import abc 

18 

19import re 

20from typing import Iterable 

21 

22 

23_FIELD_PATH_MISSING_TOP = "{!r} is not contained in the data" 

24_FIELD_PATH_MISSING_KEY = "{!r} is not contained in the data for the key {!r}" 

25_FIELD_PATH_WRONG_TYPE = ( 

26 "The data at {!r} is not a dictionary, so it cannot contain the key {!r}" 

27) 

28 

29_FIELD_PATH_DELIMITER = "." 

30_BACKSLASH = "\\" 

31_ESCAPED_BACKSLASH = _BACKSLASH * 2 

32_BACKTICK = "`" 

33_ESCAPED_BACKTICK = _BACKSLASH + _BACKTICK 

34 

35_SIMPLE_FIELD_NAME = re.compile("^[_a-zA-Z][_a-zA-Z0-9]*$") 

36_LEADING_ALPHA_INVALID = re.compile("^[_a-zA-Z][_a-zA-Z0-9]*[^_a-zA-Z0-9]") 

37PATH_ELEMENT_TOKENS = [ 

38 ("SIMPLE", r"[_a-zA-Z][_a-zA-Z0-9]*"), # unquoted elements 

39 ("QUOTED", r"`(?:\\`|[^`])*?`"), # quoted elements, unquoted 

40 ("DOT", r"\."), # separator 

41] 

42TOKENS_PATTERN = "|".join("(?P<{}>{})".format(*pair) for pair in PATH_ELEMENT_TOKENS) 

43TOKENS_REGEX = re.compile(TOKENS_PATTERN) 

44 

45 

46def _tokenize_field_path(path: str): 

47 """Lex a field path into tokens (including dots). 

48 

49 Args: 

50 path (str): field path to be lexed. 

51 Returns: 

52 List(str): tokens 

53 """ 

54 pos = 0 

55 get_token = TOKENS_REGEX.match 

56 match = get_token(path) 

57 while match is not None: 

58 type_ = match.lastgroup 

59 value = match.group(type_) 

60 yield value 

61 pos = match.end() 

62 match = get_token(path, pos) 

63 if pos != len(path): 

64 raise ValueError("Path {} not consumed, residue: {}".format(path, path[pos:])) 

65 

66 

67def split_field_path(path: str): 

68 """Split a field path into valid elements (without dots). 

69 

70 Args: 

71 path (str): field path to be lexed. 

72 Returns: 

73 List(str): tokens 

74 Raises: 

75 ValueError: if the path does not match the elements-interspersed- 

76 with-dots pattern. 

77 """ 

78 if not path: 

79 return [] 

80 

81 elements = [] 

82 want_dot = False 

83 

84 for element in _tokenize_field_path(path): 

85 if want_dot: 

86 if element != ".": 

87 raise ValueError("Invalid path: {}".format(path)) 

88 else: 

89 want_dot = False 

90 else: 

91 if element == ".": 

92 raise ValueError("Invalid path: {}".format(path)) 

93 elements.append(element) 

94 want_dot = True 

95 

96 if not want_dot or not elements: 

97 raise ValueError("Invalid path: {}".format(path)) 

98 

99 return elements 

100 

101 

102def parse_field_path(api_repr: str): 

103 """Parse a **field path** from into a list of nested field names. 

104 

105 See :func:`field_path` for more on **field paths**. 

106 

107 Args: 

108 api_repr (str): 

109 The unique Firestore api representation which consists of 

110 either simple or UTF-8 field names. It cannot exceed 

111 1500 bytes, and cannot be empty. Simple field names match 

112 ``'^[_a-zA-Z][_a-zA-Z0-9]*$'``. All other field names are 

113 escaped by surrounding them with backticks. 

114 

115 Returns: 

116 List[str, ...]: The list of field names in the field path. 

117 """ 

118 # code dredged back up from 

119 # https://github.com/googleapis/google-cloud-python/pull/5109/files 

120 field_names = [] 

121 for field_name in split_field_path(api_repr): 

122 # non-simple field name 

123 if field_name[0] == "`" and field_name[-1] == "`": 

124 field_name = field_name[1:-1] 

125 field_name = field_name.replace(_ESCAPED_BACKTICK, _BACKTICK) 

126 field_name = field_name.replace(_ESCAPED_BACKSLASH, _BACKSLASH) 

127 field_names.append(field_name) 

128 return field_names 

129 

130 

131def render_field_path(field_names: Iterable[str]): 

132 """Create a **field path** from a list of nested field names. 

133 

134 A **field path** is a ``.``-delimited concatenation of the field 

135 names. It is used to represent a nested field. For example, 

136 in the data 

137 

138 .. code-block:: python 

139 

140 data = { 

141 'aa': { 

142 'bb': { 

143 'cc': 10, 

144 }, 

145 }, 

146 } 

147 

148 the field path ``'aa.bb.cc'`` represents that data stored in 

149 ``data['aa']['bb']['cc']``. 

150 

151 Args: 

152 field_names: The list of field names. 

153 

154 Returns: 

155 str: The ``.``-delimited field path. 

156 """ 

157 result = [] 

158 

159 for field_name in field_names: 

160 match = _SIMPLE_FIELD_NAME.match(field_name) 

161 if match and match.group(0) == field_name: 

162 result.append(field_name) 

163 else: 

164 replaced = field_name.replace(_BACKSLASH, _ESCAPED_BACKSLASH).replace( 

165 _BACKTICK, _ESCAPED_BACKTICK 

166 ) 

167 result.append(_BACKTICK + replaced + _BACKTICK) 

168 

169 return _FIELD_PATH_DELIMITER.join(result) 

170 

171 

172get_field_path = render_field_path # backward-compatibility 

173 

174 

175def get_nested_value(field_path: str, data: dict): 

176 """Get a (potentially nested) value from a dictionary. 

177 

178 If the data is nested, for example: 

179 

180 .. code-block:: python 

181 

182 >>> data 

183 { 

184 'top1': { 

185 'middle2': { 

186 'bottom3': 20, 

187 'bottom4': 22, 

188 }, 

189 'middle5': True, 

190 }, 

191 'top6': b'\x00\x01 foo', 

192 } 

193 

194 a **field path** can be used to access the nested data. For 

195 example: 

196 

197 .. code-block:: python 

198 

199 >>> get_nested_value('top1', data) 

200 { 

201 'middle2': { 

202 'bottom3': 20, 

203 'bottom4': 22, 

204 }, 

205 'middle5': True, 

206 } 

207 >>> get_nested_value('top1.middle2', data) 

208 { 

209 'bottom3': 20, 

210 'bottom4': 22, 

211 } 

212 >>> get_nested_value('top1.middle2.bottom3', data) 

213 20 

214 

215 See :meth:`~google.cloud.firestore_v1.client.Client.field_path` for 

216 more information on **field paths**. 

217 

218 Args: 

219 field_path (str): A field path (``.``-delimited list of 

220 field names). 

221 data (Dict[str, Any]): The (possibly nested) data. 

222 

223 Returns: 

224 Any: (A copy of) the value stored for the ``field_path``. 

225 

226 Raises: 

227 KeyError: If the ``field_path`` does not match nested data. 

228 """ 

229 field_names = parse_field_path(field_path) 

230 

231 nested_data = data 

232 for index, field_name in enumerate(field_names): 

233 if isinstance(nested_data, abc.Mapping): 

234 if field_name in nested_data: 

235 nested_data = nested_data[field_name] 

236 else: 

237 if index == 0: 

238 msg = _FIELD_PATH_MISSING_TOP.format(field_name) 

239 raise KeyError(msg) 

240 else: 

241 partial = render_field_path(field_names[:index]) 

242 msg = _FIELD_PATH_MISSING_KEY.format(field_name, partial) 

243 raise KeyError(msg) 

244 else: 

245 partial = render_field_path(field_names[:index]) 

246 msg = _FIELD_PATH_WRONG_TYPE.format(partial, field_name) 

247 raise KeyError(msg) 

248 

249 return nested_data 

250 

251 

252class FieldPath(object): 

253 """Field Path object for client use. 

254 

255 A field path is a sequence of element keys, separated by periods. 

256 Each element key can be either a simple identifier, or a full unicode 

257 string. 

258 

259 In the string representation of a field path, non-identifier elements 

260 must be quoted using backticks, with internal backticks and backslashes 

261 escaped with a backslash. 

262 

263 Args: 

264 parts: (one or more strings) 

265 Indicating path of the key to be used. 

266 """ 

267 

268 def __init__(self, *parts): 

269 for part in parts: 

270 if not isinstance(part, str) or not part: 

271 error = "One or more components is not a string or is empty." 

272 raise ValueError(error) 

273 self.parts = tuple(parts) 

274 

275 @classmethod 

276 def from_api_repr(cls, api_repr: str): 

277 """Factory: create a FieldPath from the string formatted per the API. 

278 

279 Args: 

280 api_repr (str): a string path, with non-identifier elements quoted 

281 It cannot exceed 1500 characters, and cannot be empty. 

282 Returns: 

283 (:class:`FieldPath`) An instance parsed from ``api_repr``. 

284 Raises: 

285 ValueError if the parsing fails 

286 """ 

287 api_repr = api_repr.strip() 

288 if not api_repr: 

289 raise ValueError("Field path API representation cannot be empty.") 

290 return cls(*parse_field_path(api_repr)) 

291 

292 @classmethod 

293 def from_string(cls, path_string: str): 

294 """Factory: create a FieldPath from a unicode string representation. 

295 

296 This method splits on the character `.` and disallows the 

297 characters `~*/[]`. To create a FieldPath whose components have 

298 those characters, call the constructor. 

299 

300 Args: 

301 path_string (str): A unicode string which cannot contain 

302 `~*/[]` characters, cannot exceed 1500 bytes, and cannot be empty. 

303 

304 Returns: 

305 (:class:`FieldPath`) An instance parsed from ``path_string``. 

306 """ 

307 try: 

308 return cls.from_api_repr(path_string) 

309 except ValueError: 

310 elements = path_string.split(".") 

311 for element in elements: 

312 if not element: 

313 raise ValueError("Empty element") 

314 if _LEADING_ALPHA_INVALID.match(element): 

315 raise ValueError( 

316 "Non-alphanum char in element with leading alpha: {}".format( 

317 element 

318 ) 

319 ) 

320 return FieldPath(*elements) 

321 

322 def __repr__(self): 

323 paths = "" 

324 for part in self.parts: 

325 paths += "'" + part + "'," 

326 paths = paths[:-1] 

327 return "FieldPath({})".format(paths) 

328 

329 def __hash__(self): 

330 return hash(self.to_api_repr()) 

331 

332 def __eq__(self, other): 

333 if isinstance(other, FieldPath): 

334 return self.parts == other.parts 

335 return NotImplemented 

336 

337 def __lt__(self, other): 

338 if isinstance(other, FieldPath): 

339 return self.parts < other.parts 

340 return NotImplemented 

341 

342 def __add__(self, other): 

343 """Adds `other` field path to end of this field path. 

344 

345 Args: 

346 other (~google.cloud.firestore_v1._helpers.FieldPath, str): 

347 The field path to add to the end of this `FieldPath`. 

348 """ 

349 if isinstance(other, FieldPath): 

350 parts = self.parts + other.parts 

351 return FieldPath(*parts) 

352 elif isinstance(other, str): 

353 parts = self.parts + FieldPath.from_string(other).parts 

354 return FieldPath(*parts) 

355 else: 

356 return NotImplemented 

357 

358 def to_api_repr(self): 

359 """Render a quoted string representation of the FieldPath 

360 

361 Returns: 

362 (str) Quoted string representation of the path stored 

363 within this FieldPath. 

364 """ 

365 return render_field_path(self.parts) 

366 

367 def eq_or_parent(self, other): 

368 """Check whether ``other`` is an ancestor. 

369 

370 Returns: 

371 (bool) True IFF ``other`` is an ancestor or equal to ``self``, 

372 else False. 

373 """ 

374 return self.parts[: len(other.parts)] == other.parts[: len(self.parts)] 

375 

376 def lineage(self): 

377 """Return field paths for all parents. 

378 

379 Returns: Set[:class:`FieldPath`] 

380 """ 

381 indexes = range(1, len(self.parts)) 

382 return {FieldPath(*self.parts[:index]) for index in indexes} 

383 

384 @staticmethod 

385 def document_id(): 

386 """A special FieldPath value to refer to the ID of a document. It can be used 

387 in queries to sort or filter by the document ID. 

388 

389 Returns: A special sentinel value to refer to the ID of a document. 

390 """ 

391 return "__name__"