sdata 0.24.0__tar.gz → 0.25.2__tar.gz

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 (106) hide show
  1. {sdata-0.24.0/sdata.egg-info → sdata-0.25.2}/PKG-INFO +25 -6
  2. {sdata-0.24.0 → sdata-0.25.2}/README.md +1 -1
  3. sdata-0.25.2/pyproject.toml +3 -0
  4. {sdata-0.24.0 → sdata-0.25.2}/sdata/__init__.py +1 -1
  5. {sdata-0.24.0 → sdata-0.25.2}/sdata/data.py +77 -19
  6. sdata-0.25.2/sdata/iolib/hashtree.py +264 -0
  7. sdata-0.25.2/sdata/iolib/json1sqlitestore.py +596 -0
  8. sdata-0.25.2/sdata/iolib/jsonsqlitestore.py +691 -0
  9. sdata-0.25.2/sdata/iolib/owncloudfs.py +105 -0
  10. {sdata-0.24.0 → sdata-0.25.2}/sdata/iolib/vault.py +17 -4
  11. {sdata-0.24.0 → sdata-0.25.2}/sdata/metadata.py +3 -3
  12. sdata-0.25.2/sdata/node.py +145 -0
  13. sdata-0.25.2/sdata/parameter.py +411 -0
  14. sdata-0.25.2/sdata/parameters_deprecated.py +394 -0
  15. {sdata-0.24.0 → sdata-0.25.2}/sdata/suuid.py +18 -0
  16. {sdata-0.24.0 → sdata-0.25.2/sdata.egg-info}/PKG-INFO +25 -6
  17. {sdata-0.24.0 → sdata-0.25.2}/sdata.egg-info/SOURCES.txt +11 -0
  18. sdata-0.25.2/tests/iolib/test_hasttree.py +67 -0
  19. sdata-0.25.2/tests/iolib/test_jsonsqlitestore.py +156 -0
  20. sdata-0.25.2/tests/test_parameter.py +118 -0
  21. {sdata-0.24.0 → sdata-0.25.2}/LICENSE-MIT +0 -0
  22. {sdata-0.24.0 → sdata-0.25.2}/MANIFEST.in +0 -0
  23. {sdata-0.24.0 → sdata-0.25.2}/sdata/blob.py +0 -0
  24. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/__init__.py +0 -0
  25. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/attrdict/__init__.py +0 -0
  26. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/attrdict/default.py +0 -0
  27. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/attrdict/dictionary.py +0 -0
  28. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/attrdict/mapping.py +0 -0
  29. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/attrdict/merge.py +0 -0
  30. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/attrdict/mixins.py +0 -0
  31. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/piexif/__init__.py +0 -0
  32. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/piexif/_common.py +0 -0
  33. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/piexif/_dump.py +0 -0
  34. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/piexif/_exceptions.py +0 -0
  35. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/piexif/_exif.py +0 -0
  36. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/piexif/_insert.py +0 -0
  37. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/piexif/_load.py +0 -0
  38. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/piexif/_remove.py +0 -0
  39. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/piexif/_transplant.py +0 -0
  40. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/piexif/_webp.py +0 -0
  41. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/piexif/helper.py +0 -0
  42. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/ranger/__init__.py +0 -0
  43. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/ranger/collections/__init__.py +0 -0
  44. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/ranger/collections/rangebucketmap.py +0 -0
  45. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/ranger/collections/rangemap.py +0 -0
  46. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/ranger/collections/rangeset.py +0 -0
  47. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/ranger/range/__init__.py +0 -0
  48. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/ranger/range/cut.py +0 -0
  49. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/ranger/range/range.py +0 -0
  50. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/semver.py +0 -0
  51. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/simple_graph_db/LICENSE +0 -0
  52. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/simple_graph_db/__init__.py +0 -0
  53. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/simple_graph_db/database.py +0 -0
  54. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/simple_graph_db/schema.sql +0 -0
  55. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/sobol_seq.py +0 -0
  56. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/sortedcontainers/LICENSE +0 -0
  57. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/sortedcontainers/__init__.py +0 -0
  58. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/sortedcontainers/sorteddict.py +0 -0
  59. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/sortedcontainers/sortedlist.py +0 -0
  60. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/sortedcontainers/sortedset.py +0 -0
  61. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/sqlitedict.py +0 -0
  62. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/timeflake/__init__.py +0 -0
  63. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/timeflake/flake.py +0 -0
  64. {sdata-0.24.0 → sdata-0.25.2}/sdata/contrib/timeflake/utils.py +0 -0
  65. {sdata-0.24.0 → sdata-0.25.2}/sdata/doe.py +0 -0
  66. {sdata-0.24.0 → sdata-0.25.2}/sdata/experiments/__init__.py +0 -0
  67. {sdata-0.24.0 → sdata-0.25.2}/sdata/experiments/ks2.py +0 -0
  68. {sdata-0.24.0 → sdata-0.25.2}/sdata/experiments/lapshear.py +0 -0
  69. {sdata-0.24.0 → sdata-0.25.2}/sdata/experiments/material.py +0 -0
  70. {sdata-0.24.0 → sdata-0.25.2}/sdata/image.py +0 -0
  71. {sdata-0.24.0 → sdata-0.25.2}/sdata/iolib/__init__.py +0 -0
  72. {sdata-0.24.0 → sdata-0.25.2}/sdata/iolib/db.py +0 -0
  73. {sdata-0.24.0 → sdata-0.25.2}/sdata/iolib/hdf.py +0 -0
  74. {sdata-0.24.0 → sdata-0.25.2}/sdata/iolib/isomme.py +0 -0
  75. {sdata-0.24.0 → sdata-0.25.2}/sdata/iolib/pgp.py +0 -0
  76. {sdata-0.24.0 → sdata-0.25.2}/sdata/iolib/pud.py +0 -0
  77. {sdata-0.24.0 → sdata-0.25.2}/sdata/iolib/simple_signature.py +0 -0
  78. {sdata-0.24.0 → sdata-0.25.2}/sdata/timestamp.py +0 -0
  79. {sdata-0.24.0 → sdata-0.25.2}/sdata/tools.py +0 -0
  80. {sdata-0.24.0 → sdata-0.25.2}/sdata/workbook.py +0 -0
  81. {sdata-0.24.0 → sdata-0.25.2}/sdata.egg-info/dependency_links.txt +0 -0
  82. {sdata-0.24.0 → sdata-0.25.2}/sdata.egg-info/requires.txt +0 -0
  83. {sdata-0.24.0 → sdata-0.25.2}/sdata.egg-info/top_level.txt +0 -0
  84. {sdata-0.24.0 → sdata-0.25.2}/setup.cfg +0 -0
  85. {sdata-0.24.0 → sdata-0.25.2}/setup.py +0 -0
  86. {sdata-0.24.0 → sdata-0.25.2}/tests/__init__.py +0 -0
  87. {sdata-0.24.0 → sdata-0.25.2}/tests/iolib/__init__.py +0 -0
  88. {sdata-0.24.0 → sdata-0.25.2}/tests/iolib/pud/__init__.py +0 -0
  89. {sdata-0.24.0 → sdata-0.25.2}/tests/iolib/pud/test_pud.py +0 -0
  90. {sdata-0.24.0 → sdata-0.25.2}/tests/iolib/test_hdf.py +0 -0
  91. {sdata-0.24.0 → sdata-0.25.2}/tests/test_attribute.py +0 -0
  92. {sdata-0.24.0 → sdata-0.25.2}/tests/test_blob.py +0 -0
  93. {sdata-0.24.0 → sdata-0.25.2}/tests/test_data.py +0 -0
  94. {sdata-0.24.0 → sdata-0.25.2}/tests/test_image.py +0 -0
  95. {sdata-0.24.0 → sdata-0.25.2}/tests/test_io.py +0 -0
  96. {sdata-0.24.0 → sdata-0.25.2}/tests/test_ks2.py +0 -0
  97. {sdata-0.24.0 → sdata-0.25.2}/tests/test_metadata.py +0 -0
  98. {sdata-0.24.0 → sdata-0.25.2}/tests/test_sqlite.py +0 -0
  99. {sdata-0.24.0 → sdata-0.25.2}/tests/test_suuid.py +0 -0
  100. {sdata-0.24.0 → sdata-0.25.2}/tests/test_table.py +0 -0
  101. {sdata-0.24.0 → sdata-0.25.2}/tests/test_testprogram.py +0 -0
  102. {sdata-0.24.0 → sdata-0.25.2}/tests/test_timeflake.py +0 -0
  103. {sdata-0.24.0 → sdata-0.25.2}/tests/test_timestamp.py +0 -0
  104. {sdata-0.24.0 → sdata-0.25.2}/tests/test_tools.py +0 -0
  105. {sdata-0.24.0 → sdata-0.25.2}/tests/test_vault.py +0 -0
  106. {sdata-0.24.0 → sdata-0.25.2}/tests/test_workbook.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: sdata
