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/dtypes.py ADDED
@@ -0,0 +1,510 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Reine-stdlib dtype-Registry für :class:`sdata.metadata.Attribute`.
3
+
4
+ Single Source of Truth für Wert-Coercion, JSON-(De)Serialisierung, die zu einem
5
+ dtype gehörige Python-Klasse und das XSD-Typ-Mapping (das die JSON-LD-Schicht
6
+ konsumiert).
7
+
8
+ Designziele:
9
+
10
+ * **Rückwärtskompatibel** – im *lenienten* Default-Modus (``strict=False``)
11
+ verhält sich die Coercion byte-genau wie der bisherige
12
+ ``Attribute._set_value``: leere/falsy Werte werden je dtype zu ``np.nan`` /
13
+ ``None`` / ``""`` / ``False``; nicht-castbare Werte lösen ``DtypeError`` aus
14
+ (vom Setter geloggt, Wert bleibt unverändert).
15
+ * **Strikt opt-in** – ``strict=True`` wirft ``DtypeError`` statt still zu
16
+ degradieren.
17
+ * **Erweiterbar** – neben den 6 Alt-dtypes (str/int/float/bool/timestamp/list)
18
+ zusätzlich ``bytes`` (base64), ``json`` (dict/list), ``uri``, ``date``, ``time``,
19
+ ``duration`` (ISO 8601 / ``timedelta``), ``decimal`` (exakt), ``complex``,
20
+ ``floatlist`` (typisierte Float-Liste) sowie ``langstring`` (sprach-getaggt,
21
+ ``rdf:langString``). ``complex``/``floatlist`` haben keinen Standard-XSD-Typ und
22
+ nutzen eigene Datentyp-CURIEs (``sdata:complex`` / ``sdata:floatlist``);
23
+ ``langstring`` wird in JSON-LD über ``@language`` ausgedrückt.
24
+ """
25
+ import base64
26
+ import binascii
27
+ import datetime
28
+ import json as _json
29
+ import re
30
+ from decimal import Decimal, InvalidOperation
31
+ from typing import Any, Callable, Dict, List, Optional
32
+ from urllib.parse import urlsplit
33
+
34
+ import numpy as np
35
+ import pandas as pd
36
+
37
+ from sdata.timestamp import TimeStamp
38
+
39
+ __all__ = [
40
+ "DtypeError", "DtypeSpec", "LangString", "register", "get", "names", "resolve",
41
+ "coerce", "xsd_map", "json_default", "XSD", "DTYPES", "DTYPES_INV",
42
+ ]
43
+
44
+
45
+ class DtypeError(ValueError):
46
+ """Wert kann nicht in den Ziel-dtype überführt werden (v.a. im strict-Modus)."""
47
+
48
+
49
+ class LangString:
50
+ """Ein sprach-getaggter String (``rdf:langString``): ``text`` + BCP-47 ``lang``.
51
+
52
+ In JSON-LD als ``{"@value": text, "@language": lang}`` repräsentiert (nicht über
53
+ ``@type``). Die kompakte Textform ist ``"text@lang"`` (z. B. ``"Hallo@de"``).
54
+ """
55
+
56
+ __slots__ = ("text", "lang")
57
+
58
+ def __init__(self, text: Any, lang: str = "") -> None:
59
+ self.text = str(text)
60
+ self.lang = str(lang or "")
61
+
62
+ def __eq__(self, other: Any) -> bool:
63
+ return (isinstance(other, LangString)
64
+ and self.text == other.text and self.lang == other.lang)
65
+
66
+ def __hash__(self) -> int:
67
+ return hash((self.text, self.lang))
68
+
69
+ def __str__(self) -> str:
70
+ return "{}@{}".format(self.text, self.lang) if self.lang else self.text
71
+
72
+ def __repr__(self) -> str:
73
+ return "LangString({!r}, {!r})".format(self.text, self.lang)
74
+
75
+
76
+ def _isna(value):
77
+ """``pd.isna`` skalar; Array-Eingaben (Liste o.ä.) gelten als nicht-NA."""
78
+ result = pd.isna(value)
79
+ return bool(result) if isinstance(result, (bool, np.bool_)) else False
80
+
81
+
82
+ # --- Coercion-Funktionen (value, strict) -> python value --------------------
83
+ def _c_str(value, strict):
84
+ if value == "":
85
+ return ""
86
+ if not value:
87
+ return None
88
+ return str(value)
89
+
90
+
91
+ def _is_empty(value):
92
+ """None, leerer/Whitespace-String oder NA-artig -> kein Messwert."""
93
+ if value is None or (isinstance(value, str) and not value.strip()):
94
+ return True
95
+ return _isna(value)
96
+
97
+
98
+ # Hinweis: Eine numerische Null ist ein gueltiger Messwert (z. B. 0 Grad zur
99
+ # Walzrichtung) und bleibt erhalten. bool: False -> 0, True -> 1 (wie int(False)).
100
+ def _c_int(value, strict):
101
+ if _is_empty(value):
102
+ return np.nan
103
+ try:
104
+ return int(value)
105
+ except (ValueError, TypeError) as exp:
106
+ raise DtypeError("int: {!r}".format(value)) from exp
107
+
108
+
109
+ def _c_float(value, strict):
110
+ if _is_empty(value):
111
+ return np.nan
112
+ try:
113
+ return float(value)
114
+ except (ValueError, TypeError) as exp:
115
+ raise DtypeError("float: {!r}".format(value)) from exp
116
+
117
+
118
+ # leniente bool-Abbildung = exakt das bisherige Verhalten
119
+ _LEGACY_BOOL_TRUE = [1, "1", "true", "True"]
120
+ # strenge, case-insensitive Erweiterung
121
+ _STRICT_TRUE = {"1", "true", "yes", "y", "on", "t", "wahr"}
122
+ _STRICT_FALSE = {"0", "false", "no", "n", "off", "f", "falsch", ""}
123
+
124
+
125
+ def _c_bool(value, strict):
126
+ if isinstance(value, bool):
127
+ return value
128
+ if not strict:
129
+ # Altverhalten: True genau dann, wenn value in {1,"1","true","True"}
130
+ return value in _LEGACY_BOOL_TRUE
131
+ if isinstance(value, (int, float)) and value in (0, 1):
132
+ return bool(value)
133
+ token = str(value).strip().lower()
134
+ if token in _STRICT_TRUE:
135
+ return True
136
+ if token in _STRICT_FALSE:
137
+ return False
138
+ raise DtypeError("bool: {!r}".format(value))
139
+
140
+
141
+ def _c_timestamp(value, strict):
142
+ if value is None or value == "":
143
+ return None
144
+ if isinstance(value, TimeStamp):
145
+ return value
146
+ if isinstance(value, datetime.datetime):
147
+ return TimeStamp(value.isoformat())
148
+ try:
149
+ return TimeStamp(str(value))
150
+ except Exception as exp: # ParseError u.ä.
151
+ raise DtypeError("timestamp: {!r}".format(value)) from exp
152
+
153
+
154
+ def _c_list(value, strict):
155
+ if value is None or value == "":
156
+ return []
157
+ if isinstance(value, str):
158
+ return [v.strip() for v in value.split(",") if v.strip()]
159
+ if isinstance(value, (list, tuple)):
160
+ return [str(v) for v in value]
161
+ raise DtypeError("list[str]: {!r}".format(value))
162
+
163
+
164
+ def _c_bytes(value, strict):
165
+ if value is None:
166
+ return None
167
+ if isinstance(value, bytes):
168
+ return value
169
+ if isinstance(value, (bytearray, memoryview)):
170
+ return bytes(value)
171
+ if isinstance(value, str):
172
+ try:
173
+ return base64.b64decode(value, validate=True)
174
+ except (binascii.Error, ValueError):
175
+ return value.encode("utf-8")
176
+ raise DtypeError("bytes: {!r}".format(value))
177
+
178
+
179
+ def _c_json(value, strict):
180
+ if value is None:
181
+ return None
182
+ if isinstance(value, (dict, list)):
183
+ return value
184
+ if isinstance(value, str):
185
+ try:
186
+ return _json.loads(value)
187
+ except (ValueError, TypeError) as exp:
188
+ raise DtypeError("json: {!r}".format(value)) from exp
189
+ raise DtypeError("json: {!r}".format(value))
190
+
191
+
192
+ def _c_uri(value, strict):
193
+ if value is None or value == "":
194
+ return None if value is None else ""
195
+ text = str(value).strip()
196
+ if strict and not (urlsplit(text).scheme or urlsplit(text).path):
197
+ raise DtypeError("uri: {!r}".format(value))
198
+ return text
199
+
200
+
201
+ def _c_date(value, strict):
202
+ if value is None or value == "":
203
+ return None
204
+ if isinstance(value, datetime.datetime): # vor date prüfen (Subklasse!)
205
+ return value.date()
206
+ if isinstance(value, datetime.date):
207
+ return value
208
+ try:
209
+ return datetime.date.fromisoformat(str(value))
210
+ except (ValueError, TypeError) as exp:
211
+ raise DtypeError("date: {!r} (ISO 8601 'YYYY-MM-DD')".format(value)) from exp
212
+
213
+
214
+ def _c_time(value, strict):
215
+ if value is None or value == "":
216
+ return None
217
+ if isinstance(value, datetime.datetime):
218
+ return value.timetz()
219
+ if isinstance(value, datetime.time):
220
+ return value
221
+ try:
222
+ return datetime.time.fromisoformat(str(value))
223
+ except (ValueError, TypeError) as exp:
224
+ raise DtypeError("time: {!r} (ISO 8601 'HH:MM:SS')".format(value)) from exp
225
+
226
+
227
+ #: ISO-8601-Dauer ohne Jahre/Monate (nicht als ``timedelta`` darstellbar).
228
+ _ISO_DURATION = re.compile(
229
+ r"^(?P<sign>-?)P"
230
+ r"(?:(?P<weeks>\d+(?:\.\d+)?)W)?"
231
+ r"(?:(?P<days>\d+(?:\.\d+)?)D)?"
232
+ r"(?:T"
233
+ r"(?:(?P<hours>\d+(?:\.\d+)?)H)?"
234
+ r"(?:(?P<minutes>\d+(?:\.\d+)?)M)?"
235
+ r"(?:(?P<seconds>\d+(?:\.\d+)?)S)?"
236
+ r")?$"
237
+ )
238
+
239
+
240
+ def _c_duration(value, strict):
241
+ if value is None or value == "":
242
+ return None
243
+ if isinstance(value, datetime.timedelta):
244
+ return value
245
+ if isinstance(value, bool): # bool ist int-Subklasse → ablehnen
246
+ raise DtypeError("duration: {!r}".format(value))
247
+ if isinstance(value, (int, float)):
248
+ return datetime.timedelta(seconds=value) # Zahl = Sekunden
249
+ match = _ISO_DURATION.match(str(value).strip())
250
+ parts = ({k: float(v) for k, v in match.groupdict().items()
251
+ if v is not None and k != "sign"} if match else None)
252
+ if not parts:
253
+ raise DtypeError("duration: {!r} (ISO 8601 like 'PT1H30M'; years/months "
254
+ "are not representable as a timedelta)".format(value))
255
+ td = datetime.timedelta(
256
+ weeks=parts.get("weeks", 0), days=parts.get("days", 0),
257
+ hours=parts.get("hours", 0), minutes=parts.get("minutes", 0),
258
+ seconds=parts.get("seconds", 0))
259
+ return -td if match.group("sign") == "-" else td
260
+
261
+
262
+ def _c_decimal(value, strict):
263
+ if value is None or value == "":
264
+ return None
265
+ if isinstance(value, Decimal):
266
+ return value
267
+ if isinstance(value, bool): # bool ist kein Dezimalwert
268
+ raise DtypeError("decimal: {!r}".format(value))
269
+ try:
270
+ return Decimal(str(value)) # via str: keine Float-Binär-Artefakte
271
+ except (InvalidOperation, ValueError, TypeError) as exp:
272
+ raise DtypeError("decimal: {!r}".format(value)) from exp
273
+
274
+
275
+ def _c_complex(value, strict):
276
+ if value is None or value == "":
277
+ return None
278
+ if isinstance(value, complex):
279
+ return value
280
+ if isinstance(value, bool): # bool ist kein komplexer Wert
281
+ raise DtypeError("complex: {!r}".format(value))
282
+ try:
283
+ return complex(value.strip()) if isinstance(value, str) else complex(value)
284
+ except (ValueError, TypeError) as exp:
285
+ raise DtypeError("complex: {!r}".format(value)) from exp
286
+
287
+
288
+ def _c_floatlist(value, strict):
289
+ if value is None:
290
+ return []
291
+ if isinstance(value, str): # "" / "1,2,3" (Leer-Check skalar-sicher)
292
+ items = [s for s in (p.strip() for p in value.split(",")) if s]
293
+ elif isinstance(value, (list, tuple)):
294
+ items = value
295
+ elif hasattr(value, "tolist"): # numpy-Array & Co. -> Liste
296
+ items = value.tolist()
297
+ else:
298
+ raise DtypeError("floatlist: {!r}".format(value))
299
+ try:
300
+ return [float(x) for x in items]
301
+ except (ValueError, TypeError) as exp:
302
+ raise DtypeError("floatlist: {!r}".format(value)) from exp
303
+
304
+
305
+ #: Sprach-Tag am Stringende (BCP-47-artig) zum Zerlegen von ``"text@lang"``.
306
+ _LANG_TAG = re.compile(r"^(.*)@([A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*)$", re.DOTALL)
307
+
308
+
309
+ def _c_langstring(value, strict):
310
+ if value is None:
311
+ return None
312
+ if isinstance(value, LangString):
313
+ return value
314
+ if isinstance(value, (tuple, list)) and len(value) == 2:
315
+ return LangString(value[0], value[1]) # (text, lang) explizit & eindeutig
316
+ if isinstance(value, dict):
317
+ return LangString(value.get("@value", value.get("text", "")),
318
+ value.get("@language", value.get("lang", "")))
319
+ text = str(value)
320
+ if text == "":
321
+ return None
322
+ match = _LANG_TAG.match(text) # "text@lang" -> (text, lang)
323
+ return LangString(match.group(1), match.group(2)) if match else LangString(text, "")
324
+
325
+
326
+ # --- JSON-Serialisierung je dtype -------------------------------------------
327
+ def _ts_to_json(value):
328
+ return str(value.utc) if isinstance(value, TimeStamp) else value
329
+
330
+
331
+ def _bytes_to_json(value):
332
+ return base64.b64encode(value).decode("ascii") if isinstance(value, bytes) else value
333
+
334
+
335
+ def _date_to_json(value):
336
+ return value.isoformat() if isinstance(value, datetime.date) else value
337
+
338
+
339
+ def _time_to_json(value):
340
+ return value.isoformat() if isinstance(value, datetime.time) else value
341
+
342
+
343
+ def _duration_to_json(value):
344
+ """``timedelta`` → kanonische ISO-8601-Dauer (``PT0S`` für null)."""
345
+ if not isinstance(value, datetime.timedelta):
346
+ return value
347
+ total = value.total_seconds()
348
+ sign = "-" if total < 0 else ""
349
+ total = abs(total)
350
+ days = int(total // 86400)
351
+ rem = total - days * 86400
352
+ hours = int(rem // 3600)
353
+ rem -= hours * 3600
354
+ minutes = int(rem // 60)
355
+ seconds = rem - minutes * 60
356
+ date_part = "{:d}D".format(days) if days else ""
357
+ time_part = ""
358
+ if hours:
359
+ time_part += "{:d}H".format(hours)
360
+ if minutes:
361
+ time_part += "{:d}M".format(minutes)
362
+ if seconds or not (date_part or time_part):
363
+ time_part += "{:g}S".format(seconds)
364
+ return sign + "P" + date_part + ("T" + time_part if time_part else "")
365
+
366
+
367
+ def _decimal_to_json(value):
368
+ return str(value) if isinstance(value, Decimal) else value
369
+
370
+
371
+ def _complex_to_json(value):
372
+ return str(value) if isinstance(value, complex) else value
373
+
374
+
375
+ def _langstring_to_json(value):
376
+ return str(value) if isinstance(value, LangString) else value
377
+
378
+
379
+ class DtypeSpec:
380
+ """Beschreibt einen dtype: Coercion, JSON-Repräsentation, Klasse, XSD-Typ."""
381
+
382
+ def __init__(self, name: str, pytype: type,
383
+ coerce: Callable[[Any, bool], Any], xsd: str,
384
+ to_json: Optional[Callable[[Any], Any]] = None) -> None:
385
+ self.name = name
386
+ self.pytype = pytype
387
+ self._coerce = coerce
388
+ self.xsd = xsd
389
+ self._to_json = to_json or (lambda v: v)
390
+
391
+ def coerce(self, value: Any, strict: bool = False) -> Any:
392
+ return self._coerce(value, strict)
393
+
394
+ def to_json(self, value: Any) -> Any:
395
+ return self._to_json(value)
396
+
397
+
398
+ _REGISTRY: Dict[str, "DtypeSpec"] = {}
399
+
400
+
401
+ def register(spec: "DtypeSpec") -> None:
402
+ _REGISTRY[spec.name] = spec
403
+
404
+
405
+ for _spec in [
406
+ DtypeSpec("str", str, _c_str, "xsd:string"),
407
+ DtypeSpec("int", int, _c_int, "xsd:integer"),
408
+ DtypeSpec("float", float, _c_float, "xsd:double"),
409
+ DtypeSpec("bool", bool, _c_bool, "xsd:boolean"),
410
+ DtypeSpec("timestamp", datetime.datetime, _c_timestamp, "xsd:dateTime", _ts_to_json),
411
+ DtypeSpec("list", list, _c_list, "xsd:string"),
412
+ DtypeSpec("bytes", bytes, _c_bytes, "xsd:base64Binary", _bytes_to_json),
413
+ DtypeSpec("json", dict, _c_json, "xsd:string"),
414
+ DtypeSpec("uri", str, _c_uri, "xsd:anyURI"),
415
+ DtypeSpec("date", datetime.date, _c_date, "xsd:date", _date_to_json),
416
+ DtypeSpec("time", datetime.time, _c_time, "xsd:time", _time_to_json),
417
+ DtypeSpec("duration", datetime.timedelta, _c_duration, "xsd:duration", _duration_to_json),
418
+ DtypeSpec("decimal", Decimal, _c_decimal, "xsd:decimal", _decimal_to_json),
419
+ # komplexe Zahlen & typisierte Float-Listen haben keinen Standard-XSD-Typ
420
+ # -> eigener Datentyp-CURIE in der sdata-Namespace (verlustfreier JSON-LD-Roundtrip).
421
+ DtypeSpec("complex", complex, _c_complex, "sdata:complex", _complex_to_json),
422
+ DtypeSpec("floatlist", list, _c_floatlist, "sdata:floatlist"),
423
+ # sprach-getaggter String: JSON-LD nutzt @language (nicht @type), siehe semantic.py
424
+ DtypeSpec("langstring", LangString, _c_langstring, "rdf:langString", _langstring_to_json),
425
+ ]:
426
+ register(_spec)
427
+
428
+
429
+ def get(name: str) -> Optional["DtypeSpec"]:
430
+ """DtypeSpec zum kanonischen Namen oder ``None``."""
431
+ return _REGISTRY.get(name)
432
+
433
+
434
+ def names() -> List[str]:
435
+ """Liste aller kanonischen dtype-Namen."""
436
+ return list(_REGISTRY.keys())
437
+
438
+
439
+ # Kompat-Aliasse (entsprechen exakt den bisherigen Attribute.DTYPES/_INV
440
+ # für die 6 Alt-dtypes; neue dtypes hängen hinten an, str bleibt -> 'str').
441
+ DTYPES = {name: spec.pytype for name, spec in _REGISTRY.items()}
442
+ DTYPES_INV = {
443
+ float: "float", int: "int", str: "str",
444
+ datetime.datetime: "timestamp", bool: "bool", list: "list",
445
+ bytes: "bytes", dict: "json",
446
+ datetime.date: "date", datetime.time: "time",
447
+ datetime.timedelta: "duration", Decimal: "decimal",
448
+ complex: "complex", LangString: "langstring",
449
+ }
450
+ XSD = {name: spec.xsd for name, spec in _REGISTRY.items()}
451
+
452
+
453
+ def xsd_map() -> Dict[str, str]:
454
+ """Kopie der ``{dtype_name: xsd_iri}``-Tabelle (für die JSON-LD-Schicht)."""
455
+ return dict(XSD)
456
+
457
+
458
+ def resolve(dtype: Any) -> Optional[str]:
459
+ """Normalisiere einen dtype-Input (String ODER Klasse) auf einen Registry-Key.
460
+
461
+ Spiegelt die bisherigen ``_set_dtype``-Regeln: Klassen via ``DTYPES_INV``,
462
+ ``'float64'``/``'int32'`` -> ``'float'``/``'int'``, Unbekanntes -> ``'str'``.
463
+ ``None`` -> ``None`` (kein dtype-Wechsel).
464
+ """
465
+ if dtype is None:
466
+ return None
467
+ if isinstance(dtype, type):
468
+ return DTYPES_INV.get(dtype, "str")
469
+ token = str(dtype).strip().lower()
470
+ if token in _REGISTRY:
471
+ return token
472
+ if token in ("list[float]", "float[]"): # Alias -> floatlist (vor 'list'/'float')
473
+ return "floatlist"
474
+ if "float" in token:
475
+ return "float"
476
+ if "int" in token:
477
+ return "int"
478
+ if "bool" in token:
479
+ return "bool"
480
+ if "list" in token:
481
+ return "list"
482
+ return "str"
483
+
484
+
485
+ def coerce(value: Any, dtype: Any, strict: bool = False) -> Any:
486
+ """Überführe ``value`` in ``dtype`` (String oder Klasse)."""
487
+ spec = _REGISTRY[resolve(dtype) or "str"]
488
+ return spec.coerce(value, strict=strict)
489
+
490
+
491
+ def json_default(obj: Any) -> Any:
492
+ """``default=`` für ``json.dumps``: serialisiert die nicht-nativen dtype-Werte
493
+ (TimeStamp/bytes/Decimal/timedelta/date/time) JSON-sicher."""
494
+ if isinstance(obj, TimeStamp):
495
+ return str(obj.utc)
496
+ if isinstance(obj, (bytes, bytearray)):
497
+ return base64.b64encode(bytes(obj)).decode("ascii")
498
+ if isinstance(obj, Decimal):
499
+ return str(obj)
500
+ if isinstance(obj, complex):
501
+ return str(obj)
502
+ if isinstance(obj, LangString):
503
+ return str(obj)
504
+ if isinstance(obj, datetime.timedelta):
505
+ return _duration_to_json(obj)
506
+ if isinstance(obj, datetime.date): # fängt auch datetime.datetime
507
+ return obj.isoformat()
508
+ if isinstance(obj, datetime.time):
509
+ return obj.isoformat()
510
+ raise TypeError("not JSON serializable: {!r}".format(type(obj)))