sdata 1.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sdata/__init__.py +140 -0
- sdata/base.py +684 -0
- sdata/contrib/__init__.py +0 -0
- sdata/contrib/piexif/__init__.py +12 -0
- sdata/contrib/piexif/_common.py +94 -0
- sdata/contrib/piexif/_dump.py +346 -0
- sdata/contrib/piexif/_exceptions.py +2 -0
- sdata/contrib/piexif/_exif.py +638 -0
- sdata/contrib/piexif/_insert.py +60 -0
- sdata/contrib/piexif/_load.py +270 -0
- sdata/contrib/piexif/_remove.py +56 -0
- sdata/contrib/piexif/_transplant.py +45 -0
- sdata/contrib/piexif/_webp.py +240 -0
- sdata/contrib/piexif/helper.py +66 -0
- sdata/contrib/ranger/__init__.py +8 -0
- sdata/contrib/ranger/collections/__init__.py +0 -0
- sdata/contrib/ranger/collections/rangebucketmap.py +346 -0
- sdata/contrib/ranger/collections/rangemap.py +396 -0
- sdata/contrib/ranger/collections/rangeset.py +483 -0
- sdata/contrib/ranger/range/__init__.py +0 -0
- sdata/contrib/ranger/range/cut.py +285 -0
- sdata/contrib/ranger/range/range.py +644 -0
- sdata/contrib/sobol_seq.py +394 -0
- sdata/contrib/sortedcontainers/LICENSE +13 -0
- sdata/contrib/sortedcontainers/__init__.py +74 -0
- sdata/contrib/sortedcontainers/sorteddict.py +789 -0
- sdata/contrib/sortedcontainers/sortedlist.py +2646 -0
- sdata/contrib/sortedcontainers/sortedset.py +733 -0
- sdata/contrib/sqlitedict.py +598 -0
- sdata/contrib/timeflake/__init__.py +40 -0
- sdata/contrib/timeflake/flake.py +69 -0
- sdata/contrib/timeflake/utils.py +47 -0
- sdata/deprecated/__init__.py +10 -0
- sdata/deprecated/data.py +1503 -0
- sdata/did/__init__.py +129 -0
- sdata/did/_http.py +67 -0
- sdata/did/base58btc.py +49 -0
- sdata/did/did_github.py +152 -0
- sdata/did/did_peer4.py +155 -0
- sdata/did/did_web.py +222 -0
- sdata/did/diddoc.py +75 -0
- sdata/did/ed25519.py +225 -0
- sdata/did/eddsa.py +107 -0
- sdata/did/errors.py +40 -0
- sdata/did/issue_vc.py +193 -0
- sdata/did/jose.py +126 -0
- sdata/did/keys.py +104 -0
- sdata/did/utils_didkey.py +161 -0
- sdata/did/verify_vp.py +158 -0
- sdata/doe.py +101 -0
- sdata/dtypes.py +510 -0
- sdata/experiments/__init__.py +331 -0
- sdata/experiments/blob0.py +131 -0
- sdata/experiments/file.py +42 -0
- sdata/experiments/filegroup.py +144 -0
- sdata/experiments/image_old.py +313 -0
- sdata/experiments/ks2.py +15 -0
- sdata/experiments/lapshear.py +8 -0
- sdata/experiments/material.py +7 -0
- sdata/experiments/parameters_deprecated.py +394 -0
- sdata/experiments/workbook.py +226 -0
- sdata/format.py +125 -0
- sdata/imagemeta.py +533 -0
- sdata/interactive.py +118 -0
- sdata/iolib/__init__.py +51 -0
- sdata/iolib/hashtree.py +264 -0
- sdata/iolib/json1sqlitestore.py +554 -0
- sdata/iolib/jsonsqlitestore.py +704 -0
- sdata/iolib/pud.py +242 -0
- sdata/iolib/reader.py +262 -0
- sdata/iolib/writer.py +419 -0
- sdata/metadata.py +882 -0
- sdata/parameter.py +414 -0
- sdata/schema.py +273 -0
- sdata/sclass/__init__.py +111 -0
- sdata/sclass/bfo_classes.py +362 -0
- sdata/sclass/blob.py +362 -0
- sdata/sclass/content.py +87 -0
- sdata/sclass/dataframe.py +1251 -0
- sdata/sclass/dataframegroup.py +187 -0
- sdata/sclass/dependency_graph.py +85 -0
- sdata/sclass/filereference.py +124 -0
- sdata/sclass/image.py +218 -0
- sdata/sclass/lazy_namespace.py +155 -0
- sdata/sclass/process.py +383 -0
- sdata/sclass/process_graph.py +221 -0
- sdata/sclass/zipfile.py +135 -0
- sdata/semantic.py +346 -0
- sdata/suuid.py +136 -0
- sdata/timestamp.py +336 -0
- sdata/tools.py +14 -0
- sdata/units.py +602 -0
- sdata/vocab.py +149 -0
- sdata-1.5.0.dist-info/METADATA +264 -0
- sdata-1.5.0.dist-info/RECORD +98 -0
- sdata-1.5.0.dist-info/WHEEL +5 -0
- sdata-1.5.0.dist-info/licenses/LICENSE-MIT +21 -0
- sdata-1.5.0.dist-info/top_level.txt +1 -0
sdata/parameter.py
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Dict, Tuple, List, Union, Optional
|
|
4
|
+
import random
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Distribution:
|
|
10
|
+
def __init__(self, name: str, **params):
|
|
11
|
+
self.name = name
|
|
12
|
+
self.params = params
|
|
13
|
+
self.dtype = float
|
|
14
|
+
|
|
15
|
+
def to_dict(self) -> Dict:
|
|
16
|
+
return {
|
|
17
|
+
"name": self.name,
|
|
18
|
+
"params": self.params
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_dict(cls, data: Dict):
|
|
23
|
+
return cls(name=data["name"], **data.get("params", {}))
|
|
24
|
+
|
|
25
|
+
def generate_value(self, bounds: Union[Tuple[float, float], List[Union[int, str]]]) -> Union[float, int, str]:
|
|
26
|
+
min_val, max_val = bounds
|
|
27
|
+
if self.name == "uniform":
|
|
28
|
+
return random.uniform(min_val, max_val)
|
|
29
|
+
raise ValueError(f"Unsupported distribution: {self.name}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Parameter:
|
|
33
|
+
def __init__(self, name: str, dtype: type, description: Optional[str] = None, fix: bool = False):
|
|
34
|
+
self.name = name
|
|
35
|
+
self.dtype = dtype
|
|
36
|
+
self.description = description or ""
|
|
37
|
+
self.fix = fix
|
|
38
|
+
|
|
39
|
+
def _validate_and_cast_value(self, value):
|
|
40
|
+
if not isinstance(value, self.dtype):
|
|
41
|
+
try:
|
|
42
|
+
return self.dtype(value)
|
|
43
|
+
except (ValueError, TypeError):
|
|
44
|
+
import warnings
|
|
45
|
+
warnings.warn(f"Value {value} could not be cast to {self.dtype}. Keeping the original value.")
|
|
46
|
+
return value
|
|
47
|
+
return value
|
|
48
|
+
|
|
49
|
+
def to_dict(self) -> Dict:
|
|
50
|
+
return {
|
|
51
|
+
"class_name": self.__class__.__name__,
|
|
52
|
+
"name": self.name,
|
|
53
|
+
# "dtype": self.dtype.__name__,
|
|
54
|
+
"description": self.description,
|
|
55
|
+
"fix": self.fix
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def get_subclasses(cls):
|
|
60
|
+
class_name = "Parameter"
|
|
61
|
+
all_subclasses = {}
|
|
62
|
+
subclasses = {subclass.__name__: subclass for subclass in cls.__subclasses__()}
|
|
63
|
+
all_subclasses.update(subclasses)
|
|
64
|
+
for subclassname, subcls in subclasses.items():
|
|
65
|
+
subsubclasses = {subclass.__name__: subclass for subclass in subcls.__subclasses__()}
|
|
66
|
+
# all_subclasses = all_subclasses | subsubclasses
|
|
67
|
+
all_subclasses.update(subsubclasses)
|
|
68
|
+
|
|
69
|
+
return all_subclasses
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_dict(cls, data: Dict):
|
|
73
|
+
class_name = data.get("class_name", "Parameter")
|
|
74
|
+
subclasses = cls.get_subclasses()
|
|
75
|
+
if class_name in subclasses:
|
|
76
|
+
return subclasses[class_name].from_dict(data)
|
|
77
|
+
raise ValueError(f"Unknown class_name '{class_name}' in data.")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Variable(Parameter):
|
|
81
|
+
def __init__(self, name: str, value: Union[bool, float, int, str], dtype: type,
|
|
82
|
+
bounds: Optional[Tuple[float, float]] = None,
|
|
83
|
+
distribution: Optional[Distribution] = None,
|
|
84
|
+
description: Optional[str] = None, fix: bool = False):
|
|
85
|
+
description = description or ""
|
|
86
|
+
super().__init__(name, dtype, description, fix)
|
|
87
|
+
self.bounds = bounds
|
|
88
|
+
self.distribution = distribution or Distribution("uniform")
|
|
89
|
+
self.value = self._validate_and_cast_value(value)
|
|
90
|
+
|
|
91
|
+
def sample_value(self) -> Union[bool, float, int, str]:
|
|
92
|
+
if self.fix:
|
|
93
|
+
return self.value
|
|
94
|
+
sampled = self.distribution.generate_value(self.bounds)
|
|
95
|
+
self.value = self._validate_and_cast_value(sampled)
|
|
96
|
+
return self.value
|
|
97
|
+
|
|
98
|
+
def to_dict(self) -> Dict:
|
|
99
|
+
data = super().to_dict()
|
|
100
|
+
data.update({
|
|
101
|
+
"value": self.value,
|
|
102
|
+
"bounds": self.bounds,
|
|
103
|
+
"distribution": self.distribution.to_dict() if self.distribution else None
|
|
104
|
+
})
|
|
105
|
+
return data
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def from_dict(cls, data: Dict):
|
|
109
|
+
distribution = Distribution.from_dict(data["distribution"]) if data.get("distribution") else None
|
|
110
|
+
return cls(
|
|
111
|
+
name=data["name"],
|
|
112
|
+
value=data["value"],
|
|
113
|
+
bounds=data.get("bounds"),
|
|
114
|
+
distribution=distribution,
|
|
115
|
+
description=data.get("description"),
|
|
116
|
+
fix=data.get("fix", False)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def sample_values(self, n: int = 1) -> List[Union[bool, float, int, str]]:
|
|
120
|
+
"""
|
|
121
|
+
Generate n sampled values based on the defined distribution.
|
|
122
|
+
"""
|
|
123
|
+
if self.fix is True:
|
|
124
|
+
return [self.value] * n
|
|
125
|
+
else:
|
|
126
|
+
return [self.dtype(self.distribution.generate_value(self.bounds)) for _ in range(n)]
|
|
127
|
+
|
|
128
|
+
def __repr__(self):
|
|
129
|
+
try:
|
|
130
|
+
value_bounds = f"{self.min:.2g},{self.max:.2g}"
|
|
131
|
+
except:
|
|
132
|
+
value_bounds = str(self.bounds)
|
|
133
|
+
return f"({self.name}|{self.value}|[{value_bounds}]|{self.dtype.__name__})"
|
|
134
|
+
|
|
135
|
+
def __str__(self):
|
|
136
|
+
return self.__repr__()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class DiscreteVariable(Parameter):
|
|
140
|
+
def __init__(self, name: str, value: Union[bool, float, int, str], dtype: type,
|
|
141
|
+
discrete_values: List[Union[bool, int, str]],
|
|
142
|
+
description: Optional[str] = None, fix: bool = False):
|
|
143
|
+
description = description or ""
|
|
144
|
+
super().__init__(name, dtype, description, fix)
|
|
145
|
+
self.discrete_values = [self._validate_and_cast_value(v) for v in discrete_values]
|
|
146
|
+
self.value = self._validate_and_cast_value(value)
|
|
147
|
+
|
|
148
|
+
def sample_value(self) -> Union[bool, float, int, str]:
|
|
149
|
+
if self.fix:
|
|
150
|
+
return self.value
|
|
151
|
+
self.value = random.choice(self.discrete_values)
|
|
152
|
+
return self.value
|
|
153
|
+
|
|
154
|
+
def to_dict(self) -> Dict:
|
|
155
|
+
data = super().to_dict()
|
|
156
|
+
data.update({
|
|
157
|
+
"value": self.value,
|
|
158
|
+
"discrete_values": self.discrete_values
|
|
159
|
+
})
|
|
160
|
+
return data
|
|
161
|
+
|
|
162
|
+
def __repr__(self):
|
|
163
|
+
try:
|
|
164
|
+
if self.discrete_values and len(self.discrete_values) > 4:
|
|
165
|
+
value_bounds = self.discrete_values[:4] + ["..."]
|
|
166
|
+
elif self.discrete_values:
|
|
167
|
+
value_bounds = self.discrete_values
|
|
168
|
+
else:
|
|
169
|
+
value_bounds = "None"
|
|
170
|
+
except:
|
|
171
|
+
value_bounds = "!"
|
|
172
|
+
return f"({self.name}|{self.value}|[{value_bounds}]|{self.dtype.__name__})"
|
|
173
|
+
|
|
174
|
+
def __str__(self):
|
|
175
|
+
return self.__repr__()
|
|
176
|
+
|
|
177
|
+
def sample_values(self, n: int = 1) -> List[Union[bool, float, int, str]]:
|
|
178
|
+
"""
|
|
179
|
+
Generate n sampled values based on the defined distribution.
|
|
180
|
+
"""
|
|
181
|
+
if self.fix is True:
|
|
182
|
+
return [self.value] * n
|
|
183
|
+
else:
|
|
184
|
+
return [random.choice(self.discrete_values) for _ in range(n)]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# Specialized Variable Classes
|
|
188
|
+
class VariableInt(Variable):
|
|
189
|
+
dtype = int
|
|
190
|
+
|
|
191
|
+
def __init__(self, name: str, value: int, bounds: Optional[Tuple[int, int]] = None,
|
|
192
|
+
distribution: Optional[Distribution] = None, description: Optional[str] = None, fix: bool = False):
|
|
193
|
+
super().__init__(name, value, int, bounds, distribution, description, fix)
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def min(self):
|
|
197
|
+
if isinstance(self.bounds, (list, tuple)):
|
|
198
|
+
return self.dtype(self.bounds[0])
|
|
199
|
+
raise ValueError("min is only available for continuous boundss")
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def max(self):
|
|
203
|
+
if isinstance(self.bounds, (list, tuple)):
|
|
204
|
+
return self.dtype(self.bounds[1])
|
|
205
|
+
raise ValueError("max is only available for continuous boundss")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class VariableFloat(Variable):
|
|
209
|
+
dtype = float
|
|
210
|
+
|
|
211
|
+
def __init__(self, name: str, value: float, bounds: Optional[Tuple[float, float]] = None,
|
|
212
|
+
distribution: Optional[Distribution] = None, description: Optional[str] = None, fix: bool = False):
|
|
213
|
+
if bounds is None:
|
|
214
|
+
bounds = [self.dtype(value), self.dtype(value)]
|
|
215
|
+
super().__init__(name, value, float, bounds, distribution, description, fix)
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def min(self):
|
|
219
|
+
if isinstance(self.bounds, (list, tuple)):
|
|
220
|
+
return self.bounds[0]
|
|
221
|
+
raise ValueError("min is only available for continuous boundss")
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def max(self):
|
|
225
|
+
if isinstance(self.bounds, (list, tuple)):
|
|
226
|
+
return self.bounds[1]
|
|
227
|
+
raise ValueError("ma"
|
|
228
|
+
"x is only available for continuous boundss")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class VariableStr(Variable):
|
|
232
|
+
dtype = str
|
|
233
|
+
|
|
234
|
+
def __init__(self, name: str, value: str, bounds: Optional[Tuple[str, str]] = None,
|
|
235
|
+
distribution: Optional[Distribution] = None, description: Optional[str] = None, fix: bool = False):
|
|
236
|
+
super().__init__(name, value, str, bounds, distribution, description, fix)
|
|
237
|
+
|
|
238
|
+
def sample_values(self, n: int = 1) -> List[Union[bool, float, int, str]]:
|
|
239
|
+
"""
|
|
240
|
+
Generate n sampled values based on the defined distribution.
|
|
241
|
+
"""
|
|
242
|
+
return [self.value] * n
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# Specialized DiscreteVariable Classes
|
|
246
|
+
class DiscreteVariableInt(DiscreteVariable):
|
|
247
|
+
dtype = int
|
|
248
|
+
|
|
249
|
+
def __init__(self, name: str, value: int, discrete_values: List[int],
|
|
250
|
+
description: Optional[str] = None, fix: bool = False):
|
|
251
|
+
super().__init__(name, value, int, discrete_values, description, fix)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class DiscreteVariableFloat(DiscreteVariable):
|
|
255
|
+
dtype = float
|
|
256
|
+
|
|
257
|
+
def __init__(self, name: str, value: float, discrete_values: List[float],
|
|
258
|
+
description: Optional[str] = None, fix: bool = False):
|
|
259
|
+
super().__init__(name, value, float, discrete_values, description, fix)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class DiscreteVariableStr(DiscreteVariable):
|
|
263
|
+
dtype = str
|
|
264
|
+
|
|
265
|
+
def __init__(self, name: str, value: str, discrete_values: List[str],
|
|
266
|
+
description: Optional[str] = None, fix: bool = False):
|
|
267
|
+
super().__init__(name, value, str, discrete_values, description, fix)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class DiscreteVariableBool(DiscreteVariable):
|
|
271
|
+
dtype = bool
|
|
272
|
+
|
|
273
|
+
def __init__(self, name: str, value: bool, discrete_values: Optional[List[bool]] = None,
|
|
274
|
+
description: Optional[str] = None, fix: bool = False):
|
|
275
|
+
discrete_values = [True, False]
|
|
276
|
+
super().__init__(name, value, bool, discrete_values, description, fix)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class ParameterSet:
|
|
280
|
+
def __init__(self, name=None):
|
|
281
|
+
"""
|
|
282
|
+
ParameterSet class to hold multiple _parameter in a dictionary.
|
|
283
|
+
"""
|
|
284
|
+
self.name = name or "N.N."
|
|
285
|
+
self._parameter = {}
|
|
286
|
+
|
|
287
|
+
@property
|
|
288
|
+
def parameters(self):
|
|
289
|
+
return self._parameter
|
|
290
|
+
|
|
291
|
+
def update(self, parameter_list):
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
"""
|
|
295
|
+
for parameter in parameter_list:
|
|
296
|
+
self._parameter[parameter.name] = parameter
|
|
297
|
+
|
|
298
|
+
def __getitem__(self, name):
|
|
299
|
+
return self.get(name)
|
|
300
|
+
|
|
301
|
+
def __contains__(self, item):
|
|
302
|
+
return item in self._parameter.keys()
|
|
303
|
+
|
|
304
|
+
def pop(self, pname):
|
|
305
|
+
try:
|
|
306
|
+
return self._parameter.pop(pname)
|
|
307
|
+
except KeyError as exp:
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
def __iter__(self):
|
|
311
|
+
for x in self._parameter.values():
|
|
312
|
+
yield x
|
|
313
|
+
|
|
314
|
+
def items(self):
|
|
315
|
+
"""
|
|
316
|
+
Provide a dict-like items view for parameters.
|
|
317
|
+
|
|
318
|
+
:return: Items view of parameters.
|
|
319
|
+
"""
|
|
320
|
+
return self._parameter.items()
|
|
321
|
+
|
|
322
|
+
def keys(self):
|
|
323
|
+
"""
|
|
324
|
+
Provide a dict-like keys view for parameters.
|
|
325
|
+
|
|
326
|
+
:return: Keys view of parameters.
|
|
327
|
+
"""
|
|
328
|
+
return self._parameter.keys()
|
|
329
|
+
|
|
330
|
+
def values(self):
|
|
331
|
+
"""
|
|
332
|
+
Provide a dict-like values view for parameters.
|
|
333
|
+
|
|
334
|
+
:return: Values view of parameters.
|
|
335
|
+
"""
|
|
336
|
+
return self._parameter.values()
|
|
337
|
+
|
|
338
|
+
def get(self, name, default=None):
|
|
339
|
+
if self._parameter.get(name) is not None:
|
|
340
|
+
return self._parameter.get(name)
|
|
341
|
+
else:
|
|
342
|
+
return default
|
|
343
|
+
|
|
344
|
+
def set(self, parameter: Parameter):
|
|
345
|
+
"""
|
|
346
|
+
Add a parameter to the set.
|
|
347
|
+
|
|
348
|
+
:param parameter: A Parameter object to add.
|
|
349
|
+
"""
|
|
350
|
+
self._parameter[parameter.name] = parameter
|
|
351
|
+
|
|
352
|
+
def add_parameters(self, parameter_list: List):
|
|
353
|
+
"""
|
|
354
|
+
Add a parameter to the set.
|
|
355
|
+
|
|
356
|
+
:param parameter: A Parameter object to add.
|
|
357
|
+
"""
|
|
358
|
+
for parameter in parameter_list:
|
|
359
|
+
self._parameter[parameter.name] = parameter
|
|
360
|
+
|
|
361
|
+
def to_dict(self) -> Dict[str, Dict]:
|
|
362
|
+
"""
|
|
363
|
+
Convert the entire parameter set to a dictionary.
|
|
364
|
+
|
|
365
|
+
:return: A dictionary of parameter dictionaries.
|
|
366
|
+
"""
|
|
367
|
+
d = {"name": self.name}
|
|
368
|
+
d["parameter"] = {pname: param.to_dict() for pname, param in self._parameter.items()}
|
|
369
|
+
return d
|
|
370
|
+
|
|
371
|
+
@classmethod
|
|
372
|
+
def from_dict(cls, data: Dict):
|
|
373
|
+
"""
|
|
374
|
+
Create a ParameterSet instance from a dictionary.
|
|
375
|
+
|
|
376
|
+
:param data: A dictionary containing parameter set data.
|
|
377
|
+
:return: A ParameterSet instance.
|
|
378
|
+
"""
|
|
379
|
+
instance = cls(name=data.get("name", "N.N."))
|
|
380
|
+
parameters = data.get("parameter", {})
|
|
381
|
+
for pname, param_data in parameters.items():
|
|
382
|
+
parameter = Parameter.from_dict(param_data) # Assuming Parameter has a from_dict method
|
|
383
|
+
instance.set(parameter)
|
|
384
|
+
return instance
|
|
385
|
+
|
|
386
|
+
def to_json(self) -> str:
|
|
387
|
+
"""
|
|
388
|
+
Convert the ParameterSet to a JSON string.
|
|
389
|
+
|
|
390
|
+
:return: JSON string representation of the ParameterSet.
|
|
391
|
+
"""
|
|
392
|
+
import json
|
|
393
|
+
return json.dumps(self.to_dict())
|
|
394
|
+
|
|
395
|
+
@classmethod
|
|
396
|
+
def from_json(cls, json_str: str):
|
|
397
|
+
"""
|
|
398
|
+
Create a ParameterSet instance from a JSON string.
|
|
399
|
+
|
|
400
|
+
:param json_str: JSON string containing parameter set data.
|
|
401
|
+
:return: A ParameterSet instance.
|
|
402
|
+
"""
|
|
403
|
+
data = json.loads(json_str)
|
|
404
|
+
return cls.from_dict(data)
|
|
405
|
+
|
|
406
|
+
def print(self):
|
|
407
|
+
for pname in sorted(self._parameter.keys()):
|
|
408
|
+
logger.info("%s", self._parameter[pname])
|
|
409
|
+
|
|
410
|
+
def __repr__(self):
|
|
411
|
+
return f"ParameterSet({self.name}|{list(self._parameter.keys())})"
|
|
412
|
+
|
|
413
|
+
def __str__(self):
|
|
414
|
+
return self.__repr__()
|
sdata/schema.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Metadaten-Schemata / Templates zum *Vollqualifizieren* von Metadaten.
|
|
3
|
+
|
|
4
|
+
Ein :class:`MetadataSchema` deklariert die erwarteten Attribute einer Datenklasse
|
|
5
|
+
(Name, dtype, Einheit, required, ontology, …) und kann Metadaten dagegen
|
|
6
|
+
*validieren* sowie *vervollständigen* (Defaults/Einheiten/Ontologie auffüllen).
|
|
7
|
+
|
|
8
|
+
Reine-Python-Validierung ist immer verfügbar; mit dem optionalen Extra
|
|
9
|
+
``[schema]`` (jsonschema) lässt sich zusätzlich gegen ein generiertes JSON Schema
|
|
10
|
+
prüfen.
|
|
11
|
+
"""
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Any, List, Optional
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
import sdata.dtypes as dtypes
|
|
18
|
+
from sdata import units
|
|
19
|
+
|
|
20
|
+
try: # optionales Backend ([schema]=jsonschema)
|
|
21
|
+
import jsonschema as _jsonschema
|
|
22
|
+
except ImportError: # pragma: no cover - abhängig vom Environment
|
|
23
|
+
_jsonschema = None
|
|
24
|
+
|
|
25
|
+
__all__ = ["AttrSpec", "MetadataSchema", "TableSchema", "ValidationReport"]
|
|
26
|
+
|
|
27
|
+
# sdata-dtype -> JSON-Schema-Typ
|
|
28
|
+
_JSON_TYPE = {
|
|
29
|
+
"str": "string", "uri": "string", "bytes": "string", "timestamp": "string",
|
|
30
|
+
"int": "integer", "float": "number", "bool": "boolean",
|
|
31
|
+
"list": "array", "json": "object",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _is_empty(value):
|
|
36
|
+
if value is None or value == "":
|
|
37
|
+
return True
|
|
38
|
+
try:
|
|
39
|
+
return bool(np.isnan(value))
|
|
40
|
+
except (TypeError, ValueError):
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class AttrSpec:
|
|
46
|
+
"""Spezifikation eines erwarteten Attributs."""
|
|
47
|
+
name: str
|
|
48
|
+
dtype: str = "str"
|
|
49
|
+
unit: str = "-"
|
|
50
|
+
required: bool = False
|
|
51
|
+
ontology: str = ""
|
|
52
|
+
description: str = ""
|
|
53
|
+
default: Any = None
|
|
54
|
+
allowed: Optional[List[Any]] = None
|
|
55
|
+
|
|
56
|
+
def to_dict(self):
|
|
57
|
+
return {"name": self.name, "dtype": self.dtype, "unit": self.unit,
|
|
58
|
+
"required": self.required, "ontology": self.ontology,
|
|
59
|
+
"description": self.description, "default": self.default,
|
|
60
|
+
"allowed": self.allowed}
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_dict(cls, d):
|
|
64
|
+
return cls(**{k: d[k] for k in d if k in cls.__dataclass_fields__})
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class ValidationReport:
|
|
69
|
+
"""Ergebnis einer Schema-Validierung (truthy, wenn ``ok``)."""
|
|
70
|
+
ok: bool
|
|
71
|
+
missing: List[str] = field(default_factory=list)
|
|
72
|
+
type_errors: List[tuple] = field(default_factory=list)
|
|
73
|
+
unit_errors: List[str] = field(default_factory=list)
|
|
74
|
+
enum_errors: List[str] = field(default_factory=list)
|
|
75
|
+
extra: List[str] = field(default_factory=list)
|
|
76
|
+
messages: List[str] = field(default_factory=list)
|
|
77
|
+
|
|
78
|
+
def __bool__(self):
|
|
79
|
+
return self.ok
|
|
80
|
+
|
|
81
|
+
def _repr_html_(self):
|
|
82
|
+
color = "#2e7d32" if self.ok else "#c62828"
|
|
83
|
+
status = "OK" if self.ok else "INVALID"
|
|
84
|
+
rows = ""
|
|
85
|
+
for label, items in [("missing", self.missing), ("type", self.type_errors),
|
|
86
|
+
("unit", self.unit_errors), ("enum", self.enum_errors),
|
|
87
|
+
("extra", self.extra), ("messages", self.messages)]:
|
|
88
|
+
if items:
|
|
89
|
+
rows += "<tr><td><b>{}</b></td><td>{}</td></tr>".format(label, items)
|
|
90
|
+
return ("<div><b style='color:{}'>ValidationReport: {}</b>"
|
|
91
|
+
"<table>{}</table></div>").format(color, status, rows)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class MetadataSchema:
|
|
95
|
+
"""Sammlung von :class:`AttrSpec` für eine Datenklasse."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, name, specs, topology_class=None, sdata_class=None):
|
|
98
|
+
self.name = name
|
|
99
|
+
self.specs = list(specs)
|
|
100
|
+
self.topology_class = topology_class
|
|
101
|
+
self.sdata_class = sdata_class
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def _by_name(self):
|
|
105
|
+
return {s.name: s for s in self.specs}
|
|
106
|
+
|
|
107
|
+
def to_dict(self):
|
|
108
|
+
return {"name": self.name,
|
|
109
|
+
"topology_class": self.topology_class,
|
|
110
|
+
"sdata_class": self.sdata_class,
|
|
111
|
+
"specs": [s.to_dict() for s in self.specs]}
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def from_dict(cls, d):
|
|
115
|
+
return cls(name=d.get("name", "N.N."),
|
|
116
|
+
specs=[AttrSpec.from_dict(s) for s in d.get("specs", [])],
|
|
117
|
+
topology_class=d.get("topology_class"),
|
|
118
|
+
sdata_class=d.get("sdata_class"))
|
|
119
|
+
|
|
120
|
+
def validate(self, metadata):
|
|
121
|
+
"""Prüfe ``metadata`` gegen das Schema (wirft nie; liefert ValidationReport)."""
|
|
122
|
+
report = ValidationReport(ok=True)
|
|
123
|
+
specs = self._by_name
|
|
124
|
+
for name, spec in specs.items():
|
|
125
|
+
attr = metadata.get(name)
|
|
126
|
+
if spec.required and (attr is None or _is_empty(attr.value)):
|
|
127
|
+
report.missing.append(name)
|
|
128
|
+
if attr is None:
|
|
129
|
+
continue
|
|
130
|
+
if dtypes.resolve(attr.dtype) != dtypes.resolve(spec.dtype):
|
|
131
|
+
report.type_errors.append((name, spec.dtype))
|
|
132
|
+
if spec.unit not in ("-", "") and \
|
|
133
|
+
units.normalize_symbol(attr.unit) != units.normalize_symbol(spec.unit):
|
|
134
|
+
report.unit_errors.append(name)
|
|
135
|
+
if spec.allowed is not None and attr.value not in spec.allowed:
|
|
136
|
+
report.enum_errors.append(name)
|
|
137
|
+
for name in metadata.user_attributes.keys():
|
|
138
|
+
if name not in specs:
|
|
139
|
+
report.extra.append(name)
|
|
140
|
+
report.ok = not (report.missing or report.type_errors
|
|
141
|
+
or report.unit_errors or report.enum_errors)
|
|
142
|
+
return report
|
|
143
|
+
|
|
144
|
+
def apply(self, metadata):
|
|
145
|
+
"""Vervollständige ``metadata`` in-place: fehlende Attribute aus Defaults
|
|
146
|
+
anlegen, vorhandene um Einheit/Ontologie/Beschreibung ergänzen. Gibt
|
|
147
|
+
``metadata`` zurück (für nicht-destruktiv: ``schema.apply(md.copy())``)."""
|
|
148
|
+
for spec in self.specs:
|
|
149
|
+
attr = metadata.get(spec.name)
|
|
150
|
+
if attr is None:
|
|
151
|
+
metadata.set_attr(spec.name, spec.default, dtype=spec.dtype,
|
|
152
|
+
unit=spec.unit, ontology=spec.ontology,
|
|
153
|
+
description=spec.description)
|
|
154
|
+
continue
|
|
155
|
+
if attr.unit in ("-", "", None) and spec.unit not in ("-", ""):
|
|
156
|
+
attr.unit = spec.unit
|
|
157
|
+
if not attr.ontology and spec.ontology:
|
|
158
|
+
attr.ontology = spec.ontology
|
|
159
|
+
if not attr.description and spec.description:
|
|
160
|
+
attr.description = spec.description
|
|
161
|
+
return metadata
|
|
162
|
+
|
|
163
|
+
def to_json_schema(self):
|
|
164
|
+
"""Generiere ein JSON Schema (Draft 2020-12) für ``metadata.get_udict()``."""
|
|
165
|
+
properties = {}
|
|
166
|
+
required = []
|
|
167
|
+
for spec in self.specs:
|
|
168
|
+
prop = {"type": _JSON_TYPE.get(dtypes.resolve(spec.dtype), "string")}
|
|
169
|
+
if spec.description:
|
|
170
|
+
prop["description"] = spec.description
|
|
171
|
+
if spec.allowed is not None:
|
|
172
|
+
prop["enum"] = spec.allowed
|
|
173
|
+
properties[spec.name] = prop
|
|
174
|
+
if spec.required:
|
|
175
|
+
required.append(spec.name)
|
|
176
|
+
schema = {"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
177
|
+
"type": "object", "properties": properties}
|
|
178
|
+
if required:
|
|
179
|
+
schema["required"] = required
|
|
180
|
+
return schema
|
|
181
|
+
|
|
182
|
+
def validate_jsonschema(self, metadata):
|
|
183
|
+
"""Validiere via ``jsonschema`` (falls installiert), sonst native ``validate``."""
|
|
184
|
+
if _jsonschema is None:
|
|
185
|
+
return self.validate(metadata)
|
|
186
|
+
try:
|
|
187
|
+
_jsonschema.validate(metadata.get_udict(), self.to_json_schema())
|
|
188
|
+
return ValidationReport(ok=True)
|
|
189
|
+
except _jsonschema.ValidationError as exp:
|
|
190
|
+
return ValidationReport(ok=False, messages=[str(exp).splitlines()[0]])
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class TableSchema:
|
|
194
|
+
"""Spalten-Schema für eine :class:`~sdata.sclass.dataframe.DataFrame`.
|
|
195
|
+
|
|
196
|
+
Deklariert je Spalte den erwarteten ``name``/``dtype``/``unit``/``required``
|
|
197
|
+
(wiederverwendet :class:`AttrSpec`) und kann ein DataFrame dagegen *validieren*
|
|
198
|
+
sowie dessen ``column_metadata`` aus dem Schema *vervollständigen*.
|
|
199
|
+
|
|
200
|
+
Der dtype wird gegen die tatsächlichen ``df.dtypes`` geprüft, die Einheit gegen
|
|
201
|
+
die ``column_metadata``-Annotation der Spalte.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
def __init__(self, name, columns):
|
|
205
|
+
self.name = name
|
|
206
|
+
self.columns = list(columns)
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def _by_name(self):
|
|
210
|
+
return {s.name: s for s in self.columns}
|
|
211
|
+
|
|
212
|
+
def to_dict(self):
|
|
213
|
+
return {"name": self.name, "columns": [s.to_dict() for s in self.columns]}
|
|
214
|
+
|
|
215
|
+
@classmethod
|
|
216
|
+
def from_dict(cls, d):
|
|
217
|
+
return cls(name=d.get("name", "N.N."),
|
|
218
|
+
columns=[AttrSpec.from_dict(s) for s in d.get("columns", [])])
|
|
219
|
+
|
|
220
|
+
def validate(self, dataframe):
|
|
221
|
+
"""Prüfe ein :class:`DataFrame` gegen das Spalten-Schema.
|
|
222
|
+
|
|
223
|
+
Wirft nie; liefert einen :class:`ValidationReport` mit fehlenden Spalten,
|
|
224
|
+
dtype-Abweichungen (gegen ``df.dtypes``), Einheiten-Abweichungen (gegen
|
|
225
|
+
``column_metadata``) und zusätzlichen (nicht spezifizierten) Spalten.
|
|
226
|
+
"""
|
|
227
|
+
report = ValidationReport(ok=True)
|
|
228
|
+
df = dataframe.df
|
|
229
|
+
cm = dataframe.column_metadata
|
|
230
|
+
present = {str(c) for c in df.columns}
|
|
231
|
+
specs = self._by_name
|
|
232
|
+
for name, spec in specs.items():
|
|
233
|
+
if name not in present:
|
|
234
|
+
report.missing.append(name)
|
|
235
|
+
continue
|
|
236
|
+
actual = dtypes.resolve(str(df[name].dtype))
|
|
237
|
+
if actual is not None and dtypes.resolve(spec.dtype) != actual:
|
|
238
|
+
report.type_errors.append((name, spec.dtype))
|
|
239
|
+
if spec.unit not in ("-", ""):
|
|
240
|
+
attr = cm.get(name)
|
|
241
|
+
attr_unit = attr.unit if attr is not None else "-"
|
|
242
|
+
if units.normalize_symbol(attr_unit) != units.normalize_symbol(spec.unit):
|
|
243
|
+
report.unit_errors.append(name)
|
|
244
|
+
for name in present:
|
|
245
|
+
if name not in specs:
|
|
246
|
+
report.extra.append(name)
|
|
247
|
+
report.ok = not (report.missing or report.type_errors or report.unit_errors)
|
|
248
|
+
return report
|
|
249
|
+
|
|
250
|
+
def apply(self, dataframe):
|
|
251
|
+
"""Vervollständige ``column_metadata`` in-place aus dem Schema.
|
|
252
|
+
|
|
253
|
+
Für jede im df vorhandene Schema-Spalte: fehlende Annotation anlegen bzw.
|
|
254
|
+
leere ``unit``/``ontology``/``description`` auffüllen. Gibt ``dataframe`` zurück.
|
|
255
|
+
"""
|
|
256
|
+
cm = dataframe.column_metadata
|
|
257
|
+
present = {str(c) for c in dataframe.df.columns}
|
|
258
|
+
for spec in self.columns:
|
|
259
|
+
if spec.name not in present:
|
|
260
|
+
continue
|
|
261
|
+
attr = cm.get(spec.name)
|
|
262
|
+
if attr is None:
|
|
263
|
+
cm.set_attr(spec.name, spec.default, dtype=spec.dtype,
|
|
264
|
+
unit=spec.unit, ontology=spec.ontology,
|
|
265
|
+
description=spec.description)
|
|
266
|
+
continue
|
|
267
|
+
if attr.unit in ("-", "", None) and spec.unit not in ("-", ""):
|
|
268
|
+
attr.unit = spec.unit
|
|
269
|
+
if not attr.ontology and spec.ontology:
|
|
270
|
+
attr.ontology = spec.ontology
|
|
271
|
+
if not attr.description and spec.description:
|
|
272
|
+
attr.description = spec.description
|
|
273
|
+
return dataframe
|