/
data.py
361 lines (286 loc) · 10.1 KB
/
data.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
try:
from typing import TypeVar # noqa: F401
D = TypeVar("D", bound="Data")
except ImportError:
pass
import hashlib
from uuid import uuid4
from uuid import UUID
from copy import deepcopy
import compas
# ==============================================================================
# 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.
Parameters
----------
name : str, optional
The name of the object.
Attributes
----------
dtype : str, read-only
The type of the object in the form of a fully qualified module name and a class name, separated by a forward slash ("/").
For example: ``"compas.datastructures/Mesh"``.
data : dict
The representation of the object as a dictionary containing only built-in Python data types.
The structure of the dict is described by the data schema.
guid : str, read-only
The globally unique identifier of the object.
The guid is generated with ``uuid.uuid4()``.
name : str
The name of the object.
This name is not necessarily unique and can be set by the user.
The default value is the object's class name: ``self.__class__.__name__``.
See Also
--------
:class:`compas.data.DataEncoder`
:class:`compas.data.DataDecoder`
Notes
-----
Objects created from classes that implement this data class
can be serialized to JSON and unserialized without loss of information using:
* :func:`compas.data.json_dump`
* :func:`compas.data.json_dumps`
* :func:`compas.data.json_load`
* :func:`compas.data.json_loads`
"""
DATASCHEMA = {}
def __init__(self, name=None, **kwargs):
self._guid = None
self.attributes = kwargs or {}
if name:
self.name = name
def __jsondump__(self, minimal=False):
"""Return the required information for serialization with the COMPAS JSON serializer.
Parameters
----------
minimal : bool, optional
If True, exclude the GUID from the dump dict.
Returns
-------
dict
"""
state = {
"dtype": self.dtype,
"data": self.data,
}
if self.attributes:
state["attrs"] = self.attributes
if minimal:
return state
state["guid"] = str(self.guid)
return state
@classmethod
def __jsonload__(cls, data, guid=None, attrs=None):
"""Construct an object of this type from the provided data to support COMPAS JSON serialization.
Parameters
----------
data : dict
The raw Python data representing the object.
guid : str, optional
The GUID of the object.
attrs : dict, optional
The additional attributes of the object.
Returns
-------
object
"""
obj = cls.from_data(data)
if guid is not None:
obj._guid = UUID(guid)
if attrs is not None:
obj.attributes.update(attrs)
return obj
def __getstate__(self):
state = self.__jsondump__()
state["__dict__"] = self.__dict__
return state
def __setstate__(self, state):
self.__dict__.update(state["__dict__"])
if "guid" in state:
self._guid = UUID(state["guid"])
# could be that this is already taken care of by the first line
if "attrs" in state:
self.attributes.update(state["attrs"])
@property
def dtype(self):
return "{}/{}".format(".".join(self.__class__.__module__.split(".")[:2]), self.__class__.__name__)
@property
def data(self):
raise NotImplementedError
def ToString(self):
"""Converts the instance to a string.
This method exists for .NET compatibility. When using IronPython,
the implicit string conversion that usually takes place in CPython
will not kick-in, and in its place, IronPython will default to
printing self.GetType().FullName or similar. Overriding the `ToString`
method of .NET object class fixes that and makes Rhino/Grasshopper
display proper string representations when the objects are printed or
connected to a panel or other type of string output.
"""
return str(self)
@property
def guid(self):
if not self._guid:
self._guid = uuid4()
return self._guid
@property
def name(self):
return self.attributes.get("name") or self.__class__.__name__
@name.setter
def name(self, name):
self.attributes["name"] = name
@classmethod
def from_data(cls, data): # type: (dict) -> Data
"""Construct an object of this type from the provided data.
Parameters
----------
data : dict
The data dictionary.
Returns
-------
:class:`compas.data.Data`
An instance of this object type if the data contained in the dict has the correct schema.
"""
return cls(**data)
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): # type: (...) -> Data
"""Construct an object of this type from a JSON file.
Parameters
----------
filepath : str
The path to the JSON file.
Returns
-------
:class:`compas.data.Data`
An instance of this object type if the data contained in the file has the correct schema.
Raises
------
TypeError
If the data in the file is not a :class:`compas.data.Data`.
"""
data = compas.json_load(filepath)
if not isinstance(data, cls):
raise TypeError("The data in the file is not a {}.".format(cls))
return data
def to_json(self, filepath, pretty=False):
"""Convert an object to its native data representation and save it to a JSON file.
Parameters
----------
filepath : str
The path to the JSON file.
pretty : bool, optional
If True, the JSON file will be pretty printed.
Defaults to False.
"""
compas.json_dump(self, filepath, pretty=pretty)
@classmethod
def from_jsonstring(cls, string): # type: (...) -> Data
"""Construct an object of this type from a JSON string.
Parameters
----------
string : str
The JSON string.
Returns
-------
:class:`compas.data.Data`
An instance of this object type if the data contained in the string has the correct schema.
Raises
------
TypeError
If the data in the string is not a :class:`compas.data.Data`.
"""
data = compas.json_loads(string)
if not isinstance(data, cls):
raise TypeError("The data in the string is not a {}.".format(cls))
return data
def to_jsonstring(self, pretty=False):
"""Convert an object to its native data representation and save it to a JSON string.
Parameters
----------
pretty : bool, optional
If True, the JSON string will be pretty printed.
Defaults to False.
Returns
-------
str
The JSON string.
"""
return compas.json_dumps(self, pretty=pretty)
def copy(self, cls=None): # type: (...) -> D
"""Make an independent copy of the data object.
Parameters
----------
cls : Type[: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`
An independent copy of this object.
"""
if not cls:
cls = type(self)
obj = cls.from_data(deepcopy(self.data))
obj.attributes = deepcopy(self.attributes)
return obj # type: ignore
def sha256(self, as_string=False):
"""Compute a hash of the data for comparison during version control using the sha256 algorithm.
Parameters
----------
as_string : bool, optional
If True, return the digest in hexadecimal format rather than as bytes.
Returns
-------
bytes | str
Examples
--------
>>> from compas.datastructures import Mesh
>>> mesh = Mesh.from_obj(compas.get('faces.obj'))
>>> v1 = mesh.sha256()
>>> v2 = mesh.sha256()
>>> mesh.vertex_attribute(mesh.vertex_sample(1)[0], 'z', 1)
>>> v3 = mesh.sha256()
>>> v1 == v2
True
>>> v2 == v3
False
"""
h = hashlib.sha256()
h.update(compas.json_dumps(self).encode())
if as_string:
return h.hexdigest()
return h.digest()
@classmethod
def validate_data(cls, data):
"""Validate the data against the object's data schema.
The data is the raw data that can be used to construct an object of this type with the classmethod ``from_data``.
Parameters
----------
data : Any
The data for validation.
Returns
-------
Any
"""
from jsonschema import Draft202012Validator
validator = Draft202012Validator(cls.DATASCHEMA) # type: ignore
validator.validate(data)
return data