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
@@ -0,0 +1,221 @@
1
+ import logging
2
+ from typing import Dict, Type, List, Union, Iterable, TypeVar, Hashable, Set
3
+
4
+ T = TypeVar('T', bound=Hashable)
5
+
6
+ from sdata.sclass.process import ProcessNode, CompositeProcess
7
+
8
+
9
+ try:
10
+ import graphviz
11
+ except ImportError as e:
12
+ logging.warning("graphviz not available.")
13
+ graphviz = None
14
+
15
+ class CircularDependencyError(ValueError):
16
+ """Exception raised when circular dependencies are detected during topological sort."""
17
+
18
+ def __init__(self, remaining_graph: Dict[T, Set[T]]):
19
+ message = (
20
+ "Circular dependencies exist among these items: "
21
+ f"{{{', '.join(f'{key}: {sorted(value)}' for key, value in sorted(remaining_graph.items()))}}}"
22
+ )
23
+ super().__init__(message)
24
+ self.remaining_graph = remaining_graph
25
+
26
+ class ProcessGraph:
27
+ def __init__(self, processes: List[ProcessNode] = None, name: str = "ProcessGraph"):
28
+ self.name = name
29
+ self.data_nodes = set()
30
+ self.process_nodes = set()
31
+ self.edges = []
32
+ self.class_map: Dict[str, Type[ProcessData]] = {}
33
+ self.process_class_map: Dict[str, Union[Type[ProcessNode], Type[CompositeProcess]]] = {}
34
+ if processes is None:
35
+ processes = []
36
+ self.processes: List[ProcessNode] = processes
37
+ for process in self.processes:
38
+ self.add_process(process)
39
+
40
+ def get_dependencies(self):
41
+ dependencies = {}
42
+ for process in self.processes:
43
+ d = process.get_dependencies()
44
+ dependencies.update(d)
45
+ return dependencies
46
+
47
+ def get_subprocess_graph(self):
48
+ sg = self.__class__(name="subprocessgraph")
49
+ for p in self.processes:
50
+ for sp in p.processes:
51
+ sg.add_process(sp)
52
+ return sg
53
+
54
+ def get_dag(self, flatten=False, sort: bool = True):
55
+ dag = self.toposort(self.get_dependencies())
56
+ if flatten:
57
+ dag_flat: List[T] = []
58
+ for level in dag:
59
+ dag_flat.extend(sorted(level) if sort else list(level))
60
+ return dag_flat
61
+ else:
62
+ return dag
63
+
64
+
65
+ def __str__(self):
66
+ return f"<{self.__class__.__name__}:proc={len(self.process_nodes)}:data={len(self.data_nodes)}>"
67
+
68
+ __repr__ = __str__
69
+
70
+ def add_process(self, process_class: Union[Type[ProcessNode], ]):
71
+ self.proc_name = proc_name = process_class.__name__ if hasattr(process_class, '__name__') else process_class.name
72
+ self.process_nodes.add(proc_name)
73
+ self.process_class_map[proc_name] = process_class
74
+
75
+ self.processes.append(process_class)
76
+
77
+ # Instantiate a dummy to get attributes, as composites set them in __init__
78
+ dummy = process_class(inputs=None) # Use inputs=None for dummy
79
+ self.input_classes = input_classes = dummy.input_classes
80
+ self.output_classes = output_classes = dummy.output_classes
81
+ # computation = dummy.computation # Not needed here, but for consistency
82
+
83
+
84
+ for input_name, input_class in input_classes.items():
85
+ self.data_nodes.add(input_name)
86
+ self.class_map[input_name] = input_class
87
+ self.edges.append((input_name, proc_name))
88
+
89
+ for output_name, output_class in output_classes.items():
90
+ self.data_nodes.add(output_name)
91
+ self.class_map[output_name] = output_class
92
+ self.edges.append((proc_name, output_name))
93
+
94
+ def update_class_map(self):
95
+ self.data_nodes = set()
96
+ self.process_nodes = set()
97
+ self.edges = []
98
+ self.class_map: Dict[str, Type[ProcessData]] = {}
99
+
100
+ for input_name, input_class in self.input_classes.items():
101
+ self.data_nodes.add(input_name)
102
+ self.class_map[input_name] = input_class
103
+ self.edges.append((input_name, self.proc_name))
104
+
105
+ for output_name, output_class in self.output_classes.items():
106
+ self.data_nodes.add(output_name)
107
+ self.class_map[output_name] = output_class
108
+ self.edges.append((self.proc_name, output_name))
109
+
110
+ def to_graphviz(self, name: str = None, rankdir="TD", collapsed=True):
111
+ if name is None:
112
+ name = self.name
113
+ dot = graphviz.Digraph(name=name, graph_attr={'rankdir': rankdir})
114
+
115
+ for data in self.data_nodes:
116
+ data_cls = self.class_map[data]
117
+ label = data_cls.__name__
118
+ dot.node(data, shape='box', label=label)
119
+
120
+ for proc in self.process_nodes:
121
+ proc_cls = self.process_class_map[proc]
122
+ # Instantiate dummy to get computation
123
+ label = proc_cls.__name__
124
+ dot.node(proc, shape='component', label=label, color="lightblue", style="filled")
125
+
126
+ for src, dst in self.edges:
127
+ dot.edge(src, dst)
128
+
129
+ return dot
130
+
131
+ def to_graphviz_cluster(self, name: str = None, rankdir="TD", collapsed=True):
132
+
133
+ g = graphviz.Digraph(name=name, graph_attr={'rankdir': rankdir})
134
+
135
+ sg = self.get_subprocess_graph()
136
+
137
+ # Cluster-Subgraph als Box erstellen
138
+ with g.subgraph(name='cluster_use_eol') as cluster:
139
+ # Attribute für die Box setzen
140
+ cluster.attr(label=self.name, style='filled', color='black', peripheries='1', penwidth='1.5',
141
+ fillcolor='#efffff', labelloc="t", labeljust="l")
142
+
143
+ for data in sg.data_nodes:
144
+ if data not in self.data_nodes:
145
+ data_cls = sg.class_map[data]
146
+ label = data_cls.__name__
147
+ cluster.node(data, shape='box', label=label)
148
+
149
+ for proc in sg.process_nodes:
150
+ proc_cls = sg.process_class_map[proc]
151
+ # Instantiate dummy to get computation
152
+ label = proc_cls.__name__
153
+ cluster.node(proc, shape='component', label=label, color="lightblue", style="filled")
154
+
155
+ for data in self.data_nodes:
156
+ data_cls = self.class_map[data]
157
+ label = data_cls.__name__
158
+ g.node(data, shape='box', label=label)
159
+
160
+ for src, dst in sg.edges:
161
+ if src not in self.input_classes and src not in self.output_classes and dst not in self.input_classes and dst not in self.output_classes:
162
+ cluster.edge(src, dst)
163
+ else:
164
+ g.edge(src, dst)
165
+
166
+ return g
167
+
168
+ @staticmethod
169
+ def toposort(graph: Dict[T, Iterable[T]]) -> Iterable[Set[T]]:
170
+ """Perform a topological sort on a directed acyclic graph (DAG).
171
+
172
+ Dependencies are expressed as a dictionary where keys are nodes and values are
173
+ iterables of dependent nodes (nodes that the key depends on). The output is a
174
+ generator yielding sets of nodes in topological order. The first set contains
175
+ nodes with no dependencies, and each subsequent set contains nodes that depend
176
+ only on nodes in preceding sets.
177
+
178
+ If the graph contains cycles, a CircularDependencyError is raised.
179
+
180
+ Args:
181
+ graph: A dictionary representing the dependency graph.
182
+
183
+ Yields:
184
+ Sets of nodes in topological order.
185
+
186
+ Raises:
187
+ CircularDependencyError: If a cycle is detected in the graph.
188
+ """
189
+ if not graph:
190
+ return
191
+
192
+ # Create a copy of the graph, converting dependencies to sets and removing self-dependencies.
193
+ graph = {node: set(dep for dep in deps if dep != node) for node, deps in graph.items()}
194
+
195
+ # Identify nodes that appear only as dependencies (no outgoing edges defined).
196
+ extra_nodes = {dep for deps in graph.values() for dep in deps} - set(graph)
197
+ graph.update({node: set() for node in extra_nodes})
198
+
199
+ while True:
200
+ # Find nodes with no remaining dependencies.
201
+ independent_nodes = {node for node, deps in graph.items() if not deps}
202
+ if not independent_nodes:
203
+ break
204
+ yield independent_nodes
205
+ # Remove processed nodes from the graph.
206
+ graph = {
207
+ node: (deps - independent_nodes)
208
+ for node, deps in graph.items()
209
+ if node not in independent_nodes
210
+ }
211
+
212
+ if graph:
213
+ raise CircularDependencyError(graph)
214
+
215
+ def get_process_graph(processes: List[Type[ProcessNode]]) -> ProcessGraph:
216
+ g = ProcessGraph()
217
+ for process in processes:
218
+ g.add_process(process)
219
+ dot_graph = g.to_graphviz(name="CombinedProcess", rankdir="TD", collapsed=True)
220
+ return dot_graph
221
+
@@ -0,0 +1,135 @@
1
+ import pandas as pd
2
+ import io
3
+ import os
4
+ import base64
5
+ from typing import List, Dict, Any, Optional, Type, Literal, Union, Tuple, BinaryIO, Iterable
6
+ import logging
7
+ from pathlib import Path
8
+ from sdata import SUUID
9
+ from sdata.metadata import Metadata, Attribute
10
+ # from sdata.sclass.blob import Blob # Assuming Blob is in sdata.blob or adjust import
11
+ from sdata.base import Base
12
+ import hashlib
13
+ import datetime
14
+ from sdata.timestamp import get_utc_timestamp
15
+ import zipfile
16
+
17
+ logger = logging.getLogger(__name__)
18
+ from sdata.sclass.filereference import FileReference, FileReferences
19
+
20
+
21
+ class ZipFile(Base):
22
+ SDATA_CLS = "sdata.sclass.zipfile.ZipFile"
23
+
24
+ def __init__(
25
+ self,
26
+ **kwargs: Any
27
+ ) -> None:
28
+ """
29
+ Initialize FileReferences with content type, value, and filetype.
30
+ :param kwargs: Keyword arguments passed to Base.__init__ (e.g., name, description).
31
+ :raises ValueError: If invalid content_type or mismatched value type.
32
+ """
33
+ super().__init__(**kwargs)
34
+ self.filereferences = {}
35
+
36
+ def add(self, filepath) -> None:
37
+ filereference = FileReference.from_file(filepath)
38
+ self.filereferences[filereference.sname] = filereference
39
+
40
+ def get_filereferences(self) -> Iterable[FileReference]:
41
+ return self.filereferences.values()
42
+
43
+ def get_filereferences_df(self) -> pd.DataFrame:
44
+ filereferences = []
45
+ for sname, filereference in self.filereferences.items():
46
+ filereferences.append(filereference.to_dataframe())
47
+ return pd.concat(filereferences)
48
+
49
+ def to_zip(
50
+ self,
51
+ filepath: Optional[Union[str, Path]] = None,
52
+ *,
53
+ compresslevel: int = 6,
54
+ deterministic: bool = True,
55
+ ) -> io.BytesIO:
56
+ """
57
+ Serialisiert das Objekt via to_json() und packt es als data.json in ein ZIP.
58
+ - Wenn 'filepath' gesetzt ist, wird die ZIP-Datei geschrieben.
59
+ - Rückgabe ist immer ein BytesIO, beginnend bei Position 0.
60
+ """
61
+
62
+
63
+ buf = io.BytesIO()
64
+ compression = zipfile.ZIP_DEFLATED
65
+
66
+ # Optional: deterministische ZIPs (feste Timestamp -> reproduzierbar)
67
+ for filereference in self.filereferences.values():
68
+ arcname = f"{filereference.sname}.sjson"
69
+ json_str = filereference.to_json()
70
+ if deterministic:
71
+ info = zipfile.ZipInfo(arcname, date_time=(1980, 1, 1, 0, 0, 0))
72
+ info.compress_type = compression
73
+ with zipfile.ZipFile(buf, mode="w", compression=compression, compresslevel=compresslevel) as zf:
74
+ zf.writestr(info, json_str)
75
+ else:
76
+ with zipfile.ZipFile(buf, mode="w", compression=compression, compresslevel=compresslevel) as zf:
77
+ zf.writestr(arcname, json_str)
78
+
79
+ buf.seek(0)
80
+
81
+ if filepath is not None:
82
+ p = Path(filepath)
83
+ p.parent.mkdir(parents=True, exist_ok=True)
84
+ p.write_bytes(buf.getvalue())
85
+ buf.seek(0)
86
+
87
+ return buf
88
+
89
+ @classmethod
90
+ def from_zip(
91
+ cls,
92
+ src: Union[str, Path, bytes, io.BytesIO, BinaryIO],
93
+ *,
94
+ member: Optional[str] = None,
95
+ encoding: str = "utf-8",
96
+ ):
97
+ """
98
+ Erzeugt eine Instanz aus einem ZIP, das eine JSON-Datei enthält.
99
+ 'src' kann ein Dateipfad, Bytes, BytesIO oder beliebiger file-like Stream sein.
100
+ - 'member': Name des JSON-Eintrags im ZIP. Wenn None:
101
+ * nimm die einzige .json-Datei
102
+ * oder, wenn nur ein Eintrag existiert, nimm diesen
103
+ * sonst Fehler (Mehrdeutigkeit).
104
+ """
105
+ def _open_zipfile(source):
106
+ # Gibt einen Kontextmanager zurück, der ein ZipFile liefert
107
+ if isinstance(source, (str, Path)):
108
+ return zipfile.ZipFile(source, mode="r")
109
+ if isinstance(source, bytes):
110
+ return zipfile.ZipFile(io.BytesIO(source), mode="r")
111
+ if isinstance(source, io.BytesIO):
112
+ source.seek(0)
113
+ return zipfile.ZipFile(source, mode="r")
114
+ # generischer file-like Stream
115
+ try:
116
+ source.seek(0)
117
+ except Exception: # pragma: no cover - defensiv; ZipFile braucht danach seek
118
+ pass
119
+ return zipfile.ZipFile(source, mode="r")
120
+
121
+ with _open_zipfile(src) as zf:
122
+ names = zf.namelist()
123
+ chosen = member
124
+ if chosen is None:
125
+ json_members = [n for n in names if n.lower().endswith(".sjson")]
126
+ if len(json_members) == 1:
127
+ chosen = json_members[0]
128
+ elif len(json_members) == 0 and len(names) == 1:
129
+ chosen = names[0]
130
+ else:
131
+ raise ValueError(
132
+ f"ZIP enthält {len(names)} Einträge ({names}). Bitte 'member' angeben."
133
+ )
134
+ data = zf.read(chosen).decode(encoding)
135
+ return cls.from_json(data)