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/semantic.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""JSON-LD-/Linked-Data-Serialisierung für sdata-Metadaten + Sidecar-Dateien.
|
|
3
|
+
|
|
4
|
+
Wandelt eine :class:`sdata.metadata.Metadata` in ein selbstbeschreibendes
|
|
5
|
+
JSON-LD-Dokument (und zurück) und schreibt/liest optionale Sidecar-Dateien
|
|
6
|
+
``<sname>.meta.jsonld`` neben einem Datenblob.
|
|
7
|
+
|
|
8
|
+
Modellierung (siehe Projektentscheidungen):
|
|
9
|
+
|
|
10
|
+
* ``@id`` = DID des Objekts ``did:suuid:<sname>:sdata``.
|
|
11
|
+
* ``@type`` = ``[sdata:<Klasse>, <BFO-IRI der Topologieklasse>]``.
|
|
12
|
+
* Reservierte ``_sdata_*``-Felder -> schema.org/DCAT/PROV-Terme (name,
|
|
13
|
+
identifier, generatedAtTime, wasDerivedFrom, isPartOf, …).
|
|
14
|
+
* **Hybrid** für User-Attribute: mit echter Einheit -> ``qudt:Quantity``-Knoten;
|
|
15
|
+
ohne Einheit aber mit ``ontology`` -> getypter Knoten; sonst einfaches
|
|
16
|
+
typisiertes Literal. ``ontology`` ist stets ein ``@type``/Klasse des Werts;
|
|
17
|
+
das Prädikat ist ``sdata:<name>``.
|
|
18
|
+
|
|
19
|
+
Pure-Python; keine Pflicht-Dependency.
|
|
20
|
+
"""
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
|
|
24
|
+
import sdata.dtypes as dtypes
|
|
25
|
+
from sdata import vocab, units
|
|
26
|
+
|
|
27
|
+
try: # optionales RDF-Backend ([rdf]=rdflib)
|
|
28
|
+
import rdflib as _rdflib
|
|
29
|
+
except ImportError: # pragma: no cover - abhängig vom Environment
|
|
30
|
+
_rdflib = None
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"to_jsonld", "from_jsonld", "to_rdf", "to_turtle", "rdf_from_doc",
|
|
34
|
+
"column_node", "write_sidecar", "read_sidecar",
|
|
35
|
+
"to_verifiable_credential", "verify_credential", "SIDECAR_SUFFIX",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
SIDECAR_SUFFIX = ".meta.jsonld"
|
|
39
|
+
|
|
40
|
+
# reserviertes Feld -> JSON-LD-Term (für Top-Level-Mapping & Rück-Parsing)
|
|
41
|
+
_RESERVED_TERMS = {
|
|
42
|
+
"_sdata_name": "name",
|
|
43
|
+
"_sdata_suuid": "identifier",
|
|
44
|
+
"_sdata_ctime": "generatedAtTime",
|
|
45
|
+
"_sdata_topology_class": "topologyClass",
|
|
46
|
+
"_sdata_version": "sdata:version",
|
|
47
|
+
"_sdata_unit_system": "sdata:unitSystem",
|
|
48
|
+
}
|
|
49
|
+
_TERMS_RESERVED = {v: k for k, v in _RESERVED_TERMS.items()}
|
|
50
|
+
|
|
51
|
+
# XSD-CURIE -> dtype (eindeutige Rückabbildung; list/json kommen über JSON-Typ)
|
|
52
|
+
_DTYPE_FROM_XSD = {
|
|
53
|
+
"xsd:string": "str", "xsd:integer": "int", "xsd:double": "float",
|
|
54
|
+
"xsd:boolean": "bool", "xsd:dateTime": "timestamp",
|
|
55
|
+
"xsd:base64Binary": "bytes", "xsd:anyURI": "uri",
|
|
56
|
+
"xsd:date": "date", "xsd:time": "time",
|
|
57
|
+
"xsd:duration": "duration", "xsd:decimal": "decimal",
|
|
58
|
+
"sdata:complex": "complex",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _did(sname):
|
|
63
|
+
return "did:suuid:{}:sdata".format(sname)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _sname_from_did(ref):
|
|
67
|
+
"""Extrahiere den sname aus ``did:suuid:<sname>:sdata`` (oder aus {'@id':…})."""
|
|
68
|
+
if isinstance(ref, dict):
|
|
69
|
+
ref = ref.get("@id", "")
|
|
70
|
+
parts = str(ref).split(":")
|
|
71
|
+
if len(parts) >= 4 and parts[0] == "did" and parts[1] == "suuid":
|
|
72
|
+
return parts[2]
|
|
73
|
+
return str(ref)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _class_local(class_spec):
|
|
77
|
+
"""``"sdata.sclass.dataframe:DataFrame"`` -> ``"DataFrame"``."""
|
|
78
|
+
return str(class_spec).split(":")[-1].split(".")[-1]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _value_literal(attr):
|
|
82
|
+
"""Typisiertes JSON-LD-Literal ``{"@value": …, "@type": xsd}`` für ein Attribut.
|
|
83
|
+
|
|
84
|
+
Ausnahme ``langstring``: rdf:langString wird über ``@language`` ausgedrückt
|
|
85
|
+
(nicht ``@type``).
|
|
86
|
+
"""
|
|
87
|
+
if attr.dtype == "langstring" and isinstance(attr.value, dtypes.LangString):
|
|
88
|
+
node = {"@value": attr.value.text}
|
|
89
|
+
if attr.value.lang:
|
|
90
|
+
node["@language"] = attr.value.lang
|
|
91
|
+
return node
|
|
92
|
+
spec = dtypes.get(attr.dtype) or dtypes.get("str")
|
|
93
|
+
return {"@value": spec.to_json(attr.value), "@type": vocab.xsd_for_dtype(attr.dtype)}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _attr_node(attr):
|
|
97
|
+
"""Hybrid-Repräsentation eines User-Attributs (Knoten oder Literal)."""
|
|
98
|
+
has_unit = attr.unit not in (None, "", "-")
|
|
99
|
+
types = []
|
|
100
|
+
if has_unit:
|
|
101
|
+
types.append("qudt:Quantity")
|
|
102
|
+
type_of = vocab.type_iri(attr.ontology)
|
|
103
|
+
if type_of:
|
|
104
|
+
types.append(type_of)
|
|
105
|
+
# ohne Einheit und ohne ontology -> einfaches Literal
|
|
106
|
+
if not types:
|
|
107
|
+
return _value_literal(attr)
|
|
108
|
+
node = {"@type": types if len(types) > 1 else types[0],
|
|
109
|
+
"name": attr.name,
|
|
110
|
+
"value": _value_literal(attr)}
|
|
111
|
+
if has_unit:
|
|
112
|
+
node.update(units.unit_node(attr.unit))
|
|
113
|
+
if attr.description:
|
|
114
|
+
node["description"] = attr.description
|
|
115
|
+
if attr.label:
|
|
116
|
+
node["label"] = attr.label
|
|
117
|
+
if attr.symbol:
|
|
118
|
+
node["quantitySymbol"] = attr.symbol
|
|
119
|
+
if attr.required:
|
|
120
|
+
node["required"] = bool(attr.required)
|
|
121
|
+
return node
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def column_node(attr):
|
|
125
|
+
"""JSON-LD-Knoten (CSVW) für eine Tabellenspalte aus ``column_metadata``.
|
|
126
|
+
|
|
127
|
+
``attr.name`` = Spaltenname, ``attr.value`` = pandas-dtype-Name (z.B.
|
|
128
|
+
``float64``); optional ``unit``/``label``/``description`` und ``ontology``
|
|
129
|
+
(Klasse der Spaltenwerte, als ``rdfType``).
|
|
130
|
+
"""
|
|
131
|
+
node = {"name": attr.name, "datatype": vocab.xsd_for_dtype(attr.value)}
|
|
132
|
+
if attr.unit not in (None, "", "-"):
|
|
133
|
+
node.update(units.unit_node(attr.unit))
|
|
134
|
+
rdf_type = vocab.type_iri(attr.ontology)
|
|
135
|
+
if rdf_type:
|
|
136
|
+
node["rdfType"] = rdf_type
|
|
137
|
+
if attr.label:
|
|
138
|
+
node["label"] = attr.label
|
|
139
|
+
if attr.symbol:
|
|
140
|
+
node["quantitySymbol"] = attr.symbol
|
|
141
|
+
if attr.description:
|
|
142
|
+
node["description"] = attr.description
|
|
143
|
+
return node
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def to_jsonld(metadata, context_mode="inline", columns=None):
|
|
147
|
+
"""Serialisiere ``metadata`` als JSON-LD-Dokument (dict).
|
|
148
|
+
|
|
149
|
+
:param columns: optionale, **geordnete** Iterable von Spalten-``Attribute``s
|
|
150
|
+
(DataFrame, in df-Spaltenreihenfolge); wird als ``csvw:column``-Liste ergänzt.
|
|
151
|
+
"""
|
|
152
|
+
doc = {"@context": vocab.build_context(mode=context_mode)}
|
|
153
|
+
|
|
154
|
+
sname = metadata.get("_sdata_sname")
|
|
155
|
+
if sname is not None and sname.value:
|
|
156
|
+
doc["@id"] = _did(sname.value)
|
|
157
|
+
|
|
158
|
+
types = []
|
|
159
|
+
cls = metadata.get("_sdata_class")
|
|
160
|
+
if cls is not None and cls.value:
|
|
161
|
+
types.append("sdata:" + _class_local(cls.value))
|
|
162
|
+
topo = metadata.get("_sdata_topology_class")
|
|
163
|
+
bfo = vocab.bfo_iri(topo.value) if topo is not None else None
|
|
164
|
+
if bfo:
|
|
165
|
+
types.append(bfo)
|
|
166
|
+
if types:
|
|
167
|
+
doc["@type"] = types if len(types) > 1 else types[0]
|
|
168
|
+
|
|
169
|
+
# bekannte reservierte Felder -> Terme
|
|
170
|
+
for key, term in _RESERVED_TERMS.items():
|
|
171
|
+
attr = metadata.get(key)
|
|
172
|
+
if attr is None or attr.value in (None, ""):
|
|
173
|
+
continue
|
|
174
|
+
doc[term] = attr.value
|
|
175
|
+
# Relationen als DID-Referenzen
|
|
176
|
+
parent = metadata.get("_sdata_parent_sname")
|
|
177
|
+
if parent is not None and parent.value:
|
|
178
|
+
doc["wasDerivedFrom"] = {"@id": _did(parent.value)}
|
|
179
|
+
project = metadata.get("_sdata_project_sname")
|
|
180
|
+
if project is not None and project.value:
|
|
181
|
+
doc["isPartOf"] = {"@id": _did(project.value)}
|
|
182
|
+
|
|
183
|
+
# User-Attribute -> sdata:<name>
|
|
184
|
+
for attr in metadata.user_attributes.values():
|
|
185
|
+
doc[vocab.predicate_for(attr.name)] = _attr_node(attr)
|
|
186
|
+
|
|
187
|
+
# tabellarische Spalten (DataFrame) als geordnete csvw:column-Liste
|
|
188
|
+
if columns:
|
|
189
|
+
doc["columns"] = [column_node(a) for a in columns]
|
|
190
|
+
return doc
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _set_from_node(metadata, name, node):
|
|
194
|
+
"""Rekonstruiere ein User-Attribut aus Knoten/Literal."""
|
|
195
|
+
unit = "-"
|
|
196
|
+
ontology = ""
|
|
197
|
+
lang = None
|
|
198
|
+
if isinstance(node, dict) and "@value" in node:
|
|
199
|
+
raw, xsd = node.get("@value"), node.get("@type")
|
|
200
|
+
lang = node.get("@language")
|
|
201
|
+
elif isinstance(node, dict):
|
|
202
|
+
literal = node.get("value", {})
|
|
203
|
+
raw = literal.get("@value") if isinstance(literal, dict) else literal
|
|
204
|
+
xsd = literal.get("@type") if isinstance(literal, dict) else None
|
|
205
|
+
unit = node.get("symbol", "-") or "-"
|
|
206
|
+
type_list = node.get("@type", [])
|
|
207
|
+
if isinstance(type_list, str):
|
|
208
|
+
type_list = [type_list]
|
|
209
|
+
ontology = next((t for t in type_list if t != "qudt:Quantity"), "")
|
|
210
|
+
else:
|
|
211
|
+
raw, xsd = node, None
|
|
212
|
+
# dtype bestimmen: JSON-Typ/@language haben Vorrang, sonst XSD-Rückabbildung
|
|
213
|
+
if isinstance(raw, list):
|
|
214
|
+
dtype = "floatlist" if xsd == "sdata:floatlist" else "list"
|
|
215
|
+
elif isinstance(raw, dict):
|
|
216
|
+
dtype = "json"
|
|
217
|
+
elif lang:
|
|
218
|
+
dtype = "langstring"
|
|
219
|
+
raw = "{}@{}".format(raw, lang) # text@lang rekonstruieren
|
|
220
|
+
else:
|
|
221
|
+
dtype = _DTYPE_FROM_XSD.get(xsd, "str")
|
|
222
|
+
metadata.set_attr(name, raw, dtype=dtype, unit=unit, ontology=ontology)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def from_jsonld(doc):
|
|
226
|
+
"""Rekonstruiere eine :class:`Metadata` aus einem JSON-LD-Dokument (dict oder str)."""
|
|
227
|
+
from sdata.metadata import Metadata
|
|
228
|
+
if isinstance(doc, str):
|
|
229
|
+
doc = json.loads(doc)
|
|
230
|
+
m = Metadata()
|
|
231
|
+
|
|
232
|
+
if "@id" in doc:
|
|
233
|
+
m.set_attr("_sdata_sname", _sname_from_did(doc["@id"]))
|
|
234
|
+
type_list = doc.get("@type", [])
|
|
235
|
+
if isinstance(type_list, str):
|
|
236
|
+
type_list = [type_list]
|
|
237
|
+
for t in type_list:
|
|
238
|
+
if isinstance(t, str) and t.startswith("sdata:"):
|
|
239
|
+
m.set_attr("_sdata_class", t.split(":", 1)[1])
|
|
240
|
+
for term, key in _TERMS_RESERVED.items():
|
|
241
|
+
if term in doc:
|
|
242
|
+
m.set_attr(key, doc[term])
|
|
243
|
+
if "wasDerivedFrom" in doc:
|
|
244
|
+
m.set_attr("_sdata_parent_sname", _sname_from_did(doc["wasDerivedFrom"]))
|
|
245
|
+
if "isPartOf" in doc:
|
|
246
|
+
m.set_attr("_sdata_project_sname", _sname_from_did(doc["isPartOf"]))
|
|
247
|
+
|
|
248
|
+
reserved_terms = set(_TERMS_RESERVED) | {
|
|
249
|
+
"@context", "@id", "@type", "wasDerivedFrom", "isPartOf"}
|
|
250
|
+
for key, val in doc.items():
|
|
251
|
+
if key in reserved_terms:
|
|
252
|
+
continue
|
|
253
|
+
if key.startswith("sdata:"):
|
|
254
|
+
_set_from_node(m, key.split(":", 1)[1], val)
|
|
255
|
+
return m
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def to_rdf(metadata, fmt="turtle"):
|
|
259
|
+
"""Serialisiere die Metadaten als RDF.
|
|
260
|
+
|
|
261
|
+
Mit installiertem ``rdflib`` (``[rdf]``) wird das JSON-LD nach ``fmt``
|
|
262
|
+
(turtle/nt/xml/…) serialisiert. Ohne rdflib wird das JSON-LD selbst
|
|
263
|
+
zurückgegeben – ``application/ld+json`` ist bereits gültiges RDF.
|
|
264
|
+
"""
|
|
265
|
+
return rdf_from_doc(to_jsonld(metadata), fmt=fmt)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def rdf_from_doc(doc, fmt="turtle"):
|
|
269
|
+
"""Serialisiere ein bereits gebautes JSON-LD-Dokument als RDF (siehe :func:`to_rdf`)."""
|
|
270
|
+
data = json.dumps(doc, default=dtypes.json_default)
|
|
271
|
+
if _rdflib is not None:
|
|
272
|
+
graph = _rdflib.Graph()
|
|
273
|
+
graph.parse(data=data, format="json-ld")
|
|
274
|
+
return graph.serialize(format=fmt)
|
|
275
|
+
return json.dumps(doc, indent=2, default=dtypes.json_default)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def to_turtle(metadata):
|
|
279
|
+
"""Convenience: RDF im Turtle-Format (siehe :func:`to_rdf`)."""
|
|
280
|
+
return to_rdf(metadata, fmt="turtle")
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def to_verifiable_credential(metadata, issuer_did, issuer_priv_jwk,
|
|
284
|
+
kid=None, extra_claims=None):
|
|
285
|
+
"""Signiere die Metadaten als W3C Verifiable Credential (Compact-JWS, EdDSA).
|
|
286
|
+
|
|
287
|
+
Wickelt :func:`to_jsonld` als ``credentialSubject`` und signiert über den
|
|
288
|
+
pure-Python-EdDSA-Stack (:mod:`sdata.did.jose`) – keine externe Krypto.
|
|
289
|
+
|
|
290
|
+
:return: Compact-JWS-String (``header.payload.signature``)
|
|
291
|
+
"""
|
|
292
|
+
from sdata.did import jose
|
|
293
|
+
subject = to_jsonld(metadata)
|
|
294
|
+
payload = {
|
|
295
|
+
"iss": issuer_did,
|
|
296
|
+
"vc": {
|
|
297
|
+
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
|
298
|
+
"type": ["VerifiableCredential", "SdataMetadataCredential"],
|
|
299
|
+
"credentialSubject": subject,
|
|
300
|
+
},
|
|
301
|
+
}
|
|
302
|
+
if subject.get("@id"):
|
|
303
|
+
payload["sub"] = subject["@id"]
|
|
304
|
+
if extra_claims:
|
|
305
|
+
payload.update(extra_claims)
|
|
306
|
+
return jose.sign_compact(payload, issuer_priv_jwk,
|
|
307
|
+
kid=kid or (issuer_did + "#key-1"), typ="vc+ld+jwt")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def verify_credential(vc_jws, pub_jwk):
|
|
311
|
+
"""Verifiziere ein VC (Compact-JWS) und gib das ``credentialSubject`` (JSON-LD) zurück.
|
|
312
|
+
|
|
313
|
+
:raises sdata.did.errors.VerificationError: bei ungültiger Signatur.
|
|
314
|
+
"""
|
|
315
|
+
from sdata.did import jose
|
|
316
|
+
payload = jose.verify_compact(vc_jws, pub_jwk)
|
|
317
|
+
return payload["vc"]["credentialSubject"]
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _sidecar_path(path, sname):
|
|
321
|
+
"""Ziel-Dateipfad: Verzeichnis -> ``<dir>/<sname>.meta.jsonld``; sonst ``path``."""
|
|
322
|
+
if path is None or os.path.isdir(path):
|
|
323
|
+
directory = path or "."
|
|
324
|
+
return os.path.join(directory, "{}{}".format(sname, SIDECAR_SUFFIX))
|
|
325
|
+
return path
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def write_sidecar(metadata, path=None, indent=2):
|
|
329
|
+
"""Schreibe ``<sname>.meta.jsonld`` (JSON-LD) neben einen Blob; gibt den Pfad zurück."""
|
|
330
|
+
sname_attr = metadata.get("_sdata_sname")
|
|
331
|
+
sname = sname_attr.value if sname_attr is not None and sname_attr.value else "metadata"
|
|
332
|
+
return write_sidecar_doc(to_jsonld(metadata), path, sname, indent=indent)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def write_sidecar_doc(doc, path, sname, indent=2):
|
|
336
|
+
"""Schreibe ein bereits gebautes JSON-LD-Dokument als ``<sname>.meta.jsonld``."""
|
|
337
|
+
target = _sidecar_path(path, sname)
|
|
338
|
+
with open(target, "w") as fh:
|
|
339
|
+
json.dump(doc, fh, indent=indent, default=dtypes.json_default)
|
|
340
|
+
return target
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def read_sidecar(path):
|
|
344
|
+
"""Lade eine ``.meta.jsonld``-Sidecar-Datei und rekonstruiere die Metadata."""
|
|
345
|
+
with open(path, "r") as fh:
|
|
346
|
+
return from_jsonld(json.load(fh))
|
sdata/suuid.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""sdata.suuid — Adapter auf das eigenständige ``suuid``-Package.
|
|
2
|
+
|
|
3
|
+
Ab jetzt ist **``suuid``** (``pip install "suuid>=0.2.0"``) die Single Source of
|
|
4
|
+
Truth für semantische, S3-sichere IDs der Form ``Class__safe_name__huuid``.
|
|
5
|
+
Dieses Modul ist ein dünner Rückwärtskompatibilitäts-Adapter: es bildet die
|
|
6
|
+
historisch in sdata genutzte SUUID-API auf das ``suuid``-Package ab.
|
|
7
|
+
|
|
8
|
+
Mapping (sdata → suuid):
|
|
9
|
+
``suuid_str`` / ``suuid_bytes`` → :attr:`suuid.SUUID.compact_token`
|
|
10
|
+
``uuid`` / ``get_uuid()`` → :meth:`suuid.SUUID.as_uuid`
|
|
11
|
+
``from_name(ns_name=…)`` → :meth:`suuid.SUUID.from_name` (``ns=…``)
|
|
12
|
+
``from_suuid_sname(strict=False)`` → :meth:`suuid.SUUID.from_sname`
|
|
13
|
+
``from_suuid_str`` / ``from_suuid_bytes`` → :meth:`suuid.SUUID.from_compact_token`
|
|
14
|
+
``generate_safe_filename`` → :func:`suuid.safe_name`
|
|
15
|
+
|
|
16
|
+
Damit gilt durchgängig die in ``suuid`` eingefrorene, 100% S3-sichere
|
|
17
|
+
Normalisierung (kein führendes ``_``, kein ``@``, ``sname`` ∈ ``[A-Za-z0-9_]``).
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import uuid as _uuid
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
from suuid import SUUID as _SUUID, OID_NAMESPACE, safe_name # noqa: F401
|
|
25
|
+
from suuid.core import clean_class_name
|
|
26
|
+
|
|
27
|
+
__all__ = ["SUUID"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SUUID(_SUUID):
|
|
31
|
+
"""sdata-Adapter um :class:`suuid.SUUID` mit Kompatibilitäts-Aliassen.
|
|
32
|
+
|
|
33
|
+
Erbt Immutabilität, Wertgleichheit und die S3-sichere Normalisierung von
|
|
34
|
+
:class:`suuid.SUUID`; ergänzt nur die in sdata genutzten Alias-Namen.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
#: Separator (Kompat-Konstante; identisch zu ``suuid`` ``SEP``).
|
|
38
|
+
SEP = "__"
|
|
39
|
+
|
|
40
|
+
# --- Property-Aliasse ------------------------------------------------
|
|
41
|
+
@property
|
|
42
|
+
def suuid_str(self) -> str:
|
|
43
|
+
"""Kompakter Base64-Token (Alias auf :attr:`compact_token`)."""
|
|
44
|
+
return self.compact_token
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def suuid_bytes(self) -> bytes:
|
|
48
|
+
""":attr:`suuid_str` als UTF-8-Bytes."""
|
|
49
|
+
return self.compact_token.encode()
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def uuid(self) -> _uuid.UUID:
|
|
53
|
+
"""Das :class:`uuid.UUID`-Objekt hinter ``huuid``."""
|
|
54
|
+
return self.as_uuid()
|
|
55
|
+
|
|
56
|
+
def get_uuid(self) -> _uuid.UUID:
|
|
57
|
+
"""Wie :attr:`uuid`."""
|
|
58
|
+
return self.as_uuid()
|
|
59
|
+
|
|
60
|
+
def to_list(self) -> list:
|
|
61
|
+
"""``[sname, suuid_str, class_name, name, huuid]`` (sdata-Kompat)."""
|
|
62
|
+
return [self.sname, self.suuid_str, self.class_name, self.name, self.huuid]
|
|
63
|
+
|
|
64
|
+
def to_dict(self) -> dict:
|
|
65
|
+
"""Wie :meth:`suuid.SUUID.to_dict`, plus sdata-Alias ``suuid``."""
|
|
66
|
+
d = super().to_dict()
|
|
67
|
+
d["suuid"] = self.suuid_str
|
|
68
|
+
return d
|
|
69
|
+
|
|
70
|
+
# --- Konstruktoren mit sdata-Signaturen ------------------------------
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_name(cls, class_name: str, name: str = "",
|
|
73
|
+
ns_name: Optional[str] = None) -> "SUUID":
|
|
74
|
+
"""Deterministische SUUID; ``ns_name`` (String) scoped die ID."""
|
|
75
|
+
ns = OID_NAMESPACE if not ns_name else ns_name
|
|
76
|
+
return super().from_name(class_name, name, ns=ns)
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_suuid_sname(cls, s, strict: bool = False):
|
|
80
|
+
"""Parse einen ``sname``; ``strict=False`` (Default) → ``None`` bei Fehler."""
|
|
81
|
+
return super().from_sname(s, strict=strict)
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def from_suuid_str(cls, s: str) -> "SUUID":
|
|
85
|
+
"""Aus :attr:`suuid_str`/compact-token rekonstruieren."""
|
|
86
|
+
return super().from_compact_token(s)
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def from_suuid_bytes(cls, b) -> "SUUID":
|
|
90
|
+
"""Aus :attr:`suuid_bytes` rekonstruieren."""
|
|
91
|
+
token = b.decode() if isinstance(b, (bytes, bytearray)) else b
|
|
92
|
+
return super().from_compact_token(token)
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def from_file(cls, class_name: str, filepath, ns_name: Optional[str] = None) -> "SUUID":
|
|
96
|
+
"""Content-adressierte SUUID aus Dateiinhalt (``name`` = Basename)."""
|
|
97
|
+
ns = OID_NAMESPACE if not ns_name else ns_name
|
|
98
|
+
return super().from_file(class_name, filepath, ns=ns)
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def from_str(cls, class_name: str, s: str, ns_name: Optional[str] = None) -> "SUUID":
|
|
102
|
+
"""Content-adressierte SUUID aus einem String."""
|
|
103
|
+
ns = OID_NAMESPACE if not ns_name else ns_name
|
|
104
|
+
return super().from_content(class_name, "", s.encode("utf-8"), ns=ns)
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def from_uuid(cls, class_name: str, uuid_obj: _uuid.UUID) -> "SUUID":
|
|
108
|
+
"""SUUID aus ``class_name`` und einem :class:`uuid.UUID` (ohne Name)."""
|
|
109
|
+
return cls(class_name=clean_class_name(class_name), name="", huuid=uuid_obj.hex)
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def from_obj(cls, obj, class_name: Optional[str] = None):
|
|
113
|
+
"""Koerziere ein Base-artiges Objekt, einen ``sname`` oder String zu SUUID."""
|
|
114
|
+
if obj is None:
|
|
115
|
+
return None
|
|
116
|
+
if hasattr(obj, "sname"):
|
|
117
|
+
return cls.from_suuid_sname(obj.sname, strict=False)
|
|
118
|
+
if isinstance(obj, str):
|
|
119
|
+
parsed = cls.from_suuid_sname(obj, strict=False)
|
|
120
|
+
return parsed if parsed is not None else cls.from_name(class_name or cls.__name__, obj)
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
# --- Statics ---------------------------------------------------------
|
|
124
|
+
@staticmethod
|
|
125
|
+
def generate_safe_filename(name: str) -> str:
|
|
126
|
+
"""100% S3-sichere Normalisierung (Alias auf :func:`suuid.safe_name`)."""
|
|
127
|
+
return safe_name(name)
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def is_valid_suuid_str(s: str) -> bool:
|
|
131
|
+
"""``True``, wenn ``s`` ein gültiger :attr:`suuid_str` ist."""
|
|
132
|
+
try:
|
|
133
|
+
_SUUID.from_compact_token(s)
|
|
134
|
+
return True
|
|
135
|
+
except Exception:
|
|
136
|
+
return False
|