/src/compas/data/data.py

https://github.com/compas-dev/compas
Python | 290 lines | 239 code | 20 blank | 31 comment | 6 complexity | e91c974c6c9250e47d3de3273195e006 MD5 | raw file
  1. from __future__ import print_function
  2. from __future__ import absolute_import
  3. from __future__ import division
  4. import os
  5. import json
  6. from uuid import uuid4
  7. from copy import deepcopy
  8. import compas
  9. from compas.data.encoders import DataEncoder
  10. from compas.data.encoders import DataDecoder
  11. # ==============================================================================
  12. # If you ever feel tempted to use ABCMeta in your code: don't, just DON'T.
  13. # Assigning __metaclass__ = ABCMeta to a class causes a severe memory leak/performance
  14. # degradation on IronPython 2.7.
  15. # See these issues for more details:
  16. # - https://github.com/compas-dev/compas/issues/562
  17. # - https://github.com/compas-dev/compas/issues/649
  18. # ==============================================================================
  19. class Data(object):
  20. """Abstract base class for all COMPAS data objects.
  21. Attributes
  22. ----------
  23. DATASCHEMA : :class:`schema.Schema`
  24. The schema of the data dict.
  25. JSONSCHEMA : dict
  26. The schema of the serialized data dict.
  27. data : dict
  28. The fundamental data describing the object.
  29. The structure of the data dict is defined by the implementing classes.
  30. """
  31. def __init__(self, name=None):
  32. self._guid = None
  33. self._name = None
  34. self._jsondefinitions = None
  35. self._JSONSCHEMA = None
  36. self._jsonvalidator = None
  37. if name:
  38. self.name = name
  39. def __getstate__(self):
  40. """Return the object data for state serialization with older pickle protocols."""
  41. return {'__dict__': self.__dict__, 'dtype': self.dtype, 'data': self.data}
  42. def __setstate__(self, state):
  43. """Assign a deserialized state to the object data to support older pickle protocols."""
  44. self.__dict__.update(state['__dict__'])
  45. self.data = state['data']
  46. @property
  47. def DATASCHEMA(self):
  48. """:class:`schema.Schema` : The schema of the data of this object."""
  49. raise NotImplementedError
  50. @property
  51. def JSONSCHEMANAME(self):
  52. raise NotImplementedError
  53. @property
  54. def JSONSCHEMA(self):
  55. """dict : The schema of the JSON representation of the data of this object."""
  56. if not self._JSONSCHEMA:
  57. schema_filename = '{}.json'.format(self.JSONSCHEMANAME.lower())
  58. schema_path = os.path.join(os.path.dirname(__file__), 'schemas', schema_filename)
  59. with open(schema_path, 'r') as fp:
  60. self._JSONSCHEMA = json.load(fp)
  61. return self._JSONSCHEMA
  62. @property
  63. def jsondefinitions(self):
  64. """dict : Reusable schema definitions."""
  65. if not self._jsondefinitions:
  66. schema_path = os.path.join(os.path.dirname(__file__), 'schemas', 'compas.json')
  67. with open(schema_path, 'r') as fp:
  68. self._jsondefinitions = json.load(fp)
  69. return self._jsondefinitions
  70. @property
  71. def jsonvalidator(self):
  72. """:class:`jsonschema.Draft7Validator` : JSON schema validator for draft 7."""
  73. if not self._jsonvalidator:
  74. from jsonschema import RefResolver, Draft7Validator
  75. resolver = RefResolver.from_schema(self.jsondefinitions)
  76. self._jsonvalidator = Draft7Validator(self.JSONSCHEMA, resolver=resolver)
  77. return self._jsonvalidator
  78. @property
  79. def dtype(self):
  80. """str : The type of the object in the form of a '2-level' import and a class name."""
  81. return '{}/{}'.format('.'.join(self.__class__.__module__.split('.')[:2]), self.__class__.__name__)
  82. @property
  83. def data(self):
  84. """dict : The representation of the object as native Python data.
  85. The structure of the data is described by the data schema.
  86. """
  87. raise NotImplementedError
  88. @data.setter
  89. def data(self, data):
  90. raise NotImplementedError
  91. @property
  92. def jsonstring(self):
  93. """str: The representation of the object data in JSON format."""
  94. return compas.json_dumps(self.data)
  95. @property
  96. def guid(self):
  97. """str : The globally unique identifier of the object."""
  98. if not self._guid:
  99. self._guid = uuid4()
  100. return self._guid
  101. @property
  102. def name(self):
  103. """str : The name of the object.
  104. This name is not necessarily unique and can be set by the user.
  105. """
  106. if not self._name:
  107. self._name = self.__class__.__name__
  108. return self._name
  109. @name.setter
  110. def name(self, name):
  111. self._name = name
  112. @classmethod
  113. def from_data(cls, data):
  114. """Construct an object of this type from the provided data.
  115. Parameters
  116. ----------
  117. data : dict
  118. The data dictionary.
  119. Returns
  120. -------
  121. :class:`compas.data.Data`
  122. An object of the type of ``cls``.
  123. """
  124. obj = cls()
  125. obj.data = data
  126. return obj
  127. def to_data(self):
  128. """Convert an object to its native data representation.
  129. Returns
  130. -------
  131. dict
  132. The data representation of the object as described by the schema.
  133. """
  134. return self.data
  135. @classmethod
  136. def from_json(cls, filepath):
  137. """Construct an object from serialized data contained in a JSON file.
  138. Parameters
  139. ----------
  140. filepath : path string, file-like object or URL string
  141. The path, file or URL to the file for serialization.
  142. Returns
  143. -------
  144. :class:`compas.data.Data`
  145. An object of the type of ``cls``.
  146. """
  147. data = compas.json_load(filepath)
  148. return cls.from_data(data)
  149. def to_json(self, filepath, pretty=False):
  150. """Serialize the data representation of an object to a JSON file.
  151. Parameters
  152. ----------
  153. filepath : path string or file-like object
  154. The path or file-like object to the file containing the data.
  155. pretty : bool, optional
  156. If ``True`` serialize a pretty representation of the data.
  157. Default is ``False``.
  158. """
  159. compas.json_dump(self.data, filepath, pretty)
  160. @classmethod
  161. def from_jsonstring(cls, string):
  162. """Construct an object from serialized data contained in a JSON string.
  163. Parameters
  164. ----------
  165. string : str
  166. The JSON string.
  167. Returns
  168. -------
  169. :class:`compas.data.Data`
  170. An object of the type of ``cls``.
  171. """
  172. data = compas.json_loads(string)
  173. return cls.from_data(data)
  174. def to_jsonstring(self, pretty=False):
  175. """Serialize the data representation of an object to a JSON string.
  176. Parameters
  177. ----------
  178. pretty : bool, optional
  179. If ``True`` serialize a pretty representation of the data.
  180. Default is ``False``.
  181. Returns
  182. -------
  183. str
  184. A JSON string representation of the data.
  185. """
  186. return compas.json_dumps(self.data, pretty)
  187. def copy(self, cls=None):
  188. """Make an independent copy of the data object.
  189. Parameters
  190. ----------
  191. cls : :class:`compas.data.Data`, optional
  192. The type of data object to return.
  193. Defaults to the type of the current data object.
  194. Returns
  195. -------
  196. :class:`compas.data.Data`
  197. A separate, but identical data object.
  198. """
  199. if not cls:
  200. cls = type(self)
  201. return cls.from_data(deepcopy(self.data))
  202. def validate_data(self):
  203. """Validate the object's data against its data schema (`self.DATASCHEMA`).
  204. Returns
  205. -------
  206. dict
  207. The validated data.
  208. Raises
  209. ------
  210. schema.SchemaError
  211. """
  212. import schema
  213. try:
  214. data = self.DATASCHEMA.validate(self.data)
  215. except schema.SchemaError as e:
  216. print("Validation against the data schema of this object failed.")
  217. raise e
  218. return data
  219. def validate_json(self):
  220. """Validate the object's data against its json schema (`self.JSONSCHEMA`).
  221. Returns
  222. -------
  223. str
  224. The validated JSON representation of the data.
  225. Raises
  226. ------
  227. jsonschema.exceptions.ValidationError
  228. """
  229. import jsonschema
  230. jsonstring = json.dumps(self.data, cls=DataEncoder)
  231. jsondata = json.loads(jsonstring, cls=DataDecoder)
  232. try:
  233. self.jsonvalidator.validate(jsondata)
  234. except jsonschema.exceptions.ValidationError as e:
  235. print("Validation against the JSON schema of this object failed.")
  236. raise e
  237. return jsonstring