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
« 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.
15"""Utilities for managing / converting field paths to / from strings."""
17from collections import abc
19import re
20from typing import Iterable
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)
29_FIELD_PATH_DELIMITER = "."
30_BACKSLASH = "\\"
31_ESCAPED_BACKSLASH = _BACKSLASH * 2
32_BACKTICK = "`"
33_ESCAPED_BACKTICK = _BACKSLASH + _BACKTICK
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)
46def _tokenize_field_path(path: str):
47 """Lex a field path into tokens (including dots).
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:]))
67def split_field_path(path: str):
68 """Split a field path into valid elements (without dots).
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 []
81 elements = []
82 want_dot = False
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
96 if not want_dot or not elements:
97 raise ValueError("Invalid path: {}".format(path))
99 return elements
102def parse_field_path(api_repr: str):
103 """Parse a **field path** from into a list of nested field names.
105 See :func:`field_path` for more on **field paths**.
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.
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
131def render_field_path(field_names: Iterable[str]):
132 """Create a **field path** from a list of nested field names.
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
138 .. code-block:: python
140 data = {
141 'aa': {
142 'bb': {
143 'cc': 10,
144 },
145 },
146 }
148 the field path ``'aa.bb.cc'`` represents that data stored in
149 ``data['aa']['bb']['cc']``.
151 Args:
152 field_names: The list of field names.
154 Returns:
155 str: The ``.``-delimited field path.
156 """
157 result = []
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)
169 return _FIELD_PATH_DELIMITER.join(result)
172get_field_path = render_field_path # backward-compatibility
175def get_nested_value(field_path: str, data: dict):
176 """Get a (potentially nested) value from a dictionary.
178 If the data is nested, for example:
180 .. code-block:: python
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 }
194 a **field path** can be used to access the nested data. For
195 example:
197 .. code-block:: python
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
215 See :meth:`~google.cloud.firestore_v1.client.Client.field_path` for
216 more information on **field paths**.
218 Args:
219 field_path (str): A field path (``.``-delimited list of
220 field names).
221 data (Dict[str, Any]): The (possibly nested) data.
223 Returns:
224 Any: (A copy of) the value stored for the ``field_path``.
226 Raises:
227 KeyError: If the ``field_path`` does not match nested data.
228 """
229 field_names = parse_field_path(field_path)
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)
249 return nested_data
252class FieldPath(object):
253 """Field Path object for client use.
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.
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.
263 Args:
264 parts: (one or more strings)
265 Indicating path of the key to be used.
266 """
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)
275 @classmethod
276 def from_api_repr(cls, api_repr: str):
277 """Factory: create a FieldPath from the string formatted per the API.
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))
292 @classmethod
293 def from_string(cls, path_string: str):
294 """Factory: create a FieldPath from a unicode string representation.
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.
300 Args:
301 path_string (str): A unicode string which cannot contain
302 `~*/[]` characters, cannot exceed 1500 bytes, and cannot be empty.
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)
322 def __repr__(self):
323 paths = ""
324 for part in self.parts:
325 paths += "'" + part + "',"
326 paths = paths[:-1]
327 return "FieldPath({})".format(paths)
329 def __hash__(self):
330 return hash(self.to_api_repr())
332 def __eq__(self, other):
333 if isinstance(other, FieldPath):
334 return self.parts == other.parts
335 return NotImplemented
337 def __lt__(self, other):
338 if isinstance(other, FieldPath):
339 return self.parts < other.parts
340 return NotImplemented
342 def __add__(self, other):
343 """Adds `other` field path to end of this field path.
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
358 def to_api_repr(self):
359 """Render a quoted string representation of the FieldPath
361 Returns:
362 (str) Quoted string representation of the path stored
363 within this FieldPath.
364 """
365 return render_field_path(self.parts)
367 def eq_or_parent(self, other):
368 """Check whether ``other`` is an ancestor.
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)]
376 def lineage(self):
377 """Return field paths for all parents.
379 Returns: Set[:class:`FieldPath`]
380 """
381 indexes = range(1, len(self.parts))
382 return {FieldPath(*self.parts[:index]) for index in indexes}
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.
389 Returns: A special sentinel value to refer to the ID of a document.
390 """
391 return "__name__"