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.
Files changed (98) hide show
  1. sdata/__init__.py +140 -0
  2. sdata/base.py +684 -0
  3. sdata/contrib/__init__.py +0 -0
  4. sdata/contrib/piexif/__init__.py +12 -0
  5. sdata/contrib/piexif/_common.py +94 -0
  6. sdata/contrib/piexif/_dump.py +346 -0
  7. sdata/contrib/piexif/_exceptions.py +2 -0
  8. sdata/contrib/piexif/_exif.py +638 -0
  9. sdata/contrib/piexif/_insert.py +60 -0
  10. sdata/contrib/piexif/_load.py +270 -0
  11. sdata/contrib/piexif/_remove.py +56 -0
  12. sdata/contrib/piexif/_transplant.py +45 -0
  13. sdata/contrib/piexif/_webp.py +240 -0
  14. sdata/contrib/piexif/helper.py +66 -0
  15. sdata/contrib/ranger/__init__.py +8 -0
  16. sdata/contrib/ranger/collections/__init__.py +0 -0
  17. sdata/contrib/ranger/collections/rangebucketmap.py +346 -0
  18. sdata/contrib/ranger/collections/rangemap.py +396 -0
  19. sdata/contrib/ranger/collections/rangeset.py +483 -0
  20. sdata/contrib/ranger/range/__init__.py +0 -0
  21. sdata/contrib/ranger/range/cut.py +285 -0
  22. sdata/contrib/ranger/range/range.py +644 -0
  23. sdata/contrib/sobol_seq.py +394 -0
  24. sdata/contrib/sortedcontainers/LICENSE +13 -0
  25. sdata/contrib/sortedcontainers/__init__.py +74 -0
  26. sdata/contrib/sortedcontainers/sorteddict.py +789 -0
  27. sdata/contrib/sortedcontainers/sortedlist.py +2646 -0
  28. sdata/contrib/sortedcontainers/sortedset.py +733 -0
  29. sdata/contrib/sqlitedict.py +598 -0
  30. sdata/contrib/timeflake/__init__.py +40 -0
  31. sdata/contrib/timeflake/flake.py +69 -0
  32. sdata/contrib/timeflake/utils.py +47 -0
  33. sdata/deprecated/__init__.py +10 -0
  34. sdata/deprecated/data.py +1503 -0
  35. sdata/did/__init__.py +129 -0
  36. sdata/did/_http.py +67 -0
  37. sdata/did/base58btc.py +49 -0
  38. sdata/did/did_github.py +152 -0
  39. sdata/did/did_peer4.py +155 -0
  40. sdata/did/did_web.py +222 -0
  41. sdata/did/diddoc.py +75 -0
  42. sdata/did/ed25519.py +225 -0
  43. sdata/did/eddsa.py +107 -0
  44. sdata/did/errors.py +40 -0
  45. sdata/did/issue_vc.py +193 -0
  46. sdata/did/jose.py +126 -0
  47. sdata/did/keys.py +104 -0
  48. sdata/did/utils_didkey.py +161 -0
  49. sdata/did/verify_vp.py +158 -0
  50. sdata/doe.py +101 -0
  51. sdata/dtypes.py +510 -0
  52. sdata/experiments/__init__.py +331 -0
  53. sdata/experiments/blob0.py +131 -0
  54. sdata/experiments/file.py +42 -0
  55. sdata/experiments/filegroup.py +144 -0
  56. sdata/experiments/image_old.py +313 -0
  57. sdata/experiments/ks2.py +15 -0
  58. sdata/experiments/lapshear.py +8 -0
  59. sdata/experiments/material.py +7 -0
  60. sdata/experiments/parameters_deprecated.py +394 -0
  61. sdata/experiments/workbook.py +226 -0
  62. sdata/format.py +125 -0
  63. sdata/imagemeta.py +533 -0
  64. sdata/interactive.py +118 -0
  65. sdata/iolib/__init__.py +51 -0
  66. sdata/iolib/hashtree.py +264 -0
  67. sdata/iolib/json1sqlitestore.py +554 -0
  68. sdata/iolib/jsonsqlitestore.py +704 -0
  69. sdata/iolib/pud.py +242 -0
  70. sdata/iolib/reader.py +262 -0
  71. sdata/iolib/writer.py +419 -0
  72. sdata/metadata.py +882 -0
  73. sdata/parameter.py +414 -0
  74. sdata/schema.py +273 -0
  75. sdata/sclass/__init__.py +111 -0
  76. sdata/sclass/bfo_classes.py +362 -0
  77. sdata/sclass/blob.py +362 -0
  78. sdata/sclass/content.py +87 -0
  79. sdata/sclass/dataframe.py +1251 -0
  80. sdata/sclass/dataframegroup.py +187 -0
  81. sdata/sclass/dependency_graph.py +85 -0
  82. sdata/sclass/filereference.py +124 -0
  83. sdata/sclass/image.py +218 -0
  84. sdata/sclass/lazy_namespace.py +155 -0
  85. sdata/sclass/process.py +383 -0
  86. sdata/sclass/process_graph.py +221 -0
  87. sdata/sclass/zipfile.py +135 -0
  88. sdata/semantic.py +346 -0
  89. sdata/suuid.py +136 -0
  90. sdata/timestamp.py +336 -0
  91. sdata/tools.py +14 -0
  92. sdata/units.py +602 -0
  93. sdata/vocab.py +149 -0
  94. sdata-1.5.0.dist-info/METADATA +264 -0
  95. sdata-1.5.0.dist-info/RECORD +98 -0
  96. sdata-1.5.0.dist-info/WHEEL +5 -0
  97. sdata-1.5.0.dist-info/licenses/LICENSE-MIT +21 -0
  98. sdata-1.5.0.dist-info/top_level.txt +1 -0
