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/sclass/blob.py ADDED
@@ -0,0 +1,362 @@
1
+ import base64
2
+ import logging
3
+ import hashlib
4
+ import os
5
+ import io
6
+ import mimetypes
7
+ import typing
8
+ from typing import Any, List, Dict, Optional, Literal, Optional, Type
9
+ from sdata.base import Base
10
+ from sdata.sclass.content import ContentIntegrityMixin
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # Import fsspec for handling various URI schemes (local file, S3, Zip, etc.)
14
+ # Note: fsspec must be installed (pip install fsspec) and for S3, also install s3fs (pip install s3fs)
15
+ try:
16
+ import fsspec
17
+ except ImportError: # pragma: no cover - optionales Backend (sdata[blob])
18
+ logger.warning('fsspec not installed')
19
+ fsspec = None
20
+
21
+ class Blob(ContentIntegrityMixin, Base):
22
+ """
23
+ A derived class from Base that represents a generic binary large object (Blob).
24
+ Stores the content in self.data['content'] as a dictionary with:
25
+ - 'type': 'bytes' for in-memory bytes (base64-encoded for serialization) or 'uri' for a filesystem URI (local path, S3 object, Zip path, etc., handled via fsspec).
26
+ - 'value': The base64-encoded bytes string (for 'bytes') or the URI string (for 'uri').
27
+ - 'filetype': The file type (e.g., 'pdf', 'png', 'jpg', 'txt', or any custom type). This is always stored and serialized.
28
+
29
+ Additionally, integrates hash calculations (SHA1 and MD5) from the provided class for integrity checks.
30
+ The actual bytes are loaded lazily when accessed via .content_bytes property, ensuring large content is not loaded unless explicitly requested.
31
+ For serialization in to_dict(), bytes are base64-encoded if type is 'bytes'; URIs are kept as-is.
32
+ Supports PDFs, images (png, jpg), or any arbitrary file types.
33
+ Uses fsspec to handle various URI schemes:
34
+ - Local file: 'file:///path/to/file.pdf' or simply '/path/to/file.pdf'
35
+ - S3: 's3://bucket/key.pdf'
36
+ - Zip: 'zip://innerfile.txt::/path/to/outer.zip'
37
+ """
38
+
39
+ ContentType = Literal['bytes', 'uri']
40
+
41
+ DEFAULT_METADATA = {
42
+ # "uuid": {
43
+ # "name": "uuid",
44
+ # "value": "123e4567-e89b-12d3-a456-426614174000",
45
+ # "unit": None,
46
+ # "dtype": "str",
47
+ # "description": "A universally unique identification number (Universally Unique Identifier) that makes the asset globally unique. It is used to avoid duplicates and enable references.",
48
+ # "label": "UUID",
49
+ # "required": True,
50
+ # "ontology": "saf:uuid"
51
+ # },
52
+ "checksum": {
53
+ "name": "checksum",
54
+ "value": None, #"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
55
+ "unit": None,
56
+ "dtype": "str",
57
+ "description": "A hash value (based on the SHA-256 algorithm) that verifies the integrity of the asset. It ensures that the resource has not been modified (e.g., during downloads or storage).",
58
+ "label": "Checksum (SHA-256)",
59
+ "required": True,
60
+ "ontology": "schema:sha256"
61
+ },
62
+ "mime_type": {
63
+ "name": "mime_type",
64
+ "value": None, #"application/pdf",
65
+ "unit": None,
66
+ "dtype": "str",
67
+ "description": "The MIME type of the resource (e.g., 'application/pdf' for PDFs or 'text/html' for web pages), which indicates how the asset should be interpreted and processed.",
68
+ "label": "MIME Type",
69
+ "required": True,
70
+ "ontology": "dcat:mediaType"
71
+ },
72
+ "source_uri": {
73
+ "name": "source_uri",
74
+ "value": None, #"https://example.com/original/file.pdf",
75
+ "unit": None,
76
+ "dtype": "str",
77
+ "description": "The original URI (Uniform Resource Identifier) from which the asset originates (e.g., a URL to a website or file).",
78
+ "label": "Source URI",
79
+ "required": False,
80
+ "ontology": "dcterms:source"
81
+ },
82
+ "creation_date": {
83
+ "name": "creation_date",
84
+ "value": None, #"2025-10-16T12:00:00Z",
85
+ "unit": None,
86
+ "dtype": "datetime",
87
+ "description": "The creation date of the asset, to track the history.",
88
+ "label": "Creation Date",
89
+ "required": True,
90
+ "ontology": "dcterms:created"
91
+ },
92
+ "modified_date": {
93
+ "name": "modified_date",
94
+ "value": None, #"2025-10-16T14:30:00Z",
95
+ "unit": None,
96
+ "dtype": "datetime",
97
+ "description": "The date of the last modification, important for version control.",
98
+ "label": "Modified Date",
99
+ "required": False,
100
+ "ontology": "dcterms:modified"
101
+ },
102
+ "publisher": {
103
+ "name": "publisher",
104
+ "value": None, #"ISO",
105
+ "unit": None,
106
+ "dtype": "str",
107
+ "description": "The publisher or owner of the resource (e.g., an organization like ISO or a company).",
108
+ "label": "Publisher",
109
+ "required": False,
110
+ "ontology": "dcterms:publisher"
111
+ },
112
+ "license": {
113
+ "name": "license",
114
+ "value": None, #"Creative Commons BY-SA 4.0",
115
+ "unit": None,
116
+ "dtype": "str",
117
+ "description": "Information about the license (e.g., Creative Commons, proprietary), which regulates the usage rights.",
118
+ "label": "License",
119
+ "required": False,
120
+ "ontology": "dcterms:license"
121
+ }
122
+ }
123
+
124
+ def __init__(
125
+ self,
126
+ content_type: Optional[ContentType] = 'bytes',
127
+ value: Optional[Any] = None, # bytes for 'bytes', str URI for 'uri'
128
+ filetype: Optional[str] = 'binary', # Default to generic binary, e.g., 'pdf', 'png', 'jpg'
129
+ **kwargs: Any
130
+ ) -> None:
131
+ """
132
+ Initialize Blob with content type, value, and filetype.
133
+ No content loading occurs here; loading is lazy via content_bytes property.
134
+
135
+ :param content_type: 'bytes' for in-memory bytes or 'uri' for filesystem URI (local, S3, Zip, etc.).
136
+ :param value: The content value - bytes object for 'bytes', URI string for 'uri' (e.g., '/local/path', 's3://bucket/key').
137
+ :param filetype: The type of the file (e.g., 'pdf', 'png', 'jpg', 'txt').
138
+ :param kwargs: Keyword arguments passed to Base.__init__ (e.g., name, description).
139
+ :raises ValueError: If invalid content_type or mismatched value type.
140
+ """
141
+ super().__init__(**kwargs)
142
+ self._content_cache = None # nicht-serialisierter Lazy-Cache (nicht in self.data)
143
+ self.metadata.update_from_dict(self.DEFAULT_METADATA)
144
+
145
+ if content_type not in typing.get_args(self.ContentType):
146
+ raise ValueError(f"Invalid content_type: {content_type}")
147
+
148
+ self.data['content'] = {
149
+ 'type': content_type,
150
+ 'filetype': filetype,
151
+ }
152
+ self._set_value(value)
153
+ self._autofill_metadata()
154
+
155
+ logger.debug(f"Created Blob '{self.sname}' with content_type '{content_type}' and filetype '{filetype}'")
156
+
157
+ def _autofill_metadata(self) -> None:
158
+ """Fill derivable metadata: ``mime_type`` from ``filetype`` and
159
+ ``creation_date`` from the reserved ``_sdata_ctime`` (no second timestamp).
160
+ Only sets values that can be derived; leaves the rest untouched.
161
+ """
162
+ mime = mimetypes.guess_type(f"x.{self.filetype}")[0]
163
+ if mime:
164
+ self.metadata.set_attr("mime_type", mime)
165
+ ctime = self.metadata.get(self.SDATA_CTIME)
166
+ if ctime is not None and ctime.value:
167
+ self.metadata.set_attr("creation_date", ctime.value)
168
+
169
+ def _set_value(self, value: Any) -> None:
170
+ """
171
+ Set the 'value' in self.data['content'] based on content_type.
172
+ For 'bytes', store as base64-encoded string; for 'uri', store as str.
173
+ Handle value=None explicitly to avoid errors during deserialization.
174
+ """
175
+ content = self.data['content']
176
+ ctype = content['type']
177
+
178
+ if value is None:
179
+ content['value'] = None
180
+ return
181
+
182
+ if ctype == 'bytes':
183
+ if not isinstance(value, bytes):
184
+ raise ValueError("For 'bytes' type, value must be a bytes object.")
185
+ content['value'] = base64.b64encode(value).decode('utf-8')
186
+ elif ctype == 'uri':
187
+ if not isinstance(value, str):
188
+ raise ValueError("For 'uri' type, value must be a string URI.")
189
+ content['value'] = value
190
+ else:
191
+ raise ValueError(f"Unknown content_type: {ctype}")
192
+
193
+ def set_content(
194
+ self,
195
+ content_type: ContentType,
196
+ value: Any,
197
+ filetype: Optional[str] = None
198
+ ) -> None:
199
+ """
200
+ Update the content type, value, and optionally filetype.
201
+ Clears any cached content to maintain lazy loading.
202
+
203
+ :param content_type: New content_type ('bytes' or 'uri').
204
+ :param value: New value (bytes or URI str).
205
+ :param filetype: Optional new filetype (e.g., 'pdf', 'png', 'jpg').
206
+ """
207
+ self.data['content']['type'] = content_type
208
+ if filetype is not None:
209
+ self.data['content']['filetype'] = filetype
210
+ self._set_value(value)
211
+ self._content_cache = None # Clear cache for lazy reloading
212
+ self._autofill_metadata()
213
+ logger.debug(
214
+ f"Updated Blob '{self.sname}' to content_type '{content_type}' and filetype '{self.data['content']['filetype']}'")
215
+
216
+ @property
217
+ def content_bytes(self) -> bytes:
218
+ """
219
+ Lazily load and retrieve the content as bytes (only when this property is accessed).
220
+ If type is 'uri', use fsspec to open and read; if 'bytes', decode from base64.
221
+ Caches the result for subsequent accesses.
222
+
223
+ :return: The content as bytes.
224
+ :raises ValueError: If loading fails or no value set.
225
+ :raises Exception: If fsspec encounters an error (e.g., invalid URI, missing dependencies like s3fs for S3).
226
+ """
227
+ if getattr(self, '_content_cache', None) is not None:
228
+ return self._content_cache
229
+
230
+ content = self.data.get('content')
231
+ if content is None:
232
+ raise ValueError("No content set in Blob.")
233
+
234
+ ctype = content.get('type')
235
+ val = content.get('value')
236
+ if val is None:
237
+ raise ValueError("No value set in content.")
238
+
239
+ if ctype == 'bytes':
240
+ loaded_bytes = base64.b64decode(val)
241
+ elif ctype == 'uri':
242
+ if fsspec is None:
243
+ raise ImportError("fsspec is required for URI loading.")
244
+ try:
245
+ with fsspec.open(val, 'rb') as f:
246
+ loaded_bytes = f.read()
247
+ except Exception as e:
248
+ raise ValueError(f"Failed to load from URI '{val}': {str(e)}")
249
+ else:
250
+ raise ValueError(f"Unknown content_type: {ctype}")
251
+
252
+ self._content_cache = loaded_bytes # Cache the loaded bytes (instance, not serialized)
253
+ return loaded_bytes
254
+
255
+ @property
256
+ def filetype(self) -> str:
257
+ """
258
+ Retrieve the filetype (no content loading required).
259
+
260
+ :return: The filetype string (e.g., 'pdf', 'png', 'jpg').
261
+ """
262
+ return self.data['content'].get('filetype', 'binary')
263
+
264
+ def exists(self) -> bool:
265
+ """
266
+ Test whether the blob content exists.
267
+ For 'uri', checks if the path/URI is accessible via fsspec; for 'bytes', always True if value set.
268
+ """
269
+ content = self.data.get('content')
270
+ if content is None:
271
+ return False
272
+
273
+ ctype = content.get('type')
274
+ val = content.get('value')
275
+ if val is None:
276
+ return False
277
+
278
+ if ctype == 'bytes':
279
+ return True # In-memory, assumes exists if set
280
+ elif ctype == 'uri':
281
+ if fsspec is None:
282
+ logger.warning('fsspec not installed')
283
+ return False
284
+ try:
285
+ fs, _, paths = fsspec.core.get_fs_token_paths(val)
286
+ return fs.exists(paths[0])
287
+ except Exception as e:
288
+ logger.warning(f"Failed to check existence for URI '{val}': {str(e)}")
289
+ return False
290
+ else:
291
+ return False
292
+
293
+ def write(self, uri: str, **kwargs: Any) -> str:
294
+ """Write the content to a destination ``uri`` via fsspec (local/S3/zip/…).
295
+
296
+ :param uri: destination URI/path (e.g. ``/out/file.pdf``, ``s3://bucket/key``).
297
+ :param kwargs: forwarded to ``fsspec.open``.
298
+ :return: the destination ``uri``.
299
+ :raises ImportError: if fsspec is not installed (``pip install sdata[blob]``).
300
+ """
301
+ if fsspec is None:
302
+ raise ImportError("fsspec is required for write() (pip install sdata[blob]).")
303
+ data = self.content_bytes
304
+ with fsspec.open(uri, "wb", **kwargs) as f:
305
+ f.write(data)
306
+ logger.info(f"Blob content written to {uri}")
307
+ return uri
308
+
309
+ def open(self, mode: str = "rb"):
310
+ """Return a file-like handle to the content; use as a context manager.
311
+
312
+ For ``uri`` content a **streaming** fsspec handle is returned (no full
313
+ in-memory load); for ``bytes`` content an :class:`io.BytesIO` over the
314
+ decoded bytes.
315
+
316
+ :param mode: open mode (default ``"rb"``).
317
+ :raises ValueError: if no content/value is set or the content_type is unknown.
318
+ :raises ImportError: if fsspec is required (uri) but not installed.
319
+ """
320
+ content = self.data.get('content')
321
+ if content is None or content.get('value') is None:
322
+ raise ValueError("No content set in Blob.")
323
+ ctype = content['type']
324
+ if ctype == 'uri':
325
+ if fsspec is None:
326
+ raise ImportError("fsspec is required for uri open() (pip install sdata[blob]).")
327
+ return fsspec.open(content['value'], mode)
328
+ elif ctype == 'bytes':
329
+ return io.BytesIO(self.content_bytes)
330
+ else:
331
+ raise ValueError(f"Unknown content_type: {ctype}")
332
+
333
+ def to_dict(self) -> Dict[str, Any]:
334
+ """
335
+ Extend Base.to_dict to include the content dict as-is (with base64 for bytes and filetype).
336
+ Does not include or load the actual content bytes.
337
+ """
338
+ data_copy = self.data.copy()
339
+ result = super().to_dict()
340
+ result['data'] = data_copy
341
+ return result
342
+
343
+ @classmethod
344
+ def from_dict(cls, d: Dict[str, Any]) -> 'Blob':
345
+ """
346
+ Create a Blob instance from a dictionary.
347
+ Restores content dict including filetype; no content loading occurs here (lazy via content_bytes).
348
+ """
349
+ instance = super().from_dict(d)
350
+ if 'content' not in instance.data:
351
+ instance.data['content'] = {'type': 'bytes', 'filetype': 'binary'}
352
+ # Validate content structure
353
+ content = instance.data['content']
354
+ if 'type' not in content or 'filetype' not in content:
355
+ raise ValueError("Invalid content structure in dict.")
356
+ if content['type'] == 'bytes' and 'value' in content:
357
+ # Ensure value is base64 string; no need to decode here
358
+ pass
359
+ elif content['type'] == 'uri' and 'value' in content:
360
+ if not isinstance(content['value'], str):
361
+ raise ValueError("URI value must be a string.")
362
+ return instance
@@ -0,0 +1,87 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Wiederverwendbarer Integritäts-/Hash-Layer (RFC 0004, Option B).
3
+
4
+ :class:`ContentIntegrityMixin` stellt Prüfsummen (``sha1``/``md5``/``sha256``),
5
+ ``size`` sowie ``update_checksum``/``verify`` über einen ``content_bytes``-Hook und
6
+ das ``metadata``-Objekt (aus :class:`~sdata.base.Base`) bereit. Genutzt von
7
+ :class:`~sdata.sclass.blob.Blob` und :class:`~sdata.sclass.dataframe.DataFrame`.
8
+ """
9
+ import io
10
+ import hashlib
11
+ import logging
12
+ from typing import Any, Optional
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class ContentIntegrityMixin:
18
+ """Hash/``verify``/``size`` über ``self.content_bytes`` + ``self.metadata``.
19
+
20
+ Subklassen liefern eine ``content_bytes``-Property (``bytes``) und besitzen
21
+ (über :class:`~sdata.base.Base`) ein ``metadata``-Objekt.
22
+ """
23
+
24
+ def _update_hash(self, hash_obj: Any, buffer_size: int = 65536) -> None:
25
+ """Speise den Hash-Objekt-Stream aus ``content_bytes`` (chunked)."""
26
+ bytes_io = io.BytesIO(self.content_bytes)
27
+ while True:
28
+ data = bytes_io.read(buffer_size)
29
+ if not data:
30
+ break
31
+ hash_obj.update(data)
32
+
33
+ def _content_hexdigest(self, algo) -> Optional[str]:
34
+ """Hex-Digest des Inhalts mit ``algo`` (z. B. ``hashlib.sha256``); ``None`` bei Fehler."""
35
+ try:
36
+ hash_obj = algo()
37
+ self._update_hash(hash_obj)
38
+ return hash_obj.hexdigest()
39
+ except Exception as exp:
40
+ logger.error(f"Failed to compute {algo().name}: {exp}")
41
+ return None
42
+
43
+ @property
44
+ def sha1(self) -> Optional[str]:
45
+ """SHA-1 hex digest of the content (lazy; ``None`` on error)."""
46
+ return self._content_hexdigest(hashlib.sha1)
47
+
48
+ @property
49
+ def md5(self) -> Optional[str]:
50
+ """MD5 hex digest of the content (lazy; ``None`` on error)."""
51
+ return self._content_hexdigest(hashlib.md5)
52
+
53
+ @property
54
+ def sha256(self) -> Optional[str]:
55
+ """SHA-256 hex digest of the content (lazy; ``None`` on error)."""
56
+ return self._content_hexdigest(hashlib.sha256)
57
+
58
+ @property
59
+ def size(self) -> Optional[int]:
60
+ """Size of the content in bytes (lazy; ``None`` on error)."""
61
+ try:
62
+ return len(self.content_bytes)
63
+ except Exception as exp:
64
+ logger.error(f"Failed to determine size: {exp}")
65
+ return None
66
+
67
+ def update_checksum(self) -> Optional[str]:
68
+ """Store the SHA-256 of the content in the ``checksum`` metadata (``schema:sha256``).
69
+
70
+ :return: the stored SHA-256 hex digest (``None`` if the content is unavailable).
71
+ """
72
+ digest = self.sha256
73
+ self.metadata.set_attr("checksum", digest)
74
+ return digest
75
+
76
+ def verify(self) -> bool:
77
+ """Verify the content against the stored ``checksum`` (SHA-256) metadata.
78
+
79
+ :return: ``True`` iff a checksum is stored and matches the current content;
80
+ ``False`` on mismatch or when no checksum has been stored yet.
81
+ """
82
+ attr = self.metadata.get("checksum")
83
+ stored = attr.value if attr is not None else None
84
+ if not stored:
85
+ logger.warning("verify: no checksum stored (call update_checksum first)")
86
+ return False
87
+ return stored == self.sha256