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/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)))
|