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/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
@@ -0,0 +1,12 @@
1
+ from ._remove import remove
2
+ from ._load import load
3
+ from ._dump import dump
4
+ from ._transplant import transplant
5
+ from ._insert import insert
6
+ from ._exif import *
7
+ from ._exceptions import *
8
+
9
+
10
+
11
+ #VERSION = '1.1.3'
12
+ __version__ = '1.1.3'