3
- Version: 0.24.0
3
+ Version: 0.25.2
4
4
  Summary: a structured data format
5
5
  Home-page: https://github.com/lepy/sdata
6
6
  Author: Lepy
@@ -9,7 +9,6 @@ Maintainer: Lepy
9
9
  Maintainer-email: lepy@tuta.io
10
10
  License: MIT/Apache-2.0
11
11
  Keywords: open data
12
- Platform: UNKNOWN
13
12
  Classifier: Development Status :: 3 - Alpha
14
13
  Classifier: Intended Audience :: Developers
15
14
  Classifier: License :: OSI Approved :: MIT License
@@ -26,6 +25,28 @@ Classifier: Programming Language :: Python :: Implementation :: CPython
26
25
  Classifier: Programming Language :: Python :: Implementation :: PyPy
27
26
  Description-Content-Type: text/markdown
28
27
  License-File: LICENSE-MIT
28
+ Requires-Dist: numpy
29
+ Requires-Dist: pandas
30
+ Requires-Dist: tabulate
31
+ Requires-Dist: xlrd
32
+ Requires-Dist: openpyxl
33
+ Requires-Dist: xlsxwriter
34
+ Requires-Dist: pytz
35
+ Requires-Dist: requests
36
+ Requires-Dist: Pillow
37
+ Dynamic: author
38
+ Dynamic: author-email
39
+ Dynamic: classifier
40
+ Dynamic: description
41
+ Dynamic: description-content-type
42
+ Dynamic: home-page
43
+ Dynamic: keywords
44
+ Dynamic: license
45
+ Dynamic: license-file
46
+ Dynamic: maintainer
47
+ Dynamic: maintainer-email
48
+ Dynamic: requires-dist
49
+ Dynamic: summary
29
50
 
