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/did/__init__.py ADDED
@@ -0,0 +1,129 @@
1
+ # -*- coding: utf-8 -*-
2
+ """``sdata.did`` – Decentralized Identifiers (DID) & Verifiable Credentials (VC).
3
+
4
+ Dieses Subpackage bündelt eine schlanke, **pure-Python** Umsetzung gängiger
5
+ SSI-Bausteine (Self-Sovereign Identity) rund um Ed25519:
6
+
7
+ ============================ ====================================================
8
+ Bereich Modul / Funktionen
9
+ ============================ ====================================================
10
+ Schlüssel (Ed25519/JWK) :mod:`~sdata.did.keys` – :func:`gen_ed25519_jwk`, :func:`pub_from_priv_jwk`
11
+ JWK / did:key / Thumbprint :mod:`~sdata.did.utils_didkey`
12
+ JOSE / Compact-JWS (EdDSA) :mod:`~sdata.did.jose` – :func:`sign_compact`, :func:`verify_compact`
13
+ VC / VP ausstellen :mod:`~sdata.did.issue_vc` – :func:`issue_vc`, :func:`make_vp`
14
+ VC / VP verifizieren :mod:`~sdata.did.verify_vp` – :func:`verify_presentation`
15
+ did:web :mod:`~sdata.did.did_web` – :func:`build_did_document`, :func:`resolve_did_web`
16
+ did:key :mod:`~sdata.did.utils_didkey` – :func:`did_key_from_jwk_ed25519`, :func:`pub_jwk_from_did_key`
17
+ did:peer:4 :mod:`~sdata.did.did_peer4` – :func:`did_peer4_from_payload`, :func:`resolve_long_form`
18
+ did:github :mod:`~sdata.did.did_github` – :func:`resolve_did_github`
19
+ ============================ ====================================================
20
+
21
+ Unterstützte Standards: W3C DID Core, did:web, did:key, did:peer:4,
22
+ W3C Verifiable Credentials (JWT-VC), JOSE/JWS (RFC 7515), EdDSA (RFC 8037),
23
+ JWK-Thumbprint (RFC 7638).
24
+
25
+ Installation
26
+ ------------
27
+ Krypto (Ed25519) und base58btc sind **abhängigkeitsfrei** (reine
28
+ Standardbibliothek) – siehe :mod:`~sdata.did.eddsa` und
29
+ :mod:`~sdata.did.base58btc`. Auch die HTTP-Auflösung von ``did:web`` /
30
+ ``did:github`` läuft ohne externe Abhängigkeit: :mod:`~sdata.did._http` nutzt
31
+ ``requests`` falls installiert, sonst ``urllib`` (Standardbibliothek)::
32
+
33
+ pip install "sdata" # voll funktionsfähig (urllib-HTTP-Backend)
34
+ pip install "sdata[http]" # optional: 'requests' als HTTP-Backend
35
+
36
+ .. warning::
37
+ **Sicherheit / SOTA-Hinweis.** Der produktive Krypto-Pfad nutzt die
38
+ pure-Python-Ed25519-Implementierung in :mod:`sdata.did.ed25519` (über den
39
+ Adapter :mod:`sdata.did.eddsa`). Sie ist abhängigkeitsfrei, aber **nicht
40
+ garantiert constant-time** und damit potenziell anfällig für
41
+ Timing-Seitenkanäle – genau wie das zuvor genutzte ``python-ecdsa``
42
+ (ebenfalls pure Python). Für hochsensible Umgebungen ein libsodium-Backend
43
+ (PyNaCl) oder ``cryptography`` (OpenSSL) erwägen. Korrektheit ist gegen die
44
+ offiziellen RFC-8032-Testvektoren abgesichert.
45
+
46
+ Beispiel
47
+ --------
48
+ ::
49
+
50
+ from sdata.did import (gen_ed25519_jwk, pub_from_priv_jwk,
51
+ did_key_from_jwk_ed25519, sign_compact, verify_compact)
52
+
53
+ jwk = gen_ed25519_jwk()
54
+ did = did_key_from_jwk_ed25519(pub_from_priv_jwk(jwk))
55
+ kid = f"{did}#{did.split(':', 2)[2]}"
56
+ jws = sign_compact({"hello": "world"}, jwk, kid=kid, typ="JWT")
57
+ assert verify_compact(jws, pub_from_priv_jwk(jwk))["hello"] == "world"
58
+ """
59
+ from __future__ import annotations
60
+
61
+ import importlib
62
+
63
+ # Lazy-Loading (PEP 562): Submodule werden erst beim Zugriff importiert. Vorteile:
64
+ # * ``python -m sdata.did.<modul>`` löst keine runpy-Doppelimport-Warnung aus,
65
+ # * ``import sdata.did`` ist leichtgewichtig – schwerere Submodule (z. B.
66
+ # ``did_web`` mit dem HTTP-Backend) werden erst geladen, wenn eine sie
67
+ # nutzende Funktion tatsächlich angefasst wird.
68
+ # Mapping: exportierter Name -> "submodul" oder ("submodul", "originalname").
69
+ _EXPORTS = {
70
+ # Fehler
71
+ "DidError": "errors", "EncodingError": "errors",
72
+ "ResolutionError": "errors", "VerificationError": "errors",
73
+ # Schlüssel / JWK / did:key
74
+ "gen_ed25519_jwk": "keys", "pub_from_priv_jwk": "keys",
75
+ "b64url": "utils_didkey", "b64url_decode": "utils_didkey",
76
+ "jwk_thumbprint_rfc7638": "utils_didkey",
77
+ "did_key_from_jwk_ed25519": "utils_didkey",
78
+ "did_key_fragment_from_jwk": "utils_didkey",
79
+ "pub_jwk_from_did_key": "utils_didkey",
80
+ "kid_for_did_web": "utils_didkey", "kid_for_did_key": "utils_didkey",
81
+ # JOSE / JWS
82
+ "sign_compact": "jose", "verify_compact": "jose", "decode_header": "jose",
83
+ # VC / VP
84
+ "issue_vc": "issue_vc", "make_vp": "issue_vc", "sign_compact_jws": "issue_vc",
85
+ "verify_presentation": "verify_vp", "jws_header": "verify_vp",
86
+ "jws_verify": "verify_vp", "resolve_public_jwk_for_kid": "verify_vp",
87
+ # DID-Methoden
88
+ "build_did_document": "did_web", "did_web_to_url": "did_web",
89
+ "resolve_did_web": ("did_web", "resolve_public_jwk_for_kid"),
90
+ "did_peer4_from_payload": "did_peer4", "resolve_long_form": "did_peer4",
91
+ "resolve_did_github": "did_github", "parse_did_github": "did_github",
92
+ }
93
+
94
+
95
+ def __getattr__(name):
96
+ """Importiere das zugehörige Submodul beim ersten Zugriff (PEP 562)."""
97
+ target = _EXPORTS.get(name)
98
+ if target is None:
99
+ raise AttributeError(
100
+ "module {!r} has no attribute {!r}".format(__name__, name))
101
+ modname, attrname = target if isinstance(target, tuple) else (target, name)
102
+ mod = importlib.import_module("." + modname, __name__)
103
+ value = getattr(mod, attrname)
104
+ globals()[name] = value # cachen, damit __getattr__ nur einmal feuert
105
+ return value
106
+
107
+
108
+ def __dir__():
109
+ return sorted(__all__)
110
+
111
+
112
+ __all__ = [
113
+ # Fehler
114
+ "DidError", "EncodingError", "ResolutionError", "VerificationError",
115
+ # Schlüssel / JWK / did:key
116
+ "gen_ed25519_jwk", "pub_from_priv_jwk",
117
+ "b64url", "b64url_decode", "jwk_thumbprint_rfc7638",
118
+ "did_key_from_jwk_ed25519", "did_key_fragment_from_jwk", "pub_jwk_from_did_key",
119
+ "kid_for_did_web", "kid_for_did_key",
120
+ # JOSE / JWS
121
+ "sign_compact", "verify_compact", "decode_header",
122
+ # VC / VP
123
+ "issue_vc", "make_vp", "sign_compact_jws",
124
+ "verify_presentation", "resolve_public_jwk_for_kid", "jws_header", "jws_verify",
125
+ # DID-Methoden
126
+ "build_did_document", "did_web_to_url", "resolve_did_web",
127
+ "did_peer4_from_payload", "resolve_long_form",
128
+ "resolve_did_github", "parse_did_github",
129
+ ]
sdata/did/_http.py ADDED
@@ -0,0 +1,67 @@
1
+ # -*- coding: utf-8 -*-
2
+ """HTTP-GET mit optionalem ``requests``-Backend und ``urllib``-Fallback.
3
+
4
+ Wenn ``requests`` installiert ist, wird es genutzt (gewohntes Verhalten,
5
+ Connection-Pooling, certifi-CA-Bundle); andernfalls greift ein Fallback auf
6
+ ``urllib.request`` aus der Standardbibliothek. **Die TLS-Zertifikatsprüfung
7
+ bleibt in beiden Fällen aktiv** (kein ``verify=False``).
8
+
9
+ Damit ist ``requests`` für ``sdata.did`` rein optional – das Subpackage bleibt
10
+ auch ohne die Abhängigkeit voll funktionsfähig.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import ssl
15
+ import urllib.request
16
+ import urllib.error
17
+ from typing import Dict, Optional, Tuple
18
+
19
+ try: # bevorzugtes Backend, falls installiert
20
+ import requests as _requests
21
+ except ImportError: # pragma: no cover - abhängig vom Environment
22
+ _requests = None
23
+
24
+ __all__ = ["http_get", "HttpError"]
25
+
26
+
27
+ class HttpError(Exception):
28
+ """Transportfehler (DNS/Timeout/Verbindung) beim HTTP-GET – backend-neutral.
29
+
30
+ HTTP-Statuscodes (auch 4xx/5xx) sind *keine* ``HttpError`` – sie werden von
31
+ :func:`http_get` als ``status_code`` zurückgegeben.
32
+ """
33
+
34
+
35
+ def http_get(url: str, headers: Optional[Dict[str, str]] = None,
36
+ timeout: float = 10.0) -> Tuple[int, str]:
37
+ """GET ``url`` und liefere ``(status_code, body_text)``.
38
+
39
+ Nutzt ``requests`` falls verfügbar, sonst ``urllib.request``.
40
+
41
+ Raises:
42
+ HttpError: bei Transportfehlern (DNS, Timeout, Verbindungsabbruch).
43
+ """
44
+ if _requests is not None:
45
+ return _get_requests(url, headers, timeout)
46
+ return _get_urllib(url, headers, timeout)
47
+
48
+
49
+ def _get_requests(url, headers, timeout):
50
+ try:
51
+ resp = _requests.get(url, headers=headers, timeout=timeout)
52
+ except _requests.RequestException as exc:
53
+ raise HttpError(str(exc)) from exc
54
+ return resp.status_code, resp.text
55
+
56
+
57
+ def _get_urllib(url, headers, timeout):
58
+ req = urllib.request.Request(url, headers=headers or {})
59
+ ctx = ssl.create_default_context() # Zertifikats-/Hostname-Prüfung aktiv
60
+ try:
61
+ with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
62
+ charset = resp.headers.get_content_charset() or "utf-8"
63
+ return resp.status, resp.read().decode(charset, "replace")
64
+ except urllib.error.HTTPError as exc: # 4xx/5xx: Status, kein Transportfehler
65
+ return exc.code, exc.read().decode("utf-8", "replace")
66
+ except urllib.error.URLError as exc: # DNS/Timeout/Verbindung
67
+ raise HttpError(str(exc)) from exc
sdata/did/base58btc.py ADDED
@@ -0,0 +1,49 @@
1
+ # -*- coding: utf-8 -*-
2
+ """base58btc-Kodierung (Bitcoin-Alphabet) – reine Standardbibliothek.
3
+
4
+ Ersetzt die externe ``base58``-Abhängigkeit im ``did``-Subpackage. Die
5
+ Funktionen sind bewusst klein und an die multibase-/``did:key``-Nutzung
6
+ angepasst (führende Nullbytes bleiben als ``1`` erhalten).
7
+
8
+ Referenz: https://datatracker.ietf.org/doc/html/draft-msporny-base58
9
+ """
10
+ from __future__ import annotations
11
+
12
+ __all__ = ["b58encode", "b58decode"]
13
+
14
+ # Bitcoin-/IPFS-Alphabet (ohne 0, O, I, l)
15
+ _B58_ALPHABET = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
16
+ _B58_INDEX = {ch: i for i, ch in enumerate(_B58_ALPHABET)}
17
+
18
+
19
+ def b58encode(b: bytes) -> str:
20
+ """Kodiere Bytes als base58btc (führende Nullbytes bleiben als ``1`` erhalten)."""
21
+ n = int.from_bytes(b, "big", signed=False)
22
+ out = bytearray()
23
+ while n > 0:
24
+ n, r = divmod(n, 58)
25
+ out.append(_B58_ALPHABET[r])
26
+ pad = 0
27
+ for byte in b:
28
+ if byte == 0:
29
+ pad += 1
30
+ else:
31
+ break
32
+ return ("1" * pad) + out[::-1].decode()
33
+
34
+
35
+ def b58decode(s: str) -> bytes:
36
+ """Dekodiere einen base58btc-String zurück zu Bytes.
37
+
38
+ Raises:
39
+ ValueError: bei Zeichen ausserhalb des base58-Alphabets.
40
+ """
41
+ n = 0
42
+ for ch in s.encode():
43
+ try:
44
+ n = n * 58 + _B58_INDEX[ch]
45
+ except KeyError:
46
+ raise ValueError("ungültiges base58-Zeichen: {!r}".format(chr(ch)))
47
+ pad = len(s) - len(s.lstrip("1"))
48
+ full = n.to_bytes((n.bit_length() + 7) // 8, "big") if n else b""
49
+ return b"\x00" * pad + full
@@ -0,0 +1,152 @@
1
+ # -*- coding: utf-8 -*-
2
+ """``did:github``: DID-Dokumente aus GitHub-Repositories auflösen.
3
+
4
+ Eine ``did:github``-DID verweist auf ein ``did.json`` in einem GitHub-Repo und
5
+ wird über ``raw.githubusercontent.com`` geladen. Formen::
6
+
7
+ did:github:<user>:<repo>
8
+ did:github:<user>:<repo>:<ref>
9
+ did:github:<user>:<repo>:<ref>:<subpath>
10
+
11
+ Im ``subpath`` darf ``__`` als ``/``-Ersatz verwendet werden (CLI-freundlich).
12
+ Gesucht werden ``<subpath>/.well-known/did.json``, ``<subpath>/did.json`` sowie
13
+ die Repo-Wurzel – in dieser Reihenfolge, für die Refs ``<ref>``, ``main``, ``master``.
14
+
15
+ Ein optionales ``GITHUB_TOKEN`` (Umgebungsvariable) erhöht das Rate-Limit und
16
+ erlaubt Zugriff auf private Repos.
17
+
18
+ .. note::
19
+ ``did:github`` ist **keine** registrierte W3C-DID-Methode, sondern eine
20
+ pragmatische, projektspezifische Auflösung über GitHub-Rohinhalte.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import os
25
+ import re
26
+ import json
27
+ import logging
28
+ from dataclasses import dataclass
29
+ from functools import lru_cache
30
+ from typing import List, Optional, Tuple
31
+
32
+ from ._http import http_get
33
+ from .errors import EncodingError, ResolutionError
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+ __all__ = ["DidGithub", "parse_did_github", "resolve_did_github"]
38
+
39
+ DID_GITHUB_METHOD = "did:github"
40
+ RAW_BASE = "https://raw.githubusercontent.com"
41
+ HTTP_TIMEOUT = 10
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class DidGithub:
46
+ """Geparste ``did:github``-Identität."""
47
+ user: str
48
+ repo: str
49
+ ref: Optional[str] = None # z. B. "main", "master", Tag oder Commit
50
+ subpath: Optional[str] = None # optionaler Unterordner im Repo
51
+
52
+ @property
53
+ def did(self) -> str:
54
+ parts = [DID_GITHUB_METHOD, self.user, self.repo]
55
+ if self.ref:
56
+ parts.append(self.ref)
57
+ if self.subpath:
58
+ parts.append(self.subpath)
59
+ return ":".join(parts)
60
+
61
+
62
+ def make_subpath(raw: Optional[str]) -> Optional[str]:
63
+ """Mappe ``__`` auf ``/`` (echte Slashes bleiben erhalten)."""
64
+ if not raw:
65
+ return None
66
+ return raw.replace("__", "/")
67
+
68
+
69
+ def parse_did_github(did: str) -> DidGithub:
70
+ """Parse eine ``did:github``-DID.
71
+
72
+ Raises:
73
+ EncodingError: wenn ``did`` keine gültige ``did:github``-DID ist.
74
+ """
75
+ if not did.startswith(DID_GITHUB_METHOD + ":"):
76
+ raise EncodingError("kein did:github-Identifier")
77
+ parts = did.split(":")
78
+ if len(parts) < 4:
79
+ raise EncodingError("Form: did:github:<user>:<repo>[:<ref>[:<subpath>]]")
80
+ _, _, user, repo, *rest = parts
81
+ ref = rest[0] if rest else None
82
+ subpath = make_subpath(":".join(rest[1:]) if len(rest) > 1 else None)
83
+ return DidGithub(user=user, repo=repo, ref=ref, subpath=subpath)
84
+
85
+
86
+ def candidate_paths(dg: DidGithub) -> List[Tuple[str, str]]:
87
+ """Geordnete ``(ref, path)``-Kandidaten für die Auflösung."""
88
+ well_known, root = ".well-known/did.json", "did.json"
89
+ sub_candidates = []
90
+ if dg.subpath:
91
+ base = dg.subpath.rstrip("/")
92
+ sub_candidates = ["{}/{}".format(base, well_known), "{}/{}".format(base, root)]
93
+ paths = sub_candidates + [well_known, root]
94
+ refs = [r for r in [dg.ref, "main", "master"] if r]
95
+ return [(r, p) for r in refs for p in paths]
96
+
97
+
98
+ def _raw_url(user: str, repo: str, ref: str, path: str) -> str:
99
+ return "{}/{}/{}/{}/{}".format(RAW_BASE, user, repo, ref, path)
100
+
101
+
102
+ def _headers() -> dict:
103
+ headers = {"Accept": "application/json"}
104
+ token = os.getenv("GITHUB_TOKEN")
105
+ if token:
106
+ headers["Authorization"] = "Bearer {}".format(token)
107
+ return headers
108
+
109
+
110
+ def _strip_json_comments(text: str) -> str:
111
+ """Entferne ``/* … */`` und ``// …`` (rudimentär, für einfache Fälle)."""
112
+ text = re.sub(r"/\*.*?\*/", "", text, flags=re.S)
113
+ text = re.sub(r"//.*?$", "", text, flags=re.M)
114
+ return text
115
+
116
+
117
+ @lru_cache(maxsize=256)
118
+ def fetch_json(url: str) -> Optional[dict]:
119
+ """Lade JSON von ``url`` (mit kleinem Cache); ``None`` bei Status != 200."""
120
+ status, text = http_get(url, headers=_headers(), timeout=HTTP_TIMEOUT)
121
+ if status != 200:
122
+ return None
123
+ try:
124
+ return json.loads(text)
125
+ except json.JSONDecodeError:
126
+ return json.loads(_strip_json_comments(text))
127
+
128
+
129
+ def resolve_did_github(did: str) -> dict:
130
+ """Löse eine ``did:github``-DID zum DID-Dokument auf.
131
+
132
+ Raises:
133
+ EncodingError: bei ungültiger DID.
134
+ ResolutionError: wenn kein gültiges ``did.json`` gefunden wird.
135
+ EncodingError: wenn ein gefundenes Dokument ``@context``/``id`` vermisst.
136
+ """
137
+ dg = parse_did_github(did)
138
+ tried = []
139
+ for ref, path in candidate_paths(dg):
140
+ url = _raw_url(dg.user, dg.repo, ref, path)
141
+ tried.append(url)
142
+ doc = fetch_json(url)
143
+ if doc is None:
144
+ continue
145
+ if "@context" not in doc:
146
+ raise EncodingError("DID-Dokument ohne @context ({})".format(url))
147
+ if "id" not in doc:
148
+ raise EncodingError("DID-Dokument ohne id ({})".format(url))
149
+ logger.debug("did:github aufgelöst via %s", url)
150
+ return doc
151
+ raise ResolutionError(
152
+ "did:github konnte nicht aufgelöst werden. Versucht:\n- " + "\n- ".join(tried))
sdata/did/did_peer4.py ADDED
@@ -0,0 +1,155 @@
1
+ # -*- coding: utf-8 -*-
2
+ """``did:peer:4``: statische, self-zertifizierende Peer-DIDs (Long-/Short-Form).
3
+
4
+ Diese Implementierung ist bewusst **abhängigkeitsfrei** (nur Standardbibliothek)
5
+ und bringt eine eigene base58btc- und varint-Kodierung mit – passend zum
6
+ pure-Python-Designziel des Subpackages.
7
+
8
+ Aufbau einer Long-Form::
9
+
10
+ did:peer:4{hash}:{encoded_document}
11
+
12
+ * ``encoded_document`` = multibase(base58btc, multicodec(JSON) + kanonisches JSON),
13
+ * ``hash`` = multibase(base58btc, multihash(sha2-256, encoded_document)).
14
+
15
+ Die Short-Form ``did:peer:4{hash}`` ist self-zertifizierend: beim Auflösen der
16
+ Long-Form wird der Hash gegen das eingebettete Dokument validiert.
17
+
18
+ Referenzen:
19
+ * `did:peer Method, Variante 4 <https://identity.foundation/peer-did-method-spec/>`_
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import hashlib
25
+ from typing import Dict, Tuple
26
+
27
+ from .errors import EncodingError, VerificationError
28
+ # base58btc liegt jetzt zentral im eigenen Modul (frühere Inline-Variante);
29
+ # Re-Export aus Kompatibilitätsgründen.
30
+ from .base58btc import b58encode, b58decode
31
+
32
+ __all__ = ["did_peer4_from_payload", "resolve_long_form", "b58encode", "b58decode"]
33
+
34
+
35
+ # --- Varint (LEB128, wie in multicodec) -----------------------------------
36
+ def varint_encode(n: int) -> bytes:
37
+ """Kodiere ``n`` als unsigned LEB128-varint."""
38
+ out = bytearray()
39
+ while True:
40
+ to_write = n & 0x7F
41
+ n >>= 7
42
+ if n:
43
+ out.append(to_write | 0x80)
44
+ else:
45
+ out.append(to_write)
46
+ break
47
+ return bytes(out)
48
+
49
+
50
+ def varint_decode(b: bytes) -> Tuple[int, int]:
51
+ """Dekodiere einen unsigned LEB128-varint; gibt ``(wert, gelesene_bytes)`` zurück."""
52
+ shift = 0
53
+ value = 0
54
+ for i, byte in enumerate(b):
55
+ value |= (byte & 0x7F) << shift
56
+ if not (byte & 0x80):
57
+ return value, i + 1
58
+ shift += 7
59
+ raise EncodingError("ungültiger varint")
60
+
61
+
62
+ # --- Multicodec- & Multihash-Konstanten -----------------------------------
63
+ MULTICODEC_JSON = 0x0200 # multicodec-Code für JSON
64
+ MH_SHA256 = 0x12 # multihash: sha2-256
65
+ MH_SHA256_LEN = 32
66
+
67
+
68
+ def _canonical_json_bytes(payload: Dict) -> bytes:
69
+ """Deterministische JSON-Serialisierung (Keys sortiert, ohne Whitespace)."""
70
+ s = json.dumps(payload, separators=(",", ":"), sort_keys=True, ensure_ascii=False)
71
+ return s.encode("utf-8")
72
+
73
+
74
+ def encode_document(payload: Dict) -> bytes:
75
+ """Multicodec-Framing: ``varint(JSON-Code) + kanonisches JSON``."""
76
+ return varint_encode(MULTICODEC_JSON) + _canonical_json_bytes(payload)
77
+
78
+
79
+ def multibase_b58btc(data: bytes) -> str:
80
+ """Präfix ``z`` (base58btc) gemäß Multibase."""
81
+ return "z" + b58encode(data)
82
+
83
+
84
+ def multihash_sha256(data: bytes) -> bytes:
85
+ """``multihash(sha2-256, data)`` = ``0x12 0x20 || sha256(data)``."""
86
+ return bytes([MH_SHA256, MH_SHA256_LEN]) + hashlib.sha256(data).digest()
87
+
88
+
89
+ # --- did:peer:4 Erzeugung & Auflösung -------------------------------------
90
+ def did_peer4_from_payload(payload: Dict) -> Tuple[str, str, Dict]:
91
+ """Erzeuge Long-Form, Short-Form und kontextualisiertes DID-Dokument.
92
+
93
+ Args:
94
+ payload: DID-Dokument **ohne** ``id`` (relative Fragmente wie ``#key-0``
95
+ sind erlaubt).
96
+
97
+ Returns:
98
+ ``(long_form, short_form, did_document)``.
99
+ """
100
+ encoded_doc_bytes = encode_document(payload)
101
+ encoded_doc_mb = multibase_b58btc(encoded_doc_bytes)
102
+ mh_mb = multibase_b58btc(multihash_sha256(encoded_doc_bytes))
103
+
104
+ long_form = "did:peer:4{}:{}".format(mh_mb, encoded_doc_mb)
105
+ short_form = "did:peer:4{}".format(mh_mb)
106
+ did_doc = _contextualize_did_doc(payload, did_long=long_form, did_short=short_form)
107
+ return long_form, short_form, did_doc
108
+
109
+
110
+ def resolve_long_form(did_long: str) -> Dict:
111
+ """Löse eine Long-Form ``did:peer:4`` auf und validiere den Hash.
112
+
113
+ Raises:
114
+ EncodingError: bei strukturell ungültiger DID oder falschem multicodec/multihash.
115
+ VerificationError: wenn der eingebettete Hash nicht zum Dokument passt.
116
+ """
117
+ if not did_long.startswith("did:peer:4"):
118
+ raise EncodingError("keine did:peer:4")
119
+ try:
120
+ _, rest = did_long.split("did:peer:4", 1)
121
+ hash_mb, doc_mb = rest.split(":", 1)
122
+ except ValueError:
123
+ raise EncodingError("erwarte Long-Form: did:peer:4{hash}:{doc}")
124
+
125
+ if not (hash_mb.startswith("z") and doc_mb.startswith("z")):
126
+ raise EncodingError("erwarte multibase base58btc ('z'-Präfix)")
127
+
128
+ mh = b58decode(hash_mb[1:])
129
+ encoded_doc_bytes = b58decode(doc_mb[1:])
130
+
131
+ code, offset = varint_decode(encoded_doc_bytes)
132
+ if code != MULTICODEC_JSON:
133
+ raise EncodingError("multicodec-Code ist nicht JSON (0x0200)")
134
+ json_bytes = encoded_doc_bytes[offset:]
135
+
136
+ if len(mh) < 2 or mh[0] != MH_SHA256 or mh[1] != MH_SHA256_LEN:
137
+ raise EncodingError("multihash ist nicht sha2-256/32")
138
+ if mh[2:] != hashlib.sha256(encoded_doc_bytes).digest():
139
+ raise VerificationError("Hash-Validierung fehlgeschlagen")
140
+
141
+ payload = json.loads(json_bytes.decode("utf-8"))
142
+ did_short = "did:peer:4{}".format(multibase_b58btc(mh))
143
+ return _contextualize_did_doc(payload, did_long=did_long, did_short=did_short)
144
+
145
+
146
+ def _contextualize_did_doc(payload: Dict, *, did_long: str, did_short: str) -> Dict:
147
+ """Setze ``id``/``alsoKnownAs`` und ergänze fehlende ``controller``-Felder."""
148
+ doc = json.loads(json.dumps(payload)) # tiefe Kopie
149
+ doc["id"] = did_long
150
+ doc.setdefault("alsoKnownAs", [])
151
+ if did_short not in doc["alsoKnownAs"]:
152
+ doc["alsoKnownAs"].append(did_short)
153
+ for vm in doc.get("verificationMethod", []):
154
+ vm.setdefault("controller", did_long)
155
+ return doc