data.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. # Copyright (C) 2010 Internet Systems Consortium.
  2. #
  3. # Permission to use, copy, modify, and distribute this software for any
  4. # purpose with or without fee is hereby granted, provided that the above
  5. # copyright notice and this permission notice appear in all copies.
  6. #
  7. # THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
  8. # DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
  9. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
  10. # INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
  11. # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
  12. # FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
  13. # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
  14. # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  15. #
  16. # Helper functions for data elements as used in cc-channel and
  17. # configuration. There is no python equivalent for the cpp Element
  18. # class, since data elements are represented by native python types
  19. # (int, real, bool, string, list and dict respectively)
  20. #
  21. import json
  22. import re
  23. class DataNotFoundError(Exception):
  24. """Raised if an identifier does not exist according to a spec file,
  25. or if an item is addressed that is not in the current (or default)
  26. config (such as a nonexistent list or map element)"""
  27. pass
  28. class DataAlreadyPresentError(Exception):
  29. """Raised if there is an attemt to add an element to a list or a
  30. map that is already present in that list or map (i.e. if 'add'
  31. is used when it should be 'set')"""
  32. pass
  33. class DataTypeError(Exception):
  34. """Raised if there is an attempt to set an element that is of a
  35. different type than the type specified in the specification."""
  36. pass
  37. def remove_identical(a, b):
  38. """Removes the values from dict a that are the same as in dict b.
  39. Raises a DataTypeError is a or b is not a dict"""
  40. to_remove = []
  41. if type(a) != dict or type(b) != dict:
  42. raise DataTypeError("Not a dict in remove_identical()")
  43. duplicate_keys = [key for key in a.keys() if key in b and a[key] == b[key]]
  44. for id in duplicate_keys:
  45. del(a[id])
  46. def merge(orig, new):
  47. """Merges the contents of new into orig. If the element is dict
  48. orig and then it goes recursive to merge the maps.
  49. If an element value is None in new it will be removed in orig.
  50. Previously this method was relying on dict.update but this does
  51. not do deep merges in the manner required."""
  52. if type(orig) != dict or type(new) != dict:
  53. raise DataTypeError("Not a dict in merge()")
  54. for key in new.keys():
  55. if (key in orig):
  56. if (type(orig[key]) == dict):
  57. merge(orig[key], new[key])
  58. else:
  59. orig[key] = new[key]
  60. else:
  61. orig[key] = new[key]
  62. remove_null_items(orig)
  63. def remove_null_items(d):
  64. """Recursively removes all (key,value) pairs from d where the
  65. value is None"""
  66. null_keys = []
  67. for key in d.keys():
  68. if type(d[key]) == dict:
  69. remove_null_items(d[key])
  70. elif d[key] is None:
  71. null_keys.append(key)
  72. for k in null_keys:
  73. del d[k]
  74. def _concat_identifier(id_parts):
  75. """Concatenates the given identifier parts into a string,
  76. delimited with the '/' character.
  77. """
  78. return '/'.join(id_parts)
  79. def split_identifier(identifier):
  80. """Splits the given identifier into a list of identifier parts,
  81. as delimited by the '/' character.
  82. Raises a DataTypeError if identifier is not a string."""
  83. if type(identifier) != str:
  84. raise DataTypeError("identifier is not a string")
  85. id_parts = identifier.split('/')
  86. id_parts[:] = (value for value in id_parts if value != "")
  87. return id_parts
  88. def identifier_has_list_index(identifier):
  89. """Returns True if the given identifier string has at least one
  90. list index (with [I], where I is a number"""
  91. return (type(identifier) == str and
  92. re.search("\[\d+\]", identifier) is not None)
  93. def split_identifier_list_indices(identifier):
  94. """Finds list indexes in the given identifier, which are of the
  95. format [integer].
  96. Identifier must be a string.
  97. This will only give the list index for the last 'part' of the
  98. given identifier (as delimited by the '/' sign).
  99. Raises a DataTypeError if the identifier is not a string,
  100. or if the format is bad.
  101. Returns a tuple, where the first element is the string part of
  102. the identifier, and the second element is a list of (nested) list
  103. indices.
  104. Examples:
  105. 'a/b/c' will return ('a/b/c', None)
  106. 'a/b/c[1]' will return ('a/b/c', [1])
  107. 'a/b/c[1][2][3]' will return ('a/b/c', [1, 2, 3])
  108. 'a[0]/b[1]/c[2]' will return ('a[0]/b[1]/c', [2])
  109. """
  110. if type(identifier) != str:
  111. raise DataTypeError("identifier in "
  112. "split_identifier_list_indices() "
  113. "not a string: " + str(identifier))
  114. # We only work on the final 'part' of the identifier
  115. id_parts = split_identifier(identifier)
  116. id_str = id_parts[-1]
  117. i = id_str.find('[')
  118. if i < 0:
  119. if id_str.find(']') >= 0:
  120. raise DataTypeError("Bad format in identifier (] but no [): " + str(identifier))
  121. return identifier, None
  122. # keep the non-index part of that to replace later
  123. id = id_str[:i]
  124. indices = []
  125. while i >= 0:
  126. e = id_str.find(']')
  127. if e < i + 1:
  128. raise DataTypeError("Bad format in identifier (] before [): " + str(identifier))
  129. try:
  130. indices.append(int(id_str[i+1:e]))
  131. except ValueError:
  132. raise DataTypeError("List index in " + identifier + " not an integer")
  133. id_str = id_str[e + 1:]
  134. i = id_str.find('[')
  135. if i > 0:
  136. raise DataTypeError("Bad format in identifier ([ within []): " + str(identifier))
  137. if id.find(']') >= 0 or len(id_str) > 0:
  138. raise DataTypeError("Bad format in identifier (extra ]): " + str(identifier))
  139. # we replace the final part of the original identifier with
  140. # the stripped string
  141. id_parts[-1] = id
  142. id = _concat_identifier(id_parts)
  143. return id, indices
  144. def _find_child_el(element, id):
  145. """Finds the child of element with the given id. If the id contains
  146. [i], where i is a number, and the child element is a list, the
  147. i-th element of that list is returned instead of the list itself.
  148. Raises a DataTypeError if the element is of wrong type, if id
  149. is not a string, or if the id string contains a bad value.
  150. Raises a DataNotFoundError if the element at id could not be
  151. found.
  152. """
  153. id, list_indices = split_identifier_list_indices(id)
  154. if type(element) == dict and id in element.keys():
  155. result = element[id]
  156. else:
  157. raise DataNotFoundError(id + " in " + str(element))
  158. if type(result) == list and list_indices is not None:
  159. for list_index in list_indices:
  160. if list_index >= len(result):
  161. raise DataNotFoundError("Element " + str(list_index) + " in " + str(result))
  162. result = result[list_index]
  163. return result
  164. def find(element, identifier):
  165. """Returns the subelement in the given data element, raises
  166. DataNotFoundError if not found.
  167. Returns the given element if the identifier is an empty string.
  168. Raises a DataTypeError if identifier is not a string, or if
  169. identifier is not empty, and element is not a dict.
  170. """
  171. if type(identifier) != str:
  172. raise DataTypeError("identifier in find() is not a str")
  173. if identifier == "":
  174. return element
  175. if type(element) != dict:
  176. raise DataTypeError("element in find() is not a dict")
  177. id_parts = split_identifier(identifier)
  178. cur_el = element
  179. for id in id_parts:
  180. cur_el = _find_child_el(cur_el, id)
  181. return cur_el
  182. def set(element, identifier, value):
  183. """Sets the value at the element specified by identifier to value.
  184. If the value is None, it is removed from the dict. If element
  185. is not a dict, or if the identifier points to something that is
  186. not, a DataTypeError is raised. The element is updated inline,
  187. so if the original needs to be kept, you must make a copy before
  188. calling set(). The updated base element is returned (so that
  189. el.set().set().set() is possible)"""
  190. if type(element) != dict:
  191. raise DataTypeError("element in set() is not a dict")
  192. if type(identifier) != str:
  193. raise DataTypeError("identifier in set() is not a str")
  194. id_parts = split_identifier(identifier)
  195. cur_el = element
  196. for id in id_parts[:-1]:
  197. try:
  198. cur_el = _find_child_el(cur_el, id)
  199. except DataNotFoundError:
  200. if value is None:
  201. # ok we are unsetting a value that wasn't set in
  202. # the first place. Simply stop.
  203. return
  204. cur_el[id] = {}
  205. cur_el = cur_el[id]
  206. id, list_indices = split_identifier_list_indices(id_parts[-1])
  207. if list_indices is None:
  208. # value can be an empty list or dict, so check for None explicitly
  209. if value is not None:
  210. cur_el[id] = value
  211. else:
  212. del cur_el[id]
  213. else:
  214. cur_el = cur_el[id]
  215. # in case of nested lists, we need to get to the next to last
  216. for list_index in list_indices[:-1]:
  217. if type(cur_el) != list:
  218. raise DataTypeError("Element at " + identifier + " is not a list")
  219. if len(cur_el) <= list_index:
  220. raise DataNotFoundError("List index at " + identifier + " out of range")
  221. cur_el = cur_el[list_index]
  222. # value can be an empty list or dict, so check for None explicitly
  223. list_index = list_indices[-1]
  224. if type(cur_el) != list:
  225. raise DataTypeError("Element at " + identifier + " is not a list")
  226. if len(cur_el) <= list_index:
  227. raise DataNotFoundError("List index at " + identifier + " out of range")
  228. if value is not None:
  229. cur_el[list_index] = value
  230. else:
  231. del cur_el[list_index]
  232. return element
  233. def unset(element, identifier):
  234. """Removes the element at the given identifier if it exists. Raises
  235. a DataTypeError if element is not a dict or if identifier is not
  236. a string. Returns the base element."""
  237. # perhaps we can simply do with set none, and remove this whole
  238. # function
  239. return set(element, identifier, None)
  240. def find_no_exc(element, identifier):
  241. """Returns the subelement in the given data element, returns None
  242. if not found, or if an error occurred (i.e. this function should
  243. never raise an exception)"""
  244. try:
  245. return find(element, identifier)
  246. except DataNotFoundError:
  247. return None
  248. except DataTypeError:
  249. return None
  250. def parse_value_str(value_str):
  251. """Parses the given string to a native python object. If the
  252. string cannot be parsed, it is returned. If it is not a string,
  253. None is returned"""
  254. if type(value_str) != str:
  255. return None
  256. try:
  257. return json.loads(value_str)
  258. except ValueError as ve:
  259. # simply return the string itself
  260. return value_str