/src/compas/data/data.py
Python | 290 lines | 239 code | 20 blank | 31 comment | 6 complexity | e91c974c6c9250e47d3de3273195e006 MD5 | raw file
- from __future__ import print_function
- from __future__ import absolute_import
- from __future__ import division
- import os
- import json
- from uuid import uuid4
- from copy import deepcopy
- import compas
- from compas.data.encoders import DataEncoder
- from compas.data.encoders import DataDecoder
- # ==============================================================================
- # If you ever feel tempted to use ABCMeta in your code: don't, just DON'T.
- # Assigning __metaclass__ = ABCMeta to a class causes a severe memory leak/performance
- # degradation on IronPython 2.7.
- # See these issues for more details:
- # - https://github.com/compas-dev/compas/issues/562
- # - https://github.com/compas-dev/compas/issues/649
- # ==============================================================================
- class Data(object):
- """Abstract base class for all COMPAS data objects.
- Attributes
- ----------
- DATASCHEMA : :class:`schema.Schema`
- The schema of the data dict.
- JSONSCHEMA : dict
- The schema of the serialized data dict.
- data : dict
- The fundamental data describing the object.
- The structure of the data dict is defined by the implementing classes.
- """
- def __init__(self, name=None):
- self._guid = None
- self._name = None
- self._jsondefinitions = None
- self._JSONSCHEMA = None
- self._jsonvalidator = None
- if name:
- self.name = name
- def __getstate__(self):
- """Return the object data for state serialization with older pickle protocols."""
- return {'__dict__': self.__dict__, 'dtype': self.dtype, 'data': self.data}
- def __setstate__(self, state):
- """Assign a deserialized state to the object data to support older pickle protocols."""
- self.__dict__.update(state['__dict__'])
- self.data = state['data']
- @property
- def DATASCHEMA(self):
- """:class:`schema.Schema` : The schema of the data of this object."""
- raise NotImplementedError
- @property
- def JSONSCHEMANAME(self):
- raise NotImplementedError
- @property
- def JSONSCHEMA(self):
- """dict : The schema of the JSON representation of the data of this object."""
- if not self._JSONSCHEMA:
- schema_filename = '{}.json'.format(self.JSONSCHEMANAME.lower())
- schema_path = os.path.join(os.path.dirname(__file__), 'schemas', schema_filename)
- with open(schema_path, 'r') as fp:
- self._JSONSCHEMA = json.load(fp)
- return self._JSONSCHEMA
- @property
- def jsondefinitions(self):
- """dict : Reusable schema definitions."""
- if not self._jsondefinitions:
- schema_path = os.path.join(os.path.dirname(__file__), 'schemas', 'compas.json')
- with open(schema_path, 'r') as fp:
- self._jsondefinitions = json.load(fp)
- return self._jsondefinitions
- @property
- def jsonvalidator(self):
- """:class:`jsonschema.Draft7Validator` : JSON schema validator for draft 7."""
- if not self._jsonvalidator:
- from jsonschema import RefResolver, Draft7Validator
- resolver = RefResolver.from_schema(self.jsondefinitions)
- self._jsonvalidator = Draft7Validator(self.JSONSCHEMA, resolver=resolver)
- return self._jsonvalidator
- @property
- def dtype(self):
- """str : The type of the object in the form of a '2-level' import and a class name."""
- return '{}/{}'.format('.'.join(self.__class__.__module__.split('.')[:2]), self.__class__.__name__)
- @property
- def data(self):
- """dict : The representation of the object as native Python data.
- The structure of the data is described by the data schema.
- """
- raise NotImplementedError
- @data.setter
- def data(self, data):
- raise NotImplementedError
- @property
- def jsonstring(self):
- """str: The representation of the object data in JSON format."""
- return compas.json_dumps(self.data)
- @property
- def guid(self):
- """str : The globally unique identifier of the object."""
- if not self._guid:
- self._guid = uuid4()
- return self._guid
- @property
- def name(self):
- """str : The name of the object.
- This name is not necessarily unique and can be set by the user.
- """
- if not self._name:
- self._name = self.__class__.__name__
- return self._name
- @name.setter
- def name(self, name):
- self._name = name
- @classmethod
- def from_data(cls, data):
- """Construct an object of this type from the provided data.
- Parameters
- ----------
- data : dict
- The data dictionary.
- Returns
- -------
- :class:`compas.data.Data`
- An object of the type of ``cls``.
- """
- obj = cls()
- obj.data = data
- return obj
- def to_data(self):
- """Convert an object to its native data representation.
- Returns
- -------
- dict
- The data representation of the object as described by the schema.
- """
- return self.data
- @classmethod
- def from_json(cls, filepath):
- """Construct an object from serialized data contained in a JSON file.
- Parameters
- ----------
- filepath : path string, file-like object or URL string
- The path, file or URL to the file for serialization.
- Returns
- -------
- :class:`compas.data.Data`
- An object of the type of ``cls``.
- """
- data = compas.json_load(filepath)
- return cls.from_data(data)
- def to_json(self, filepath, pretty=False):
- """Serialize the data representation of an object to a JSON file.
- Parameters
- ----------
- filepath : path string or file-like object
- The path or file-like object to the file containing the data.
- pretty : bool, optional
- If ``True`` serialize a pretty representation of the data.
- Default is ``False``.
- """
- compas.json_dump(self.data, filepath, pretty)
- @classmethod
- def from_jsonstring(cls, string):
- """Construct an object from serialized data contained in a JSON string.
- Parameters
- ----------
- string : str
- The JSON string.
- Returns
- -------
- :class:`compas.data.Data`
- An object of the type of ``cls``.
- """
- data = compas.json_loads(string)
- return cls.from_data(data)
- def to_jsonstring(self, pretty=False):
- """Serialize the data representation of an object to a JSON string.
- Parameters
- ----------
- pretty : bool, optional
- If ``True`` serialize a pretty representation of the data.
- Default is ``False``.
- Returns
- -------
- str
- A JSON string representation of the data.
- """
- return compas.json_dumps(self.data, pretty)
- def copy(self, cls=None):
- """Make an independent copy of the data object.
- Parameters
- ----------
- cls : :class:`compas.data.Data`, optional
- The type of data object to return.
- Defaults to the type of the current data object.
- Returns
- -------
- :class:`compas.data.Data`
- A separate, but identical data object.
- """
- if not cls:
- cls = type(self)
- return cls.from_data(deepcopy(self.data))
- def validate_data(self):
- """Validate the object's data against its data schema (`self.DATASCHEMA`).
- Returns
- -------
- dict
- The validated data.
- Raises
- ------
- schema.SchemaError
- """
- import schema
- try:
- data = self.DATASCHEMA.validate(self.data)
- except schema.SchemaError as e:
- print("Validation against the data schema of this object failed.")
- raise e
- return data
- def validate_json(self):
- """Validate the object's data against its json schema (`self.JSONSCHEMA`).
- Returns
- -------
- str
- The validated JSON representation of the data.
- Raises
- ------
- jsonschema.exceptions.ValidationError
- """
- import jsonschema
- jsonstring = json.dumps(self.data, cls=DataEncoder)
- jsondata = json.loads(jsonstring, cls=DataDecoder)
- try:
- self.jsonvalidator.validate(jsondata)
- except jsonschema.exceptions.ValidationError as e:
- print("Validation against the JSON schema of this object failed.")
- raise e
- return jsonstring