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/base.py
ADDED
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import io
|
|
3
|
+
import uuid
|
|
4
|
+
import logging
|
|
5
|
+
import unicodedata
|
|
6
|
+
import json
|
|
7
|
+
from typing import List, Dict, Any, Optional, Type, Literal, Union, Tuple, BinaryIO
|
|
8
|
+
|
|
9
|
+
import pandas
|
|
10
|
+
from sdata import __version__
|
|
11
|
+
from sdata.format import CURRENT_FORMAT_VERSION, ensure_compatible
|
|
12
|
+
from sdata.sclass import register
|
|
13
|
+
from sdata.suuid import SUUID
|
|
14
|
+
from sdata.metadata import Metadata, Attribute, extract_name_unit
|
|
15
|
+
from sdata.timestamp import now_utc_str, now_local_str, today_str
|
|
16
|
+
import sdata.sclass
|
|
17
|
+
import gzip
|
|
18
|
+
import zipfile
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SdataUuidException(Exception):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Base:
|
|
29
|
+
"""
|
|
30
|
+
Base class for sdata objects with metadata management.
|
|
31
|
+
Provides core functionality for handling metadata, unique identifiers,
|
|
32
|
+
serialization, and hierarchical relationships.
|
|
33
|
+
"""
|
|
34
|
+
SDATA_CLS = "sdata.base.Base"
|
|
35
|
+
|
|
36
|
+
SDATA_VERSION = "_sdata_version"
|
|
37
|
+
SDATA_FORMAT_VERSION = "_sdata_format_version"
|
|
38
|
+
SDATA_CLASS = "_sdata_class"
|
|
39
|
+
SDATA_NAME = "_sdata_name"
|
|
40
|
+
SDATA_SNAME = "_sdata_sname"
|
|
41
|
+
SDATA_SUUID = "_sdata_suuid"
|
|
42
|
+
SDATA_PARENT_SNAME = "_sdata_parent_sname" # Changed to store sname for consistency
|
|
43
|
+
SDATA_PROJECT_SNAME = "_sdata_project_sname" # Changed to store sname for consistency
|
|
44
|
+
SDATA_TOPOLOGY_CLASS = "_sdata_topology_class" # Topology class, e.g. BFO (Basic Formal Ontology) top-level ontology class name
|
|
45
|
+
SDATA_CTIME = "_sdata_ctime"
|
|
46
|
+
|
|
47
|
+
SDATA_ATTRIBUTES: List[str] = [
|
|
48
|
+
SDATA_VERSION, SDATA_FORMAT_VERSION, SDATA_NAME, SDATA_CLASS, SDATA_CTIME,
|
|
49
|
+
SDATA_PARENT_SNAME, SDATA_PROJECT_SNAME, SDATA_TOPOLOGY_CLASS
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
#: optionales MetadataSchema/Template (Default None); Subklassen/Factory können
|
|
53
|
+
#: es setzen, dann werden die Metadaten beim Init vervollständigt (vollqualifiziert).
|
|
54
|
+
SDATA_SCHEMA = None
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def get_sdata_spec(cls: type) -> str:
|
|
58
|
+
"""Kanonischer, importierbarer String für eine Klasse: ``modul:Klasse``."""
|
|
59
|
+
return f"{cls.__module__}:{cls.__qualname__}"
|
|
60
|
+
|
|
61
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Initialize Base with metadata.
|
|
64
|
+
|
|
65
|
+
:param name: Required name of the object (str).
|
|
66
|
+
:param parent: Optional parent object (Base) or suuid_str (str).
|
|
67
|
+
:param project: Optional project object (Base) or suuid_str (str).
|
|
68
|
+
:param ns_name: Optional namespace for reproducible SUUID (str).
|
|
69
|
+
:param default_attributes: List of dicts for additional metadata attributes.
|
|
70
|
+
:raises ValueError: If name is empty or None.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
name = kwargs.get("name", "noname")
|
|
74
|
+
if not name:
|
|
75
|
+
raise ValueError("Name cannot be empty")
|
|
76
|
+
self.metadata = Metadata(name=name)
|
|
77
|
+
self.metadata.add(self.SDATA_CTIME, now_utc_str(), dtype="str", description="UTC creation date", required=True)
|
|
78
|
+
|
|
79
|
+
project_sname = self._resolve_relation_sname(
|
|
80
|
+
kwargs.get("project", None), kwargs.get("project_sname", None), "project")
|
|
81
|
+
self.metadata.add(
|
|
82
|
+
self.SDATA_PROJECT_SNAME, project_sname, dtype="str",
|
|
83
|
+
description="sname of the project"
|
|
84
|
+
)
|
|
85
|
+
# Namespace: expliziter ns_name-Kwarg hat Vorrang, sonst die Projekt-sname
|
|
86
|
+
# (hierarchisches Scoping), sonst der Default-Namespace.
|
|
87
|
+
ns_name = kwargs.get("ns_name") or project_sname or None
|
|
88
|
+
|
|
89
|
+
_suuid = kwargs.get("suuid", None)
|
|
90
|
+
if _suuid and isinstance(_suuid, SUUID):
|
|
91
|
+
suuid = _suuid
|
|
92
|
+
else:
|
|
93
|
+
# Deterministisch und (falls vorhanden) ns-gescopt; suuid.from_name
|
|
94
|
+
# normalisiert den Namen S3-sicher.
|
|
95
|
+
suuid = SUUID.from_name(self.__class__.__name__, name, ns_name=ns_name)
|
|
96
|
+
|
|
97
|
+
self.default_attributes: List[Dict[str, Any]] = []
|
|
98
|
+
|
|
99
|
+
self.metadata.add(
|
|
100
|
+
self.SDATA_VERSION, __version__, dtype="str",
|
|
101
|
+
description="sdata package version", required=True
|
|
102
|
+
)
|
|
103
|
+
self.metadata.add(
|
|
104
|
+
self.SDATA_FORMAT_VERSION, CURRENT_FORMAT_VERSION, dtype="int",
|
|
105
|
+
description="sdata serialization format version", required=True
|
|
106
|
+
)
|
|
107
|
+
self.metadata.add(
|
|
108
|
+
self.SDATA_TOPOLOGY_CLASS, "sdata.sclass:IndependentContinuant", dtype="str",
|
|
109
|
+
description="sdata topology class name", required=False
|
|
110
|
+
)
|
|
111
|
+
self.metadata.add(
|
|
112
|
+
self.SDATA_NAME, name, dtype="str",
|
|
113
|
+
description="name of the data object", required=True
|
|
114
|
+
)
|
|
115
|
+
self.metadata.add(
|
|
116
|
+
self.SDATA_SNAME, suuid.sname, dtype="str",
|
|
117
|
+
description="sname of the data object", required=True
|
|
118
|
+
)
|
|
119
|
+
self.metadata.add(
|
|
120
|
+
self.SDATA_SUUID, suuid.suuid_str, dtype="str",
|
|
121
|
+
description="Super Universally Unique Identifier", required=True
|
|
122
|
+
)
|
|
123
|
+
self.metadata.add(
|
|
124
|
+
self.SDATA_CLASS, self.get_sdata_spec(), dtype="str",
|
|
125
|
+
description="sdata class", required=True
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
parent_sname = self._resolve_relation_sname(
|
|
129
|
+
kwargs.get("parent", None), kwargs.get("parent_sname", None), "parent")
|
|
130
|
+
self.metadata.add(
|
|
131
|
+
self.SDATA_PARENT_SNAME, parent_sname, dtype="str",
|
|
132
|
+
description="sname of the parent"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if "default_attributes" in kwargs:
|
|
136
|
+
self.default_attributes.extend(kwargs.get("default_attributes"))
|
|
137
|
+
self.set_default_attributes()
|
|
138
|
+
|
|
139
|
+
self._description = kwargs.get("description", "")
|
|
140
|
+
self._data: Dict[str, Any] = kwargs.get("data", {})
|
|
141
|
+
|
|
142
|
+
# Optionales Schema/Template anwenden (vollqualifizieren), falls deklariert
|
|
143
|
+
if self.SDATA_SCHEMA is not None:
|
|
144
|
+
self.SDATA_SCHEMA.apply(self.metadata)
|
|
145
|
+
|
|
146
|
+
# logger.debug(f"Created {self.__class__.__name__} '{suuid.sname}'")
|
|
147
|
+
|
|
148
|
+
def validate(self):
|
|
149
|
+
"""Validiere die Metadaten gegen das deklarierte ``SDATA_SCHEMA``.
|
|
150
|
+
|
|
151
|
+
Ohne Schema ist das Ergebnis trivial gültig.
|
|
152
|
+
"""
|
|
153
|
+
from sdata.schema import ValidationReport
|
|
154
|
+
if self.SDATA_SCHEMA is None:
|
|
155
|
+
return ValidationReport(ok=True)
|
|
156
|
+
return self.SDATA_SCHEMA.validate(self.metadata)
|
|
157
|
+
|
|
158
|
+
def _resolve_relation_sname(self, obj: Any, sname: Optional[str], label: str) -> str:
|
|
159
|
+
"""Ermittle den sname-Wert für eine Relation (``project``/``parent``).
|
|
160
|
+
|
|
161
|
+
Ein direkt übergebener sname-String (``project_sname``/``parent_sname``) hat
|
|
162
|
+
Vorrang. Andernfalls muss ``obj`` ein :class:`Base` sein; ein Fremdtyp
|
|
163
|
+
erzeugt eine Warnung und einen leeren Wert.
|
|
164
|
+
"""
|
|
165
|
+
if sname is not None:
|
|
166
|
+
return sname
|
|
167
|
+
if obj is None:
|
|
168
|
+
return ""
|
|
169
|
+
if isinstance(obj, Base):
|
|
170
|
+
return obj.sname
|
|
171
|
+
logger.warning(f"{label} must be of type Base")
|
|
172
|
+
return ""
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def md(self) -> Metadata:
|
|
176
|
+
"""Shortcut to access the metadata object."""
|
|
177
|
+
return self.metadata
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def mdf(self) -> pandas.DataFrame:
|
|
181
|
+
"""Shortcut to access the metadata as a pandas DataFrame."""
|
|
182
|
+
return self.metadata.df
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def osname(self) -> str:
|
|
186
|
+
"""Returns an OS-compatible ASCII name."""
|
|
187
|
+
return SUUID.generate_safe_filename(self.name)
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def class_name(self) -> str:
|
|
191
|
+
"""Returns the class name of the object."""
|
|
192
|
+
return self.__class__.__name__
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def uuid(self) -> uuid.UUID:
|
|
196
|
+
"""Returns the UUID component of the SUUID."""
|
|
197
|
+
return self.suuid.uuid
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def huuid(self) -> str:
|
|
201
|
+
"""Returns the hex representation of the UUID."""
|
|
202
|
+
return self.suuid.uuid.hex
|
|
203
|
+
|
|
204
|
+
def _get_suuid(self) -> str:
|
|
205
|
+
return self.metadata.get(self.SDATA_SUUID).value
|
|
206
|
+
|
|
207
|
+
def _set_suuid(self, value: str) -> None:
|
|
208
|
+
if not SUUID.is_valid_suuid_str(value):
|
|
209
|
+
raise SdataUuidException("Invalid SUUID string")
|
|
210
|
+
self.metadata.set_attr(self.SDATA_SUUID, value)
|
|
211
|
+
|
|
212
|
+
suuid_str = property(
|
|
213
|
+
fget=_get_suuid, fset=_set_suuid,
|
|
214
|
+
doc="The SUUID of the object as a string."
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def suuid(self) -> SUUID:
|
|
219
|
+
"""Returns the SUUID object."""
|
|
220
|
+
return SUUID.from_suuid_sname(self.metadata.get(self.SDATA_SNAME).value)
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def suuid_bytes(self) -> bytes:
|
|
224
|
+
"""Returns the SUUID string encoded as UTF-8 bytes."""
|
|
225
|
+
return self.suuid_str.encode("utf-8")
|
|
226
|
+
|
|
227
|
+
def _get_sname(self) -> str:
|
|
228
|
+
return self.metadata.get(self.SDATA_SNAME).value
|
|
229
|
+
|
|
230
|
+
def _set_sname(self, value: str) -> None:
|
|
231
|
+
self.metadata.set_attr(self.SDATA_SNAME, value)
|
|
232
|
+
|
|
233
|
+
sname = property(
|
|
234
|
+
fget=_get_sname, fset=_set_sname,
|
|
235
|
+
doc="The sname of the object."
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def get_sdata_did_method(self) -> str:
|
|
239
|
+
"""Returns the sdata class id."""
|
|
240
|
+
module_class_name = self.md.get("_sdata_class").value or "sdata.base:Base"
|
|
241
|
+
if ":" in module_class_name:
|
|
242
|
+
module, class_name = module_class_name.rsplit(":", 1)
|
|
243
|
+
else:
|
|
244
|
+
module = "sdata.base"
|
|
245
|
+
class_name = module_class_name
|
|
246
|
+
module = module.replace(":", "-").replace(".", "-").lower()
|
|
247
|
+
logger.debug("%s %s %s", module_class_name, module, class_name)
|
|
248
|
+
return module
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def did(self):
|
|
252
|
+
"""DID"""
|
|
253
|
+
module = "sdata" # self.get_sdata_did_method()
|
|
254
|
+
return f"did:suuid:{self.sname}:{module}"
|
|
255
|
+
|
|
256
|
+
def to_jsonld(self, context_mode="inline"):
|
|
257
|
+
"""Serialisiere die Metadaten als JSON-LD (siehe :mod:`sdata.semantic`)."""
|
|
258
|
+
return self.metadata.to_jsonld(context_mode=context_mode)
|
|
259
|
+
|
|
260
|
+
def to_turtle(self):
|
|
261
|
+
"""Serialisiere die Metadaten als RDF/Turtle (siehe :mod:`sdata.semantic`)."""
|
|
262
|
+
return self.metadata.to_turtle()
|
|
263
|
+
|
|
264
|
+
def _repr_html_(self):
|
|
265
|
+
"""Jupyter-Darstellung der Metadaten (siehe :mod:`sdata.interactive`)."""
|
|
266
|
+
return self.metadata._repr_html_()
|
|
267
|
+
|
|
268
|
+
def write_sidecar(self, path=None, indent=2):
|
|
269
|
+
"""Schreibe ``<sname>.meta.jsonld`` neben einen Datenblob; gibt den Pfad zurück."""
|
|
270
|
+
return self.metadata.write_sidecar(path=path, indent=indent)
|
|
271
|
+
|
|
272
|
+
@classmethod
|
|
273
|
+
def read_sidecar(cls, path):
|
|
274
|
+
"""Lade eine ``.meta.jsonld``-Sidecar-Datei zu einer Metadata."""
|
|
275
|
+
from sdata import semantic
|
|
276
|
+
return semantic.read_sidecar(path)
|
|
277
|
+
|
|
278
|
+
def _get_name(self) -> str:
|
|
279
|
+
return self.metadata.get(self.SDATA_NAME).value
|
|
280
|
+
|
|
281
|
+
def _set_name(self, value: str) -> None:
|
|
282
|
+
if not value:
|
|
283
|
+
raise ValueError("Name cannot be empty")
|
|
284
|
+
self.metadata.set_attr(self.SDATA_NAME, value)
|
|
285
|
+
|
|
286
|
+
name = property(
|
|
287
|
+
fget=_get_name, fset=_set_name,
|
|
288
|
+
doc="The name of the object."
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
def _get_data(self) -> Dict[str, Any]:
|
|
292
|
+
return self._data
|
|
293
|
+
|
|
294
|
+
def _set_data(self, value: Dict[str, Any]) -> None:
|
|
295
|
+
if not isinstance(value, dict):
|
|
296
|
+
raise ValueError("data must be a dict")
|
|
297
|
+
self._data = value
|
|
298
|
+
|
|
299
|
+
data = property(
|
|
300
|
+
fget=_get_data, fset=_set_data,
|
|
301
|
+
doc="The data dictionary of the object."
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
def _get_description(self) -> str:
|
|
305
|
+
return self._description
|
|
306
|
+
|
|
307
|
+
def _set_description(self, value: str) -> None:
|
|
308
|
+
if not isinstance(value, str):
|
|
309
|
+
raise ValueError("description must be a string")
|
|
310
|
+
if len(value) > 1000:
|
|
311
|
+
self._description = value[:1000] + "..."
|
|
312
|
+
else:
|
|
313
|
+
self._description = value
|
|
314
|
+
|
|
315
|
+
description = property(
|
|
316
|
+
fget=_get_description, fset=_set_description,
|
|
317
|
+
doc="The description string of the object (max 1000 characters)."
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
def update_data(self, data_dict: Dict[str, Any]) -> None:
|
|
321
|
+
"""
|
|
322
|
+
Update the data dictionary with recursive merge for nested dicts.
|
|
323
|
+
|
|
324
|
+
:param data_dict: Dictionary to merge into self._data.
|
|
325
|
+
:raises ValueError: If data_dict is not a dict.
|
|
326
|
+
"""
|
|
327
|
+
if not isinstance(data_dict, dict):
|
|
328
|
+
raise ValueError("data must be a dict")
|
|
329
|
+
self._data = self._deep_update(self._data, data_dict)
|
|
330
|
+
|
|
331
|
+
def _deep_update(self, d: Dict[str, Any], u: Dict[str, Any]) -> Dict[str, Any]:
|
|
332
|
+
for k, v in u.items():
|
|
333
|
+
if isinstance(v, dict):
|
|
334
|
+
d[k] = self._deep_update(d.get(k, {}), v)
|
|
335
|
+
else:
|
|
336
|
+
d[k] = v
|
|
337
|
+
return d
|
|
338
|
+
|
|
339
|
+
@property
|
|
340
|
+
def parent(self) -> SUUID:
|
|
341
|
+
"""Returns the parent as a SUUID object."""
|
|
342
|
+
return self.get_parent()
|
|
343
|
+
|
|
344
|
+
def get_parent(self) -> SUUID:
|
|
345
|
+
"""Returns the parent SUUID."""
|
|
346
|
+
sname = self.metadata.get(self.SDATA_PARENT_SNAME).value
|
|
347
|
+
if sname:
|
|
348
|
+
return SUUID.from_suuid_sname(sname)
|
|
349
|
+
else:
|
|
350
|
+
return None
|
|
351
|
+
|
|
352
|
+
@property
|
|
353
|
+
def project(self) -> SUUID:
|
|
354
|
+
"""Returns the project as a SUUID object."""
|
|
355
|
+
return self.get_project()
|
|
356
|
+
|
|
357
|
+
def get_project(self) -> SUUID:
|
|
358
|
+
"""Returns the project SUUID."""
|
|
359
|
+
sname = self.metadata.get(self.SDATA_PROJECT_SNAME).value
|
|
360
|
+
if sname:
|
|
361
|
+
return SUUID.from_suuid_sname(sname)
|
|
362
|
+
else:
|
|
363
|
+
return None
|
|
364
|
+
|
|
365
|
+
def __str__(self) -> str:
|
|
366
|
+
return f"<{self.sname}>"
|
|
367
|
+
|
|
368
|
+
__repr__ = __str__
|
|
369
|
+
|
|
370
|
+
@property
|
|
371
|
+
def udf(self) -> pandas.DataFrame:
|
|
372
|
+
"""Returns the user-defined metadata as a DataFrame."""
|
|
373
|
+
return self.metadata.udf
|
|
374
|
+
|
|
375
|
+
@property
|
|
376
|
+
def sdf(self) -> pandas.DataFrame:
|
|
377
|
+
"""Returns the system-defined metadata as a DataFrame."""
|
|
378
|
+
return self.metadata.sdf
|
|
379
|
+
|
|
380
|
+
def set_default_attributes(self) -> None:
|
|
381
|
+
"""
|
|
382
|
+
Set default attributes from self.default_attributes list.
|
|
383
|
+
"""
|
|
384
|
+
for attr_dict in self.default_attributes:
|
|
385
|
+
self.metadata.add(**attr_dict)
|
|
386
|
+
|
|
387
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
388
|
+
"""
|
|
389
|
+
Convert the object to a dictionary representation.
|
|
390
|
+
|
|
391
|
+
:return: Dictionary with metadata, data, and description.
|
|
392
|
+
"""
|
|
393
|
+
return {
|
|
394
|
+
"metadata": self.metadata.to_dict(),
|
|
395
|
+
"data": self._data,
|
|
396
|
+
"description": self._description,
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
@staticmethod
|
|
400
|
+
def classname_from_classspec(spec: str) -> str:
|
|
401
|
+
if ":" in spec:
|
|
402
|
+
return spec.split(":")[-1]
|
|
403
|
+
else:
|
|
404
|
+
return spec
|
|
405
|
+
|
|
406
|
+
@classmethod
|
|
407
|
+
def from_dict(cls, d: Dict[str, Any], *, strict: bool = False) -> 'Base':
|
|
408
|
+
"""
|
|
409
|
+
Create an instance from a dictionary.
|
|
410
|
+
|
|
411
|
+
:param d: Dictionary with metadata, data, and description.
|
|
412
|
+
:param strict: bei einer **neueren** Formatversion hart scheitern statt zu
|
|
413
|
+
warnen (RFC 0010).
|
|
414
|
+
:return: Instance of Base or subclass.
|
|
415
|
+
"""
|
|
416
|
+
d = ensure_compatible(d, strict=strict) # RFC 0010: Version prüfen/migrieren
|
|
417
|
+
metadata = Metadata.from_dict(d.get("metadata", {}))
|
|
418
|
+
class_spec = metadata.get(cls.SDATA_CLASS).value or "sdata.base:Base"
|
|
419
|
+
class_name = cls.classname_from_classspec(class_spec)
|
|
420
|
+
b = sdata_factory(class_name)
|
|
421
|
+
b.metadata = metadata
|
|
422
|
+
b.data = d.get("data", {})
|
|
423
|
+
b.description = d.get("description", "")
|
|
424
|
+
return b
|
|
425
|
+
|
|
426
|
+
def to_json(self, filepath: Optional[str] = None, sidecar: bool = False) -> Optional[str]:
|
|
427
|
+
"""
|
|
428
|
+
Export the object to JSON format, either as a string or to a file.
|
|
429
|
+
|
|
430
|
+
:param filepath: Optional file path to write JSON (default: None).
|
|
431
|
+
:param sidecar: bei True zusätzlich ``<dir>/<sname>.meta.jsonld`` schreiben
|
|
432
|
+
(nur wirksam mit ``filepath``; Default False = unverändertes Verhalten).
|
|
433
|
+
:return: JSON string if filepath is None, else None.
|
|
434
|
+
"""
|
|
435
|
+
data_dict = self.to_dict()
|
|
436
|
+
if filepath:
|
|
437
|
+
with open(filepath, "w") as fh:
|
|
438
|
+
json.dump(data_dict, fh, indent=4)
|
|
439
|
+
if sidecar:
|
|
440
|
+
self.write_sidecar(os.path.dirname(filepath) or ".")
|
|
441
|
+
return None
|
|
442
|
+
else:
|
|
443
|
+
return json.dumps(data_dict, indent=4)
|
|
444
|
+
|
|
445
|
+
@classmethod
|
|
446
|
+
def from_json(cls, s: str) -> 'Base':
|
|
447
|
+
"""
|
|
448
|
+
Create an instance from a JSON string or file path.
|
|
449
|
+
|
|
450
|
+
:param s: JSON string or path to JSON file.
|
|
451
|
+
:return: Instance of Base or subclass.
|
|
452
|
+
:raises json.JSONDecodeError: If invalid JSON.
|
|
453
|
+
:raises FileNotFoundError: If file not found.
|
|
454
|
+
"""
|
|
455
|
+
if os.path.isfile(s):
|
|
456
|
+
with open(s, "r") as fh:
|
|
457
|
+
d = json.load(fh)
|
|
458
|
+
else:
|
|
459
|
+
d = json.loads(s)
|
|
460
|
+
return cls.from_dict(d)
|
|
461
|
+
|
|
462
|
+
def to_bytes_gzip(self):
|
|
463
|
+
json_str = self.to_json()
|
|
464
|
+
compressed = gzip.compress(json_str.encode("utf-8"))
|
|
465
|
+
return compressed
|
|
466
|
+
|
|
467
|
+
@classmethod
|
|
468
|
+
def from_bytes_gzip(cls, compressed: bytes):
|
|
469
|
+
decompressed = gzip.decompress(compressed).decode("utf-8")
|
|
470
|
+
instance = cls.from_json(decompressed)
|
|
471
|
+
return instance
|
|
472
|
+
|
|
473
|
+
def to_zip(
|
|
474
|
+
self,
|
|
475
|
+
filepath: Optional[Union[str, Path]] = None,
|
|
476
|
+
*,
|
|
477
|
+
compresslevel: int = 6,
|
|
478
|
+
deterministic: bool = True,
|
|
479
|
+
) -> io.BytesIO:
|
|
480
|
+
"""
|
|
481
|
+
Serialisiert das Objekt via to_json() und packt es als data.json in ein ZIP.
|
|
482
|
+
- Wenn 'filepath' gesetzt ist, wird die ZIP-Datei geschrieben.
|
|
483
|
+
- Rückgabe ist immer ein BytesIO, beginnend bei Position 0.
|
|
484
|
+
"""
|
|
485
|
+
json_str = self.to_json()
|
|
486
|
+
|
|
487
|
+
buf = io.BytesIO()
|
|
488
|
+
compression = zipfile.ZIP_DEFLATED
|
|
489
|
+
|
|
490
|
+
# Optional: deterministische ZIPs (feste Timestamp -> reproduzierbar)
|
|
491
|
+
arcname = f"{self.sname}.sjson"
|
|
492
|
+
|
|
493
|
+
if deterministic:
|
|
494
|
+
info = zipfile.ZipInfo(arcname, date_time=(1980, 1, 1, 0, 0, 0))
|
|
495
|
+
info.compress_type = compression
|
|
496
|
+
with zipfile.ZipFile(buf, mode="w", compression=compression, compresslevel=compresslevel) as zf:
|
|
497
|
+
zf.writestr(info, json_str)
|
|
498
|
+
else:
|
|
499
|
+
with zipfile.ZipFile(buf, mode="w", compression=compression, compresslevel=compresslevel) as zf:
|
|
500
|
+
zf.writestr(arcname, json_str)
|
|
501
|
+
|
|
502
|
+
buf.seek(0)
|
|
503
|
+
|
|
504
|
+
if filepath is not None:
|
|
505
|
+
p = Path(filepath)
|
|
506
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
507
|
+
p.write_bytes(buf.getvalue())
|
|
508
|
+
buf.seek(0)
|
|
509
|
+
|
|
510
|
+
return buf
|
|
511
|
+
|
|
512
|
+
@classmethod
|
|
513
|
+
def from_zip(
|
|
514
|
+
cls,
|
|
515
|
+
src: Union[str, Path, bytes, io.BytesIO, BinaryIO],
|
|
516
|
+
*,
|
|
517
|
+
member: Optional[str] = None,
|
|
518
|
+
encoding: str = "utf-8",
|
|
519
|
+
):
|
|
520
|
+
"""
|
|
521
|
+
Erzeugt eine Instanz aus einem ZIP, das eine JSON-Datei enthält.
|
|
522
|
+
'src' kann ein Dateipfad, Bytes, BytesIO oder beliebiger file-like Stream sein.
|
|
523
|
+
- 'member': Name des JSON-Eintrags im ZIP. Wenn None:
|
|
524
|
+
* nimm die einzige .json-Datei
|
|
525
|
+
* oder, wenn nur ein Eintrag existiert, nimm diesen
|
|
526
|
+
* sonst Fehler (Mehrdeutigkeit).
|
|
527
|
+
"""
|
|
528
|
+
def _open_zipfile(source):
|
|
529
|
+
# Gibt einen Kontextmanager zurück, der ein ZipFile liefert
|
|
530
|
+
if isinstance(source, (str, Path)):
|
|
531
|
+
return zipfile.ZipFile(source, mode="r")
|
|
532
|
+
if isinstance(source, bytes):
|
|
533
|
+
return zipfile.ZipFile(io.BytesIO(source), mode="r")
|
|
534
|
+
if isinstance(source, io.BytesIO):
|
|
535
|
+
source.seek(0)
|
|
536
|
+
return zipfile.ZipFile(source, mode="r")
|
|
537
|
+
# generischer file-like Stream
|
|
538
|
+
try:
|
|
539
|
+
source.seek(0)
|
|
540
|
+
except Exception: # pragma: no cover - defensiv; ZipFile braucht danach seek
|
|
541
|
+
pass
|
|
542
|
+
return zipfile.ZipFile(source, mode="r")
|
|
543
|
+
|
|
544
|
+
with _open_zipfile(src) as zf:
|
|
545
|
+
names = zf.namelist()
|
|
546
|
+
chosen = member
|
|
547
|
+
if chosen is None:
|
|
548
|
+
json_members = [n for n in names if n.lower().endswith(".sjson")]
|
|
549
|
+
if len(json_members) == 1:
|
|
550
|
+
chosen = json_members[0]
|
|
551
|
+
elif len(json_members) == 0 and len(names) == 1:
|
|
552
|
+
chosen = names[0]
|
|
553
|
+
else:
|
|
554
|
+
raise ValueError(
|
|
555
|
+
f"ZIP enthält {len(names)} Einträge ({names}). Bitte 'member' angeben."
|
|
556
|
+
)
|
|
557
|
+
data = zf.read(chosen).decode(encoding)
|
|
558
|
+
return cls.from_json(data)
|
|
559
|
+
|
|
560
|
+
def cls_from_spec(
|
|
561
|
+
# class_name: str,
|
|
562
|
+
sdata_spec: Optional[str] = "sdata.base:Base",
|
|
563
|
+
on_error: Literal["ignore", "strict"] = "strict",
|
|
564
|
+
sdata_attrs: Optional[Dict[str, Any]] = None,
|
|
565
|
+
**kwargs: Any
|
|
566
|
+
) -> Any:
|
|
567
|
+
"""
|
|
568
|
+
Factory function to create an instance of a dynamically generated subclass.
|
|
569
|
+
|
|
570
|
+
:param sdata_spec: The ``module:class`` spec to inherit from (default: ``sdata.base:Base``).
|
|
571
|
+
:param sdata_attrs: Optional dict of custom attributes/methods to add to the class.
|
|
572
|
+
:param kwargs: Keyword arguments to pass to the instance initialization.
|
|
573
|
+
:return: generated class.
|
|
574
|
+
"""
|
|
575
|
+
sdata_attrs = sdata_attrs or {}
|
|
576
|
+
|
|
577
|
+
if ":" in sdata_spec:
|
|
578
|
+
mod_name, class_name = sdata_spec.split(":", 1)
|
|
579
|
+
else:
|
|
580
|
+
mod_name, class_name = "", sdata_spec
|
|
581
|
+
try:
|
|
582
|
+
sdata_class = sdata.sclass.spec_to_class(sdata_spec)
|
|
583
|
+
except ModuleNotFoundError as e:
|
|
584
|
+
if on_error == "ignore":
|
|
585
|
+
sdata_class = Base
|
|
586
|
+
else:
|
|
587
|
+
raise ModuleNotFoundError(f"sdata.base.class_factory Error {e}")
|
|
588
|
+
|
|
589
|
+
cls = type(class_name, (sdata_class,), sdata_attrs)
|
|
590
|
+
|
|
591
|
+
def __init__(self, **init_kwargs: Any) -> None:
|
|
592
|
+
super(cls, self).__init__(**init_kwargs) # type: ignore
|
|
593
|
+
|
|
594
|
+
setattr(cls, '__init__', __init__)
|
|
595
|
+
return cls
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def sclass_factory(
|
|
599
|
+
# class_name: str,
|
|
600
|
+
sdata_spec: Optional[str] = "sdata.base:Base",
|
|
601
|
+
on_error: Literal["ignore", "strict"] = "strict",
|
|
602
|
+
sdata_attrs: Optional[Dict[str, Any]] = None,
|
|
603
|
+
**kwargs: Any
|
|
604
|
+
) -> Any:
|
|
605
|
+
"""
|
|
606
|
+
Factory function to create an instance of a dynamically generated subclass.
|
|
607
|
+
|
|
608
|
+
:param sdata_spec: The ``module:class`` spec to inherit from (default: ``sdata.base:Base``).
|
|
609
|
+
:param sdata_attrs: Optional dict of custom attributes/methods to add to the class.
|
|
610
|
+
:param kwargs: Keyword arguments to pass to the instance initialization.
|
|
611
|
+
:return: An instance of the generated class.
|
|
612
|
+
"""
|
|
613
|
+
sdata_attrs = sdata_attrs or {}
|
|
614
|
+
|
|
615
|
+
if ":" in sdata_spec:
|
|
616
|
+
mod_name, class_name = sdata_spec.split(":", 1)
|
|
617
|
+
else:
|
|
618
|
+
mod_name, class_name = "", sdata_spec
|
|
619
|
+
try:
|
|
620
|
+
sdata_class = sdata.sclass.spec_to_class(sdata_spec)
|
|
621
|
+
except ModuleNotFoundError as e:
|
|
622
|
+
if on_error == "ignore":
|
|
623
|
+
sdata_class = Base
|
|
624
|
+
else:
|
|
625
|
+
raise ModuleNotFoundError(f"sdata.base.class_factory Error {e}")
|
|
626
|
+
|
|
627
|
+
cls = type(class_name, (sdata_class,), sdata_attrs)
|
|
628
|
+
|
|
629
|
+
def __init__(self, **init_kwargs: Any) -> None:
|
|
630
|
+
super(cls, self).__init__(**init_kwargs) # type: ignore
|
|
631
|
+
|
|
632
|
+
setattr(cls, '__init__', __init__)
|
|
633
|
+
|
|
634
|
+
# print(sdata_spec, sdata_class, type(sdata_class), cls)
|
|
635
|
+
instance = cls(**kwargs)
|
|
636
|
+
if sdata_class.__name__ == "Base":
|
|
637
|
+
instance.metadata["_sdata_class"].value = sdata_spec
|
|
638
|
+
# print(sdata_class, sdata_class.__name__ == "Base")
|
|
639
|
+
return instance
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def sdata_factory(
|
|
643
|
+
class_name: str,
|
|
644
|
+
sdata_class: Type = Base, # Neu: Optionale Basisklasse, default ist Base
|
|
645
|
+
sdata_attrs: Optional[Dict[str, Any]] = None,
|
|
646
|
+
**kwargs: Any
|
|
647
|
+
) -> Any:
|
|
648
|
+
"""
|
|
649
|
+
Factory function to create an instance of a dynamically generated subclass.
|
|
650
|
+
|
|
651
|
+
:param class_name: The name of the class to generate (e.g., "Material").
|
|
652
|
+
:param sdata_class: The base class to inherit from (default: Base).
|
|
653
|
+
:param sdata_attrs: Optional dict of custom attributes/methods to add to the class.
|
|
654
|
+
:param kwargs: Keyword arguments to pass to the instance initialization.
|
|
655
|
+
:return: An instance of the generated class.
|
|
656
|
+
"""
|
|
657
|
+
sdata_attrs = sdata_attrs or {}
|
|
658
|
+
|
|
659
|
+
cls = type(class_name, (sdata_class,), sdata_attrs)
|
|
660
|
+
|
|
661
|
+
def __init__(self, **init_kwargs: Any) -> None:
|
|
662
|
+
super(cls, self).__init__(**init_kwargs) # type: ignore
|
|
663
|
+
|
|
664
|
+
setattr(cls, '__init__', __init__)
|
|
665
|
+
return cls(**kwargs)
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
if __name__ == '__main__':
|
|
669
|
+
class MyFancyClass(Base):
|
|
670
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
671
|
+
super().__init__(**kwargs)
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
c = MyFancyClass(name="Häl[l]o@Sp{ö}ncer;-:.")
|
|
675
|
+
|
|
676
|
+
print(c)
|
|
677
|
+
print(c.osname)
|
|
678
|
+
material = sdata_factory(
|
|
679
|
+
"Material", name="DP800", project=None, parent=c,
|
|
680
|
+
default_attributes=[{"name": "a", "value": 1.2, "dtype": float, "label": "an a"}]
|
|
681
|
+
)
|
|
682
|
+
m2 = Base.from_json(material.to_json())
|
|
683
|
+
print(m2)
|
|
684
|
+
print(material.mdf)
|
|
File without changes
|