30
51
 
31
52
  [![PyPI version](https://badge.fury.io/py/sdata.svg)](https://badge.fury.io/py/sdata)
@@ -86,7 +107,7 @@ force force 1.234 float kN x force
86
107
 
87
108
 
88
109
  * [Das sdata-Format - slides](https://lepy.github.io/sdata/ipynb/Das_sdata_Format.slides.html#)
89
-
110
+ * https://deepwiki.com/lepy/sdata
90
111
 
91
112
  ## Demo App
92
113
 
@@ -128,5 +149,3 @@ Try to paste some Excel-Data in the forms ...
128
149
  * [sdata](https://doi.org/10.5281/zenodo.4311396)
129
150
  * Ingolf Lepenies. (2020, December 8). sdata - a structured data format (Version 0.8.4). Zenodo. http://doi.org/10.5281/zenodo.4311397
130
151
 
131
-
132
-
@@ -57,7 +57,7 @@ force force 1.234 float kN x force
57
57
 
58
58
 
59
59
  * [Das sdata-Format - slides](https://lepy.github.io/sdata/ipynb/Das_sdata_Format.slides.html#)
60
-
60
+ * https://deepwiki.com/lepy/sdata
61
61
 
62
62
  ## Demo App
63
63
 
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64.0.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
@@ -1,7 +1,7 @@
1
1
  # -*-coding: utf-8-*-
2
2
  from __future__ import division
3
3
 
4
- __version__ = '0.24.0'
4
+ __version__ = '0.25.2'
5
5
  __revision__ = None
6
6
  __version_info__ = tuple([int(num) for num in __version__.split('.')])
7
7
 
@@ -265,6 +265,13 @@ class Data(object):
265
265
  s.update(tablestr)
266
266
  return s.hexdigest()
267
267
 
268
+ @property
269
+ def sha3_256_metadata(self):
270
+ s = hashlib.sha3_256()
271
+ metadatastr = self.metadata.to_json().encode(errors="replace")
272
+ s.update(metadatastr)
273
+ return s.hexdigest()
274
+
268
275
  @property
269
276
  def sha3_256(self):
270
277
  """Return a SHA3 hash of the sData object with a hashbit length of 32 bytes.
@@ -281,8 +288,8 @@ class Data(object):
281
288
  metadatastr = self.metadata.to_json().encode(errors="replace")
282
289
  s.update(metadatastr)
283
290
  if self.table is not None:
284
- tablestr = self.table.to_json().encode(errors="replace")
285
- s.update(tablestr)
291
+ # tablestr = self.table.to_json().encode(errors="replace")
292
+ s.update(self.sha3_256_table.encode(errors="replace"))
286
293
  s.update(self.description.encode(errors="replace"))
287
294
  return s.hexdigest()
288
295
 
@@ -831,6 +838,62 @@ class Data(object):
831
838
  """
832
839
  return [(x.name, x.dir()) for x in self.group.values()]
833
840
 
841
+ def to_dataframe(self):
842
+ """Export the DataFrame with additional metadata and description.
843
+
844
+ Returns:
845
+ pandas.DataFrame: A copy of the DataFrame with added attributes.
846
+ """
847
+ df = self.df.copy()
848
+ df.attrs["!sdata"] = {
849
+ "metadata": self.metadata.to_dict(),
850
+ "description": self.description
851
+ }
852
+ return df
853
+
854
+
855
+ def to_parquet(self, filepath=None, **kwargs):
856
+ """
857
+
858
+ """
859
+ engine = kwargs.get("engine", "pyarrow")
860
+ df = self.df.copy()
861
+ #df["!sdata_index"] = df.index
862
+ #df.reset_index(inplace=True)
863
+ df.attrs["!sdata"] = {"metadata": self.metadata.to_dict(),
864
+ "description": self.description}
865
+ df.to_parquet(filepath, engine=engine)
866
+
867
+ @classmethod
868
+ def from_parquet(cls, filepath):
869
+ """load data from parquet file
870
+
871
+ :param filepath:
872
+ :return: Data object
873
+ """
874
+ try:
875
+ if os.path.exists(filepath):
876
+ df = pd.read_parquet(filepath)
877
+ tt = cls(name=filepath)
878
+ tt.table = df
879
+
880
+ attrs = df.attrs.get("!sdata")
881
+ try:
882
+ tt.metadata = tt.metadata.from_dict(attrs["metadata"])
883
+ except:
884
+ logger.warning(f"ignore metadata {filepath}")
885
+ try:
886
+ tt.description = attrs.get("description")
887
+ except:
888
+ logger.warning(f"ignore description {filepath}")
889
+ if attrs is not None:
890
+ tt.df.attrs.pop("!sdata")
891
+ else:
892
+ raise Exception("parquet file '{}' not available".format(filepath))
893
+ return tt
894
+ except Exception as exp:
895
+ raise
896
+
834
897
  def to_xlsx_byteio(self):
835
898
  """get xlsx as byteio
836
899
 
@@ -941,36 +1004,31 @@ class Data(object):
941
1004
  """
942
1005
  try:
943
1006
  if os.path.exists(filepath):
944
- wb = openpyxl.load_workbook(filename=filepath)
945
- sheetnames = wb.sheetnames
946
-
947
1007
  tt = cls(name=filepath)
948
1008
 
949
1009
  # read df
950
- if "table" in sheetnames:
1010
+ try:
951
1011
  tt.table = pd.read_excel(filepath, sheet_name="table", index_col=0)
952
- else:
1012
+ except:
953
1013
  logger.info("no table data in '{}'".format(filepath))
954
1014
  dfm = pd.read_excel(filepath, sheet_name="metadata")
955
- dfm = dfm.set_index(dfm.name.values)
1015
+ dfm.set_index(dfm.name.values, inplace=True)
956
1016
  # dfm["value"] = dfm["value"].replace(np.nan, None)
957
1017
  dfm["description"] = dfm["description"].replace(np.nan, '')
958
1018
  dfm["label"] = dfm["label"].replace(np.nan, '')
959
- # print("!data.from_xlsx", dfm)
1019
+
1020
+ # read metadata
960
1021
  tt.metadata = tt.metadata.from_dataframe(dfm)
1022
+ for attr in tt.metadata.sdata_attributes.values():
1023
+ if attr.value == "nan":
1024
+ attr.value = ""
961
1025
 
962
1026
  # read description
963
- if "description" in sheetnames:
964
- cells = []
965
- for cell in wb["description"]["A"]:
966
- if cell.value is not None:
967
- cells.append(cell.value)
968
- else:
969
- cells.append("")
970
- tt.description = "\n".join(cells)
971
- else:
1027
+ try:
1028
+ dfd = pd.read_excel(filepath, sheet_name="description", header=None)
1029
+ tt.description = "\n".join(dfd[0])
1030
+ except:
972
1031
  logger.info("no description in '{}'".format(filepath))
973
-
974
1032
  return tt
975
1033
  else:
976
1034
  raise Exception("excel file '{}' not available".format(filepath))
@@ -0,0 +1,264 @@
1
+ import uuid
2
+ import hashlib
3
+ import json
4
+
5
+ NAMESPACE_SDATA = uuid.uuid5(uuid.NAMESPACE_DNS, "sdata")
6
+
7
+
8
+ def stable_hash_from_list(input_list):
9
+ """
10
+ data = [3.14159265, 42, "hello", 2.7182818]
11
+ hashstr = sdata.iolib.hashtree.stable_hash_from_list(data)
12
+ hashstr == "f54068f9b31cb94d56b454d36c1458877e5e45589684038b5c60097ad45b9052"
13
+ """
14
+ # Konvertiere die Liste in eine normalisierte Zeichenkette
15
+ normalized_str = str([
16
+ round(item, 8) if isinstance(item, float) else item
17
+ for item in input_list
18
+ ])
19
+ # Erstelle einen Hash mit hashlib (z. B. SHA256)
20
+ hash_object = hashlib.sha256(normalized_str.encode('utf-8'))
21
+ return hash_object.hexdigest()
22
+
23
+
24
+ def generate_uuid_from_hash(hash_value):
25
+ """
26
+ data = [3.14159265, 42, "hello", 2.7182818]
27
+ uid = sdata.iolib.hashtree.stable_uuid_from_list(data)
28
+ uid.hex == "f09e3370d8f953398366753ca7e16eb9"
29
+ """
30
+ # Konvertiere den Hashwert in einen String (falls nötig)
31
+ hash_string = str(hash_value)
32
+ namespace = uuid.uuid5(uuid.NAMESPACE_DNS, "sdata")
33
+ return uuid.uuid5(namespace, hash_string).hex
34
+
35
+
36
+ def stable_uuid_from_list(input_list):
37
+ hashstr = stable_hash_from_list(input_list)
38
+ return generate_uuid_from_hash(hashstr)
39
+
40
+ def uuids_from_hashes(hashes):
41
+ """
42
+
43
+ """
44
+ return [generate_uuid_from_hash(hashstr) for hashstr in hashes]
45
+
46
+ def stable_hash_from_nested_list(nested_list):
47
+ """
48
+ nested_list = [[3.14159265, 42, "hello", 2.7182818],
49
+ [1, 1.2,"ssd"],
50
+ ["a"],
51
+ ]
52
+
53
+ ['f54068f9b31cb94d56b454d36c1458877e5e45589684038b5c60097ad45b9052',
54
+ 'ce6bc57403f6d39b1dd30fe7aca428d5de956758551facfbca290500ad9100b3',
55
+ 'a326618a43c0b61aff497f4d8a82f4857fe952c5fae156ab94facf4adbf7dcb4']
56
+ """
57
+ return [stable_hash_from_list(x) for x in nested_list]
58
+
59
+
60
+ def stable_uuids_from_nested_list(nested_list):
61
+ """
62
+ nested_list = [[3.14159265, 42, "hello", 2.7182818],
63
+ [1, 1.2,"ssd"],
64
+ ["a"],
65
+ ]
66
+
67
+ ['f09e3370d8f953398366753ca7e16eb9',
68
+ 'b0465a6b35ce5fd28338e04233c422d7',
69
+ '68bb365dbf5c556faf848a6c8c72b205']
70
+ """
71
+ return [stable_uuid_from_list(x) for x in nested_list]
72
+
73
+
74
+ def generate_combined_hash(parent_hash, own_hash):
75
+ """
76
+ Berechnet einen neuen Hash basierend auf einem Parent-Hash und einem eigenen Hash.
77
+ Falls der Parent-Hash None ist, wird ein leerer String verwendet.
78
+
79
+ :param parent_hash: Der Hash des Elternknotens (String, None für Root).
80
+ :param own_hash: Der eigene Hash des aktuellen Knotens (String).
81
+ :return: Der kombinierte Hash (String).
82
+ """
83
+ parent_hash = parent_hash or "" # Falls None, verwende leeren String
84
+ combined = parent_hash + own_hash
85
+ return hashlib.sha256(combined.encode('utf-8')).hexdigest()
86
+
87
+
88
+ def cumulative_hash_list(hashes):
89
+ """
90
+ Berechnet kumulative Hashes basierend auf einer Liste, wobei der parent_hash
91
+ jeweils der vorherige Hash in der Liste ist.
92
+
93
+ :param hashes: Liste von Hashes (Strings).
94
+ :return: Liste der kumulativ berechneten Hashes.
95
+ """
96
+ if not hashes:
97
+ return []
98
+
99
+ cumulative_hashes = [hashes[0]] # Der erste Hash ist unverändert
100
+ for i in range(1, len(hashes)):
101
+ parent_hash = cumulative_hashes[i - 1]
102
+ current_hash = hashes[i]
103
+ cumulative_hashes.append(generate_combined_hash(parent_hash, current_hash))
104
+ return cumulative_hashes
105
+
106
+ def stable_cum_uuids_from_nested_list(nested_list):
107
+ """calculate stable uuids from cumulative hashes of a nested list
108
+ nested_list = [[3.14159265, 42, "hello", 2.7182818],
109
+ [1, 1.2,"ssd"],
110
+ ["a"],
111
+ ]
112
+
113
+ ['f09e3370d8f953398366753ca7e16eb9',
114
+ 'b8f6eb73756c5d3d8f7456003bf0dc6d',
115
+ 'f6dd12b913135291b58bcd2f5f63f157']
116
+
117
+ """
118
+
119
+ cum_hashes = stable_cum_hashes_from_nested_list(nested_list)
120
+ uuids = uuids_from_hashes(cum_hashes)
121
+ return uuids
122
+
123
+ def stable_cum_uuid_from_nested_list(nested_list):
124
+ """calculate stable uuid from cumulative hashes of a nested list
125
+ nested_list = [[3.14159265, 42, "hello", 2.7182818],
126
+ [1, 1.2,"ssd"],
127
+ ["a"],
128
+ ]
129
+
130
+
131
+ 'f6dd12b913135291b58bcd2f5f63f157'
132
+
133
+ """
134
+
135
+ hashes = stable_cum_hashes_from_nested_list(nested_list)
136
+ cum_uuid = generate_uuid_from_hash(hashes[-1])
137
+ return cum_uuid
138
+
139
+ def stable_cum_hashes_from_nested_list(nested_list):
140
+ """
141
+ nested_list = [[3.14159265, 42, "hello", 2.7182818],
142
+ [1, 1.2,"ssd"],
143
+ ["a"],
144
+ ]
145
+
146
+ ['3a316d6d3226f84c1e46e4447fa8d5fd800bff4a1bc6498152523cd4a602b69b',
147
+ '9f8ba3b772342d1f57ae6fe37e7e0da8f93f5ccacaf943d605d254d57489c7e3',
148
+ '03da67b7f8157f7413861ffa25069581ae5d95d59eeb0c2f599f48539dcdf38c']
149
+
150
+ """
151
+
152
+ hashes = stable_hash_from_nested_list(nested_list)
153
+ cum_hashes = cumulative_hash_list(hashes)
154
+ return cum_hashes
155
+
156
+ class TreeNode:
157
+ def __init__(self, name, parent=None):
158
+ self.name = name # Der Name des Knotens
159
+ self.children = {} # Kinderknoten
160
+ self.parent = parent # Elternknoten
161
+
162
+ def __lt__(self, other):
163
+ """
164
+ Ermöglicht die Sortierung von TreeNode-Objekten basierend auf ihrem Namen.
165
+ """
166
+ return self.name < other.name
167
+
168
+ def get_child(self, path):
169
+ """
170
+ Navigiert den Pfad und gibt den Zielknoten zurück, falls vorhanden.
171
+ :param path: Ein Pfad in der Form "child1/child2".
172
+ :return: Der Zielknoten oder None, falls der Pfad nicht existiert.
173
+ """
174
+ path_list = path.strip("/").split("/") # Pfad in eine Liste umwandeln
175
+ current_node = self
176
+ for part in path_list:
177
+ if part in current_node.children:
178
+ current_node = current_node.children[part]
179
+ else:
180
+ return None # Pfad existiert nicht
181
+ return current_node
182
+
183
+ def add_path(self, path):
184
+ path_list = path.strip("/").split("/")
185
+ self.add_path_list(path_list)
186
+
187
+ def add_path_list(self, path_list):
188
+ if not path_list:
189
+ return
190
+
191
+ head, *tail = path_list
192
+ if head not in self.children:
193
+ self.children[head] = TreeNode(head, parent=self)
194
+ self.children[head].add_path_list(tail)
195
+
196
+ def repr(self, level=0):
197
+ ret = " " * level + repr(self.name) + "\n"
198
+ for child in self.children.values():
199
+ ret += child.__repr__(level + 1)
200
+ return ret
201
+
202
+ def __repr__(self):
203
+ return self.full_name()
204
+
205
+ def __str__(self):
206
+ return self.full_name()
207
+
208
+ def to_dict(self):
209
+ """
210
+ Konvertiert den Baum in ein verschachteltes Dictionary.
211
+ """
212
+ return {
213
+ "name": self.name,
214
+ "children": {name: child.to_dict() for name, child in self.children.items()}
215
+ }
216
+
217
+ def to_json(self):
218
+ """
219
+ Exportiert den Baum in JSON-Format.
220
+ """
221
+ return json.dumps(self.to_dict(), indent=2)
222
+
223
+ def print_tree(self, padding="", is_last=True, is_root=True):
224
+ """
225
+ Druckt den Baum in einer visuellen Struktur mit Verzweigungen.
226
+ :param padding: Aktuelle Einrückung.
227
+ :param is_last: Ob der aktuelle Knoten das letzte Kind ist.
228
+ :param is_root: Ob der aktuelle Knoten der Root-Knoten ist.
229
+ """
230
+ if is_root:
231
+ print(self.name) # Root-Knoten ohne Verzweigungssymbol
232
+ else:
233
+ branch = "└─" if is_last else "├─"
234
+ print(padding + branch + self.name)
235
+ padding += " " if is_last else "│ "
236
+
237
+ children = sorted(self.children.values()) # Alphabetische Sortierung
238
+ for count, child in enumerate(children):
239
+ child.print_tree(padding, count == (len(children) - 1), is_root=False)
240
+
241
+ def full_name(self):
242
+ """
243
+ Gibt den vollständigen Pfad des Knotens zurück, von der Wurzel bis zu diesem Knoten.
244
+ :return: Ein String, der den vollständigen Pfad darstellt.
245
+ """
246
+ if self.parent is None:
247
+ return self.name # Root-Knoten
248
+ return f"{self.parent.full_name()}/{self.name}"
249
+
250
+ def get_subtree_by_name(self, name):
251
+ """
252
+ Sucht im Baum nach einem Knoten mit dem angegebenen Namen.
253
+ :param name: Der Name des gesuchten Knotens.
254
+ :return: Der Knoten (TreeNode), wenn gefunden, sonst None.
255
+ """
256
+ if self.name == name:
257
+ return self # Aktueller Knoten passt
258
+
259
+ for child in self.children.values():
260
+ result = child.get_subtree_by_name(name)
261
+ if result:
262
+ return result # Knoten gefunden
263
+
264
+ return None # Kein Knoten mit dem Namen gefunden