sdata/metadata.py ADDED
@@ -0,0 +1,882 @@
1
+ # -*-coding: utf-8-*-
2
+ import logging
3
+ logger = logging.getLogger(__name__)
4
+ from sdata import __version__
5
+ import pandas as pd
6
+ import numpy as np
7
+ from sdata.timestamp import TimeStamp
8
+ import sdata.dtypes as dtypes
9
+ import datetime
10
+ from dateutil.parser import parse as parse_date
11
+ from typing import Any, Dict, List, Optional, Tuple, Type, Union
12
+ import json
13
+ import os
14
+ import hashlib
15
+ import re
16
+ import copy
17
+ from sdata.contrib.sortedcontainers.sorteddict import SortedDict
18
+
19
+ def extract_name_unit(value):
20
+ """extract name and unit from a combined string
21
+
22
+ .. code-block:: python
23
+
24
+ value: 'Target Strain Rate (1/s) '
25
+ name : 'Target Strain Rate'
26
+ unit : '1/s'
27
+
28
+ value: 'Gauge Length [mm] monkey '
29
+ name : 'Gauge Length'
30
+ unit : 'mm'
31
+
32
+ value: 'Gauge Length <mm> whatever '
33
+ name : 'Gauge Length'
34
+ unit : 'mm'
35
+
36
+ :param value: string, e.g. 'Length <mm> whatever'
37
+ :return: name, unit
38
+ """
39
+ pattern1 = r'([\w\s\.]+) \(([\w.-\/]+)\)'
40
+ match1 = re.search(pattern1, value)
41
+ pattern2 = r'([\w\s\.]+) \[([\w.-\/]+)\]'
42
+ match2 = re.search(pattern2, value)
43
+ pattern3 = r'([\w\s\.]+) \<([\w.-\/]+)\>'
44
+ match3 = re.search(pattern3, value)
45
+ if match1:
46
+ name = match1.group(1)
47
+ unit = match1.group(2)
48
+ elif match2:
49
+ name = match2.group(1)
50
+ unit = match2.group(2)
51
+ elif match3:
52
+ name = match3.group(1)
53
+ unit = match3.group(2)
54
+ else:
55
+ name = value
56
+ unit = ""
57
+ return name, unit
58
+
59
+ def _to_utc(dt: datetime.datetime) -> datetime.datetime:
60
+ """Sichere UTC-Normalisierung (aware)."""
61
+ if dt.tzinfo is None:
62
+ return dt.replace(tzinfo=datetime.timezone.utc)
63
+ return dt.astimezone(datetime.timezone.utc)
64
+
65
+ class Attribute(object):
66
+ """Attribute class"""
67
+
68
+ # Kompat-Aliasse: identische Inhalte für die 6 Alt-dtypes; die Registry in
69
+ # :mod:`sdata.dtypes` ist die Single Source of Truth (+ bytes/json/uri).
70
+ DTYPES = dtypes.DTYPES
71
+ DTYPES_INV = dtypes.DTYPES_INV
72
+
73
+ def __init__(self, name, value, *, strict=False, **kwargs):
74
+ """Attribute(name, value, dtype=str, unit="-", description="", label="", required=False,
75
+ ontology:"BFO:quality", strict=False)
76
+ :param name
77
+ :param value
78
+ :param dtype ['float', 'int', 'str', 'timestamp', 'bool', 'list', 'bytes', 'json', 'uri']
79
+ :param description
80
+ :param unit
81
+ :param label
82
+ :param required
83
+ :param ontology
84
+ :param strict: bei True wirft fehlgeschlagene Coercion ``dtypes.DtypeError``
85
+ statt still zu degradieren (Default False = bisheriges Verhalten)
86
+ """
87
+ self._strict: bool = bool(strict)
88
+ self._name: str = None
89
+ self._value: Any = None
90
+ self._unit:str = "-"
91
+ self._description:str = ""
92
+ self._label:str = ""
93
+ self._ontology:str = ""
94
+ self._symbol:str = ""
95
+ self._dtype:str = "str"
96
+ dtype = kwargs.get("dtype", None) or self.guess_dtype(value)
97
+ self._set_dtype(dtype)
98
+ self.name = name
99
+ self._set_description(kwargs.get("description", ""))
100
+ self._set_label(kwargs.get("label", ""))
101
+ self._set_unit(kwargs.get("unit", "-"))
102
+ self._set_required(kwargs.get("required", False))
103
+ self._set_ontology(kwargs.get("ontology", ""))
104
+ self._set_symbol(kwargs.get("symbol", ""))
105
+ # set dtype first!
106
+ self._set_value(value)
107
+
108
+ def _get_name(self):
109
+ return self._name
110
+
111
+ def _set_name(self, value):
112
+ if isinstance(value, str):
113
+ try:
114
+ value = value.strip()[:256]
115
+ if len(value) > 0:
116
+ self._name = value
117
+ else:
118
+ raise ValueError("empty Attribute.name")
119
+ except ValueError as exp:
120
+ logger.warning("error Attribute.name: %s" % exp)
121
+ else:
122
+ self._name = str(value).strip()[:256]
123
+
124
+ name = property(fget=_get_name, fset=_set_name, doc="Attribute name")
125
+
126
+ def _get_value(self):
127
+ return self._value
128
+
129
+ def _set_value(self, value):
130
+ spec = dtypes.get(self._dtype) or dtypes.get("str")
131
+ try:
132
+ self._value = spec.coerce(value, strict=self._strict)
133
+ except dtypes.DtypeError as exp:
134
+ if self._strict:
135
+ raise
136
+ # lenient: loggen und vorhandenen Wert unverändert lassen (Altverhalten)
137
+ logger.error("error Attribute.value: {}".format(exp))
138
+
139
+ value = property(fget=_get_value, fset=_set_value, doc="Attribute value")
140
+
141
+ @staticmethod
142
+ def guess_dtype(value):
143
+ """returns dtype class
144
+
145
+ :param value:
146
+ :return: __class__
147
+ """
148
+ if isinstance(value, (int)):
149
+ return value.__class__
150
+ elif isinstance(value, (float)):
151
+ return value.__class__
152
+ elif isinstance(value, (str)):
153
+ return value.__class__
154
+ elif isinstance(value, (list, tuple)):
155
+ return "list[str]"
156
+ else:
157
+ return str
158
+
159
+ def _get_dtype(self):
160
+ return self._dtype
161
+
162
+ def _set_dtype(self, value):
163
+ """set dtype str
164
+
165
+ Akzeptiert dtype-String ('float', 'float64', ...) ODER Klasse (float, int).
166
+ Ist bereits ein Wert gesetzt, wird er in den neuen dtype umgecastet.
167
+
168
+ :param value: dtype-String oder -Klasse
169
+ :return:
170
+ """
171
+ name = dtypes.resolve(value)
172
+ if name is None:
173
+ return None
174
+ self._dtype = name
175
+ # Re-Cast eines bereits gesetzten Werts (während __init__ ist _value noch None)
176
+ if getattr(self, "_value", None) is not None:
177
+ self._set_value(self._value)
178
+
179
+ dtype = property(fget=_get_dtype, fset=_set_dtype, doc="Attribute type str")
180
+
181
+ def _get_description(self):
182
+ return self._description
183
+
184
+ def _set_description(self, value):
185
+ if value is None:
186
+ value = ""
187
+ self._description = str(value)
188
+
189
+ description = property(fget=_get_description, fset=_set_description, doc="Attribute description")
190
+
191
+ def _get_label(self):
192
+ return self._label
193
+
194
+ def _set_label(self, value):
195
+ if value is None:
196
+ value = ""
197
+ self._label = str(value)
198
+
199
+ label = property(fget=_get_label, fset=_set_label, doc="Attribute label")
200
+
201
+ def _get_unit(self):
202
+ return self._unit
203
+
204
+ def _set_unit(self, value):
205
+ self._unit = value
206
+
207
+ unit = property(fget=_get_unit, fset=_set_unit, doc="Attribute unit")
208
+
209
+ def _get_required(self):
210
+ return self._required
211
+
212
+ def _set_required(self, value):
213
+ if value in [True, 1, "true", "True"]:
214
+ self._required = True
215
+ else:
216
+ self._required = False
217
+
218
+ required = property(fget=_get_required, fset=_set_required, doc="Attribute required")
219
+
220
+ def _get_ontology(self):
221
+ return self._ontology
222
+
223
+ def _set_ontology(self, value):
224
+ if value is None:
225
+ value = ""
226
+ self._ontology = str(value)
227
+
228
+ ontology = property(fget=_get_ontology, fset=_set_ontology, doc="Attribute ontology")
229
+
230
+ def _get_symbol(self):
231
+ return self._symbol
232
+
233
+ def _set_symbol(self, value):
234
+ if value is None:
235
+ value = ""
236
+ self._symbol = str(value)
237
+
238
+ symbol = property(fget=_get_symbol, fset=_set_symbol,
239
+ doc="Formelzeichen der Größe, z. B. ``R_p0,2`` (RFC 0016)")
240
+
241
+ def to_dict(self) -> Dict[str, Any]:
242
+ """Return a dict of the attribute's items (name/value/unit/dtype/...)."""
243
+ return {'name': self.name,
244
+ 'value': self.value,
245
+ 'unit': self.unit,
246
+ 'dtype': self.dtype,
247
+ 'description': self.description,
248
+ 'label': self.label,
249
+ 'required': self.required,
250
+ 'ontology': self.ontology,
251
+ 'symbol': self.symbol,
252
+ }
253
+
254
+ def to_list(self):
255
+ return [self.name, self.value, self.unit, self.dtype, self.description, self.label, self.required, self.ontology,
256
+ self.symbol]
257
+
258
+ def to_csv(self, prefix="", sep=",", quote=None):
259
+ """export Attribute to csv
260
+
261
+ :param prefix:
262
+ :param sep:
263
+ :param quote:
264
+ :return:
265
+ """
266
+ xs = []
267
+ for x in self.to_list():
268
+ if x is None:
269
+ xs.append("")
270
+ else:
271
+ xs.append(str(x))
272
+ return "{}{}".format(prefix, sep.join(xs))
273
+
274
+ def __str__(self):
275
+ return "(Attr'%s':%s(%s))" % (self.name, self.value, self.dtype)
276
+
277
+ __repr__ = __str__
278
+
279
+ def _repr_html_(self):
280
+ from sdata import interactive
281
+ return interactive.attribute_html(self)
282
+
283
+
284
+ class Metadata(object):
285
+ """Metadata container class
286
+
287
+ each Metadata entry has has a
288
+ * name (256)
289
+ * value
290
+ * unit
291
+ * description
292
+ * type (int, str, float, bool, timestamp)
293
+ """
294
+
295
+ ATTRIBUTEKEYS = ["name", "value", "unit", "dtype", "description", "label", "required", "ontology", "symbol"]
296
+ #: reservierte Attribut-Schlüssel, der den Objektnamen autoritativ hält
297
+ RESERVED_NAME_KEY = "_sdata_name"
298
+
299
+ def __init__(self, **kwargs):
300
+ """Metadata class
301
+
302
+ :param kwargs:
303
+ """
304
+ self._attributes = SortedDict()
305
+ self._name = kwargs.get("name") or "N.N."
306
+
307
+ def _get_name(self):
308
+ # Single Source of Truth: das reservierte _sdata_name-Attribut ist
309
+ # autoritativ, sobald gesetzt; sonst der einfache _name-Fallback.
310
+ attr = self._attributes.get(self.RESERVED_NAME_KEY)
311
+ if attr is not None and attr.value:
312
+ return attr.value
313
+ return self._name
314
+
315
+ def _set_name(self, value):
316
+ self._name = str(value)
317
+ attr = self._attributes.get(self.RESERVED_NAME_KEY)
318
+ if attr is not None:
319
+ attr.value = str(value)
320
+
321
+ name = property(fget=_get_name, fset=_set_name, doc="Name of the Metadata")
322
+
323
+ def _get_attributes(self):
324
+ return self._attributes
325
+
326
+ def _set_attributes(self, value):
327
+ self._attributes = value
328
+
329
+ attributes = property(fget=_get_attributes, fset=_set_attributes, doc="returns Attributes")
330
+
331
+ @property
332
+ def user_attributes(self):
333
+ attrs = [(a.name, a) for a in self.attributes.values() if not a.name.startswith("_sdata")]
334
+ return SortedDict(attrs)
335
+
336
+ @property
337
+ def sdata_attributes(self):
338
+ attrs = [(a.name, a) for a in self.attributes.values() if a.name.startswith("_sdata")]
339
+ return SortedDict(attrs)
340
+
341
+ @property
342
+ def required_attributes(self):
343
+ required_attributes = [(attr.name, attr) for attr in self.attributes.values() if attr.required is True]
344
+ return SortedDict(required_attributes)
345
+
346
+ def add_attribute(self, attr: Attribute, **kwargs: Any) -> None:
347
+ """set Attribute"""
348
+ prefix: str = kwargs.get("prefix", "")
349
+ if not isinstance(attr, Attribute):
350
+ raise TypeError(f"attr must be an instance of Attribute, got {type(attr).__name__}")
351
+ self._attributes[prefix + attr.name] = attr
352
+
353
+ def set_attr(self, name="N.N.", value=None, **kwargs):
354
+ """set Attribute"""
355
+ prefix = kwargs.get("prefix", "")
356
+ if isinstance(name, Attribute) or "Attribute" in name.__class__.__name__:
357
+ attr = name # name is the Attribute!
358
+ else:
359
+ attr = self.get_attr(prefix + name) or Attribute(name, value, **kwargs)
360
+ for key in ["unit", "dtype", "description", "label", "required", "ontology", "symbol"]:
361
+ if key in kwargs:
362
+ setattr(attr, key, kwargs.get(key))
363
+ if value is not None:
364
+ attr.value = value
365
+ self._attributes[prefix + attr.name] = attr
366
+
367
+ def get_attr(self, name: str) -> Optional["Attribute"]:
368
+ """get Attribute by name"""
369
+ return self._attributes.get(name, None)
370
+
371
+ def to_dict(self) -> Dict[str, Any]:
372
+ """serialize attributes to dict"""
373
+ d = {}
374
+ for attr in self.attributes.values():
375
+ d[attr.name] = attr.to_dict()
376
+ return d
377
+
378
+ @staticmethod
379
+ def guess_dtype_from_value(value):
380
+ """guess dtype from value,
381
+ e.g.
382
+ '1.23' -> 'float'
383
+ 'otto1.23' -> 'str'
384
+ 1 -> 'int'
385
+ False -> 'bool'
386
+
387
+ :param value:
388
+ :return: dtype(value), dtype ['int', 'float', 'bool', 'str', 'list']
389
+ """
390
+ if value.__class__.__name__ in ["int", "float", "bool"]:
391
+ return value, value.__class__.__name__
392
+ elif value in ["False", "True", "true", "false"]:
393
+ return bool(value), 'bool'
394
+ try:
395
+ value = int(value)
396
+ return value, value.__class__.__name__
397
+ except:
398
+ pass
399
+ try:
400
+ value = float(value)
401
+ return value, value.__class__.__name__
402
+ except:
403
+ pass
404
+ return str(value), "str"
405
+
406
+ def update_from_dict(self, d, guess_dtype=True):
407
+ """set attributes from dict
408
+
409
+ :param d: dict
410
+ :return:
411
+ """
412
+ for k, v in d.items():
413
+ if guess_dtype is False:
414
+ value = v
415
+ dtype = None
416
+ else:
417
+ value, dtype = self.guess_dtype_from_value(v)
418
+ if dtype in ["float", "int", "bool"]:
419
+ v = {"name":k, "value":value}
420
+ # v = {"name":k, "value":value, "dtype":dtype, "unit":"", "description":"", "label":"", "required":False}
421
+ elif isinstance(v, (str,)):
422
+ v = {"name":k, "value":v, "dtype":"str", "unit":"", "description":"", "label":"", "required":False, "ontology":""}
423
+ elif hasattr(v, "keys"):
424
+ dtype = v.get("dtype", self.guess_dtype_from_value(v.get("value"))[1])
425
+ value = v.get("value")
426
+ v = {"name":k, "value":value, "dtype":dtype,
427
+ "unit":v.get("unit", ""), "description":v.get("description", ""),
428
+ "label":v.get("label", ""), "required":v.get("required", False),
429
+ "ontology":v.get("ontology", ""), "symbol":v.get("symbol", ""),}
430
+ else:
431
+ v, dtype = self.guess_dtype_from_value(v)
432
+ # v = {"name":k, "value":v, "dtype":dtype, "unit":"", "description":"", "label":"", "required":False}
433
+ v = {"name":k, "value":v, "dtype":dtype}
434
+ self.set_attr(**v)
435
+
436
+ @classmethod
437
+ def from_dict(cls, d):
438
+ """setup metadata from dict"""
439
+ metadata = cls()
440
+ metadata.update_from_dict(d)
441
+ return metadata
442
+
443
+ def _to_dict(self, attributes):
444
+ """
445
+
446
+ :param attributes:
447
+ :return:
448
+ """
449
+ d = {}
450
+ for attr in attributes.values():
451
+ d[attr.name] = attr.to_dict()
452
+ return d
453
+
454
+ def get_sdict(self):
455
+ """get sdata attribute as dict"""
456
+ d = {}
457
+ for attr in self.sdata_attributes.values():
458
+ d[attr.name] = attr.value
459
+ return d
460
+
461
+ def get_udict(self):
462
+ """get user attribute as dict"""
463
+ d = {}
464
+ for attr in self.user_attributes.values():
465
+ d[attr.name] = attr.value
466
+ return d
467
+
468
+ def get_dict(self):
469
+ """get user attribute as dict"""
470
+ d = {}
471
+ for attr in self.attributes.values():
472
+ d[attr.name] = attr.value
473
+ return d
474
+
475
+ def _to_dataframe(self, attributes):
476
+ """create dataframe from attributes"""
477
+ d = self._to_dict(attributes)
478
+ if len(d) == 0:
479
+ df = pd.DataFrame(columns=self.ATTRIBUTEKEYS)
480
+ else:
481
+ df = pd.DataFrame.from_dict(d, orient="index")
482
+ df.index.name = "key"
483
+ return df[self.ATTRIBUTEKEYS]
484
+
485
+ def to_dataframe(self):
486
+ """create dataframe"""
487
+ return self._to_dataframe(self.attributes)
488
+
489
+ @property
490
+ def df(self):
491
+ """create dataframe"""
492
+ return self._to_dataframe(self.attributes)
493
+
494
+ @property
495
+ def udf(self):
496
+ """create dataframe for user attributes"""
497
+ return self._to_dataframe(self.user_attributes)
498
+
499
+ @property
500
+ def dft(self):
501
+ """create transposed dataframe for sdata attributes"""
502
+ mt = self.df[["value"]].transpose().copy()
503
+ mt.index = [self.get("_sdata_sname").value]
504
+ return mt
505
+
506
+ def get_dft(self, index_name=None):
507
+ """create transposed dataframe for sdata attributes"""
508
+ if index_name is None:
509
+ index_name = "_sdata_sname"
510
+ mt = self.df[["value"]].transpose().copy()
511
+ mt.index = [self.get(index_name).value]
512
+ return mt
513
+
514
+
515
+ @property
516
+ def sdf(self):
517
+ """create dataframe for sdata attributes"""
518
+ return self._to_dataframe(self.sdata_attributes)
519
+
520
+ @property
521
+ def sdft(self):
522
+ """create transposed dataframe for sdata attributes"""
523
+ mt = self.sdf[["value"]].transpose().copy()
524
+ mt.index = [self.get("_sdata_uuid").value]
525
+ return mt
526
+
527
+ @classmethod
528
+ def from_dataframe(cls, df):
529
+ """create metadata from dataframe"""
530
+ df["unit"] = df.unit.fillna("")
531
+ df["label"] =df.label.fillna("")
532
+ df["description"] =df.description.fillna("")
533
+ df["symbol"] = df["symbol"].fillna("") if "symbol" in df.columns else ""
534
+ d = df.to_dict(orient='index')
535
+ metadata = cls.from_dict(d)
536
+ return metadata
537
+
538
+ def update_from_usermetadata(self, metadata):
539
+ """update user metadata from metadata"""
540
+ for attribute in metadata.user_attributes.values():
541
+ self.add(attribute)
542
+
543
+ def to_csv(self, filepath=None, sep=",", header=False):
544
+ """serialize to csv"""
545
+ try:
546
+ df = self.to_dataframe()
547
+ # df.to_csv(filepath, index=None, sep=sep)
548
+ return df.to_csv(filepath, index=None, sep=sep, header=header)
549
+ except OSError as exp:
550
+ logger.error("metadata.to_csv error: %s" % (exp))
551
+
552
+ def to_csv_header(self, prefix="#", sep=",", filepath=None):
553
+ """serialize to csv"""
554
+ try:
555
+ lines = []
556
+ for attr in self.attributes.values():
557
+ lines.append(attr.to_csv(prefix=prefix, sep=sep)+"\n")
558
+
559
+ alines = "".join(lines)
560
+ if filepath:
561
+ logger.info("export '{}'".format(filepath))
562
+ with open(filepath, "w") as fh:
563
+ fh.write(alines)
564
+ return alines
565
+ except OSError as exp:
566
+ logger.error("metadata.to_csv error: %s" % (exp))
567
+
568
+ @classmethod
569
+ def from_csv(cls, filepath):
570
+ """create metadata from dataframe"""
571
+ df = pd.read_csv(filepath, header=None)
572
+ # Dateien vor RFC 0016 haben 8 Spalten (ohne symbol)
573
+ df.columns = cls.ATTRIBUTEKEYS[:len(df.columns)]
574
+ # Index nach Spalte "name" (Spalte behalten). Kein df.name.values, da das
575
+ # in neueren pandas-Versionen ein StringArray ist, den set_index als
576
+ # unhashbaren Key behandelt (TypeError).
577
+ df.set_index("name", drop=False, inplace=True)
578
+ metadata = cls.from_dataframe(df)
579
+ return metadata
580
+
581
+ def to_json(self, filepath=None):
582
+ """create a json
583
+
584
+ :param filepath: default None
585
+ :return: json str
586
+ """
587
+ d = self.to_dict()
588
+ if filepath:
589
+ with open(filepath, "w") as fh:
590
+ json.dump(d, fh, default=dtypes.json_default)
591
+ return json.dumps(d, default=dtypes.json_default)
592
+
593
+ @classmethod
594
+ def from_json(cls, jsonstr=None, filepath=None):
595
+ """create metadata from json file
596
+
597
+ :param jsonstr: json str
598
+ :param filepath: filepath to json file
599
+ :return: Metadata
600
+ :raises ValueError: if neither jsonstr nor an existing filepath is given
601
+ """
602
+
603
+ if filepath is not None and os.path.exists(filepath):
604
+ with open(filepath, "r") as fh:
605
+ j = json.load(fh)
606
+ metadata = cls.from_dict(j)
607
+ elif jsonstr is not None:
608
+ j = json.loads(jsonstr)
609
+ metadata = cls.from_dict(j)
610
+ else:
611
+ if filepath is not None:
612
+ raise ValueError(
613
+ f"Metadata.from_json: file not found and no jsonstr given: {filepath!r}")
614
+ raise ValueError("Metadata.from_json requires jsonstr or filepath")
615
+ return metadata
616
+
617
+ def to_list(self):
618
+ """create a nested list of Attribute values
619
+
620
+ :return: list
621
+ """
622
+ return self.df.values.tolist()
623
+
624
+ @classmethod
625
+ def from_list(cls, mlist):
626
+ """create metadata from a list of Attribute values
627
+
628
+ [['force_x', 1.2, 'kN', 'float', 'force in x-direction'],
629
+ ['force_y', 3.1, 'N', 'float', 'force in y-direction', 'label', True]]
630
+ """
631
+ metadata = cls()
632
+ for alist in mlist:
633
+ if len(alist) < 2:
634
+ logger.error("Metadata.from_list skip {}".format(alist))
635
+ else:
636
+ alist.extend(["", "", "", "", ""])
637
+ #["name", "value", "unit", "dtype", "description", "label", "required"]
638
+ metadata.add(alist[0], alist[1], unit=alist[2], dtype=alist[3], description=alist[4],
639
+ label=alist[5], required=alist[6])
640
+ return metadata
641
+
642
+ # --- JSON-LD / Linked Data / Sidecar (siehe sdata.semantic) -------------
643
+ def to_jsonld(self, context_mode="inline"):
644
+ """Serialisiere als selbstbeschreibendes JSON-LD-Dokument (dict)."""
645
+ from sdata import semantic
646
+ return semantic.to_jsonld(self, context_mode=context_mode)
647
+
648
+ @classmethod
649
+ def from_jsonld(cls, doc):
650
+ """Rekonstruiere Metadata aus einem JSON-LD-Dokument (dict oder str)."""
651
+ from sdata import semantic
652
+ return semantic.from_jsonld(doc)
653
+
654
+ def get_prefixed(self, prefix):
655
+ """Neue Metadata nur mit Attributen, deren Schlüssel mit ``prefix`` beginnt."""
656
+ sub = Metadata()
657
+ for key, attr in self._attributes.items():
658
+ if key.startswith(prefix):
659
+ sub._attributes[key] = attr
660
+ return sub
661
+
662
+ @property
663
+ def a(self):
664
+ """Attribut-Autocomplete-Accessor für die interaktive Nutzung (``m.a.force_x``)."""
665
+ from sdata import interactive
666
+ return interactive.AttrAccessor(self)
667
+
668
+ def _repr_html_(self):
669
+ from sdata import interactive
670
+ return interactive.metadata_html(self)
671
+
672
+ def validate(self, schema):
673
+ """Validiere diese Metadaten gegen ein :class:`sdata.schema.MetadataSchema`."""
674
+ return schema.validate(self)
675
+
676
+ def apply_schema(self, schema):
677
+ """Vervollständige diese Metadaten in-place gemäß einem MetadataSchema."""
678
+ return schema.apply(self)
679
+
680
+ def to_rdf(self, fmt="turtle"):
681
+ """Serialisiere als RDF (rdflib falls vorhanden, sonst JSON-LD)."""
682
+ from sdata import semantic
683
+ return semantic.to_rdf(self, fmt=fmt)
684
+
685
+ def to_verifiable_credential(self, issuer_did, issuer_priv_jwk, kid=None,
686
+ extra_claims=None):
687
+ """Signiere die Metadaten als W3C Verifiable Credential (Compact-JWS)."""
688
+ from sdata import semantic
689
+ return semantic.to_verifiable_credential(
690
+ self, issuer_did, issuer_priv_jwk, kid=kid, extra_claims=extra_claims)
691
+
692
+ @classmethod
693
+ def from_verifiable_credential(cls, vc_jws, pub_jwk):
694
+ """Verifiziere ein VC und rekonstruiere die Metadata aus dem credentialSubject."""
695
+ from sdata import semantic
696
+ return cls.from_jsonld(semantic.verify_credential(vc_jws, pub_jwk))
697
+
698
+ def to_turtle(self):
699
+ """Convenience: RDF im Turtle-Format."""
700
+ from sdata import semantic
701
+ return semantic.to_turtle(self)
702
+
703
+ def write_sidecar(self, path=None, indent=2):
704
+ """Schreibe ``<sname>.meta.jsonld`` neben einen Datenblob; gibt den Pfad zurück."""
705
+ from sdata import semantic
706
+ return semantic.write_sidecar(self, path=path, indent=indent)
707
+
708
+ @classmethod
709
+ def read_sidecar(cls, path):
710
+ """Lade eine ``.meta.jsonld``-Sidecar-Datei zu Metadata."""
711
+ from sdata import semantic
712
+ return semantic.read_sidecar(path)
713
+
714
+ def __repr__(self):
715
+ return "(Metadata'%s':%d)" % (self.name, len(self.attributes))
716
+
717
+ def __str__(self):
718
+ return "(Metadata'%s':%d %s)" % (self.name, len(self.attributes), [x for x in self.attributes])
719
+
720
+ def add(self, name, value=None, **kwargs):
721
+ """add Attribute
722
+
723
+ :param name:
724
+ :param value:
725
+ :param kwargs:
726
+ :return:
727
+ """
728
+ self.set_attr(name, value, **kwargs)
729
+
730
+ def relabel(self, name, newname):
731
+ """relabel Attribute
732
+
733
+ :param name: old attribute name
734
+ :param newname: new attribute name
735
+ :return: None
736
+ """
737
+ attr = self._attributes.get(name)
738
+ if attr is None:
739
+ logger.warning("{0}: no Attribute {1} to relabel.".format(self.__class__, name))
740
+ return
741
+ # explizit umschlüsseln (kein Präfix-Raten -> robust auch bei prefixed keys)
742
+ self._attributes.pop(name)
743
+ attr.name = newname
744
+ self._attributes[newname] = attr
745
+
746
+ def get(self, name: str, default: Any = None) -> Any:
747
+ #default = default or Attribute(name=name, value=None)
748
+ if self._attributes.get(name) is not None:
749
+ return self._attributes.get(name)
750
+ else:
751
+ return default
752
+
753
+ def keys(self) -> List[str]:
754
+ """
755
+
756
+ :return: list of Attribute names
757
+ """
758
+ return list(self._attributes.keys())
759
+
760
+ def values(self):
761
+ """
762
+
763
+ :return: list of Attribute values
764
+ """
765
+ return list(self._attributes.values())
766
+
767
+ def items(self):
768
+ """
769
+
770
+ :return: list of Attribute items (keys, values)
771
+ """
772
+ return list(self._attributes.items())
773
+
774
+
775
+ def copy(self):
776
+ """returns a deep copy"""
777
+ return copy.deepcopy(self)
778
+
779
+ @property
780
+ def size(self):
781
+ """return number uf Attribute"""
782
+ return len(self.attributes)
783
+
784
+ def __getitem__(self, name):
785
+ return self.get(name)
786
+
787
+ def __setitem__(self, name, value):
788
+ """update value in Attribute or create new Attribute"""
789
+ attr = self.get(name)
790
+
791
+ if attr is None:
792
+ attr = Attribute(name=name, value=value, dtype=type(value).__name__)
793
+ self._attributes[attr.name] = attr
794
+ else:
795
+ dtype = attr.guess_dtype(value)
796
+ attr.dtype = dtype
797
+ attr.value = dtype(value)
798
+
799
+ def __contains__(self, item):
800
+ return item in self.attributes.keys()
801
+
802
+ def pop(self, item):
803
+ try:
804
+ return self._attributes.pop(item)
805
+ except KeyError as exp:
806
+ return None
807
+
808
+ def __iter__(self):
809
+ for x in self.attributes.items():
810
+ yield x
811
+
812
+ @property
813
+ def sha3_256(self):
814
+ """Return a new SHA3 hash object with a hashbit length of 32 bytes.
815
+
816
+ :return: hashlib.sha3_256.hexdigest()
817
+ """
818
+ s = hashlib.sha3_256()
819
+ metadatastr = self.to_json().encode(errors="replace")
820
+ s.update(metadatastr)
821
+ return s.hexdigest()
822
+
823
+ def update_hash(self, hashobject):
824
+ """A hash represents the object used to calculate a checksum of a
825
+ string of information.
826
+
827
+ .. code-block:: python
828
+
829
+ hashobject = hashlib.sha3_256()
830
+ metadata = Metadata()
831
+ metadata.update_hash(hashobject)
832
+ hash.hexdigest()
833
+
834
+ :param hashobject: hash object
835
+ :return: hash_function().hexdigest()
836
+ """
837
+ if not (hasattr(hashobject, "update") and hasattr(hashobject, "hexdigest")):
838
+ logger.error("Metadata.hash: given hashfunction is invalid")
839
+ raise Exception("Metadata.hash: given hashfunction is invalid")
840
+
841
+ metadatastr = self.to_json().encode(errors="replace")
842
+ hashobject.update(metadatastr)
843
+ return hashobject
844
+
845
+ def set_unit_from_name(self, add_description=True, fix_name=True):
846
+ """try to extract unit from attribute name
847
+
848
+ :return:
849
+ """
850
+ for attr in self.user_attributes.values():
851
+ new_name, unit = extract_name_unit(attr.name)
852
+ attr.unit = unit
853
+ if add_description is True and len(attr.description)==0 and len(unit)>0:
854
+ attr.description = attr.name
855
+ if fix_name is True:
856
+ self.relabel(attr.name, new_name)
857
+
858
+ def guess_value_dtype(self):
859
+ """try to cast the Attribute values, e.g. str -> float
860
+
861
+ :return:
862
+ """
863
+ for attr in list(self.user_attributes.values()):
864
+ for dtype in [float, str]:
865
+ try:
866
+ # attr.value = dtype(attr.value)
867
+ # attr.dtype = dtype.__name__
868
+ self.add(name=attr.name, value=dtype(attr.value), dtype=dtype.__name__)
869
+ # print(["ok", attr.name, attr.value, self.get(attr.name).value])
870
+ break
871
+ except (ValueError, TypeError) as exp:
872
+ pass
873
+ # print([attr.name, attr.value, dtype, exp])
874
+
875
+ def is_complete(self):
876
+ """check all required attributes"""
877
+ required_attributes = self.required_attributes.values()
878
+ for attr in required_attributes:
879
+ if attr.value is None or attr.value=="":
880
+ return False
881
+
882
+ return True