datadid-sdk-python 1.0.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.
- datadid_sdk_python-1.0.0.dist-info/METADATA +479 -0
- datadid_sdk_python-1.0.0.dist-info/RECORD +12 -0
- datadid_sdk_python-1.0.0.dist-info/WHEEL +5 -0
- datadid_sdk_python-1.0.0.dist-info/top_level.txt +1 -0
- src/__init__.py +50 -0
- src/data/__init__.py +0 -0
- src/data/client.py +473 -0
- src/data/types.py +103 -0
- src/did/__init__.py +0 -0
- src/did/client.py +225 -0
- src/did/types.py +49 -0
- src/errors.py +15 -0
src/did/client.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from ..errors import DataDIDApiError
|
|
8
|
+
from .types import (
|
|
9
|
+
CreateDIDResponse,
|
|
10
|
+
DeleteDIDResponse,
|
|
11
|
+
DIDChainInfo,
|
|
12
|
+
DIDClientOptions,
|
|
13
|
+
DIDInfoResponse,
|
|
14
|
+
SigMsgResponse,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
_PRODUCTION_URL = "https://prodidapi.memolabs.org"
|
|
18
|
+
_TESTNET_URL = "https://testdidapi.memolabs.org"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DIDClient:
|
|
22
|
+
"""
|
|
23
|
+
Async REST client for the DataDID DID API (prodidapi).
|
|
24
|
+
|
|
25
|
+
Wraps DID creation/deletion and file operations.
|
|
26
|
+
DID operations use a sign-then-submit pattern:
|
|
27
|
+
1. Call get_create_message() to get a message string
|
|
28
|
+
2. Sign that message with your wallet (off-chain, free)
|
|
29
|
+
3. Call create_did() with the signature — the server pays gas
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, options: DIDClientOptions) -> None:
|
|
33
|
+
self._base_url: str = options.base_url.rstrip("/")
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def production(cls) -> DIDClient:
|
|
37
|
+
"""Create a client pointing to production (https://prodidapi.memolabs.org)."""
|
|
38
|
+
return cls(DIDClientOptions(base_url=_PRODUCTION_URL))
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def testnet(cls) -> DIDClient:
|
|
42
|
+
"""Create a client pointing to testnet (https://testdidapi.memolabs.org)."""
|
|
43
|
+
return cls(DIDClientOptions(base_url=_TESTNET_URL))
|
|
44
|
+
|
|
45
|
+
# ── DID endpoints ────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
async def get_create_message(self, address: str) -> str:
|
|
48
|
+
"""
|
|
49
|
+
Get the message you need to sign before creating a DID.
|
|
50
|
+
Sign the returned msg with your wallet, then pass the signature to create_did().
|
|
51
|
+
|
|
52
|
+
GET /did/createsigmsg
|
|
53
|
+
"""
|
|
54
|
+
data = await self._request("GET", f"/did/createsigmsg?address={address}")
|
|
55
|
+
return data.get("msg", "") if isinstance(data, dict) else str(data)
|
|
56
|
+
|
|
57
|
+
async def create_did(self, sig: str, address: str) -> str:
|
|
58
|
+
"""
|
|
59
|
+
Create a new DID. Call get_create_message() first to get the message to sign.
|
|
60
|
+
|
|
61
|
+
POST /did/create
|
|
62
|
+
"""
|
|
63
|
+
data = await self._request("POST", "/did/create", {"sig": sig, "address": address})
|
|
64
|
+
return data.get("did", "") if isinstance(data, dict) else str(data)
|
|
65
|
+
|
|
66
|
+
async def create_did_admin(self, address: str) -> str:
|
|
67
|
+
"""
|
|
68
|
+
Admin-only: create a DID for an address without requiring a signature.
|
|
69
|
+
|
|
70
|
+
POST /did/createadmin
|
|
71
|
+
"""
|
|
72
|
+
data = await self._request("POST", "/did/createadmin", {"address": address})
|
|
73
|
+
return data.get("did", "") if isinstance(data, dict) else str(data)
|
|
74
|
+
|
|
75
|
+
async def create_ton_did(self, address: str) -> str:
|
|
76
|
+
"""
|
|
77
|
+
Create a Ton-network DID for an address.
|
|
78
|
+
|
|
79
|
+
POST /did/createton
|
|
80
|
+
"""
|
|
81
|
+
data = await self._request("POST", "/did/createton", {"address": address})
|
|
82
|
+
return data.get("did", "") if isinstance(data, dict) else str(data)
|
|
83
|
+
|
|
84
|
+
async def get_did_exists(self, address: str) -> Any:
|
|
85
|
+
"""
|
|
86
|
+
Check whether a DID exists for the given address.
|
|
87
|
+
|
|
88
|
+
GET /did/exist
|
|
89
|
+
"""
|
|
90
|
+
return await self._request("GET", f"/did/exist?address={address}")
|
|
91
|
+
|
|
92
|
+
async def get_did_info(self, address: str) -> DIDInfoResponse:
|
|
93
|
+
"""
|
|
94
|
+
Get DID info (DID string + per-chain balance/address details).
|
|
95
|
+
|
|
96
|
+
GET /did/info
|
|
97
|
+
"""
|
|
98
|
+
data = await self._request("GET", f"/did/info?address={address}")
|
|
99
|
+
chain_list = [
|
|
100
|
+
DIDChainInfo(
|
|
101
|
+
address=item.get("address", ""),
|
|
102
|
+
balance=item.get("balance", ""),
|
|
103
|
+
chain=item.get("chain", ""),
|
|
104
|
+
)
|
|
105
|
+
for item in (data.get("info") or [])
|
|
106
|
+
]
|
|
107
|
+
return DIDInfoResponse(did=data.get("did", ""), info=chain_list)
|
|
108
|
+
|
|
109
|
+
async def get_delete_message(self, did: str) -> str:
|
|
110
|
+
"""
|
|
111
|
+
Get the message you need to sign before deleting a DID.
|
|
112
|
+
Sign the returned msg with your wallet, then pass the signature to delete_did().
|
|
113
|
+
|
|
114
|
+
GET /did/deletesigmsg
|
|
115
|
+
"""
|
|
116
|
+
data = await self._request("GET", f"/did/deletesigmsg?did={did}")
|
|
117
|
+
return data.get("msg", "") if isinstance(data, dict) else str(data)
|
|
118
|
+
|
|
119
|
+
async def delete_did(self, sig: str, did: str) -> DeleteDIDResponse:
|
|
120
|
+
"""
|
|
121
|
+
Delete a DID. Call get_delete_message() first to get the message to sign.
|
|
122
|
+
|
|
123
|
+
POST /did/delete
|
|
124
|
+
"""
|
|
125
|
+
data = await self._request("POST", "/did/delete", {"sig": sig, "did": did})
|
|
126
|
+
return DeleteDIDResponse(
|
|
127
|
+
did=data.get("did", ""),
|
|
128
|
+
status=data.get("status", ""),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# ── File endpoints ───────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
async def upload_file(self, data: str, address: str) -> None:
|
|
134
|
+
"""
|
|
135
|
+
Upload a file.
|
|
136
|
+
|
|
137
|
+
POST /file/upload
|
|
138
|
+
"""
|
|
139
|
+
await self._request("POST", "/file/upload", {"data": data, "address": address})
|
|
140
|
+
|
|
141
|
+
async def list_files(self, address: str) -> Any:
|
|
142
|
+
"""
|
|
143
|
+
List files for an address.
|
|
144
|
+
|
|
145
|
+
GET /file/list
|
|
146
|
+
"""
|
|
147
|
+
return await self._request("GET", f"/file/list?address={address}")
|
|
148
|
+
|
|
149
|
+
async def download_file(self, address: str) -> Any:
|
|
150
|
+
"""
|
|
151
|
+
Download a file for an address.
|
|
152
|
+
|
|
153
|
+
GET /file/download
|
|
154
|
+
"""
|
|
155
|
+
return await self._request("GET", f"/file/download?address={address}")
|
|
156
|
+
|
|
157
|
+
# ── MFile endpoints ──────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
async def create_mfile_upload(self, data: str, address: str) -> str:
|
|
160
|
+
"""
|
|
161
|
+
Start an mfile upload — returns a message to sign.
|
|
162
|
+
Pass the signature to confirm_mfile_upload() to complete the upload.
|
|
163
|
+
|
|
164
|
+
POST /mfile/upload/create
|
|
165
|
+
"""
|
|
166
|
+
result = await self._request(
|
|
167
|
+
"POST", "/mfile/upload/create", {"data": data, "address": address}
|
|
168
|
+
)
|
|
169
|
+
return str(result)
|
|
170
|
+
|
|
171
|
+
async def confirm_mfile_upload(self, sig: str, address: str) -> str:
|
|
172
|
+
"""
|
|
173
|
+
Confirm an mfile upload with your signature.
|
|
174
|
+
|
|
175
|
+
POST /mfile/upload/confirm
|
|
176
|
+
"""
|
|
177
|
+
result = await self._request(
|
|
178
|
+
"POST", "/mfile/upload/confirm", {"sig": sig, "address": address}
|
|
179
|
+
)
|
|
180
|
+
return str(result)
|
|
181
|
+
|
|
182
|
+
async def download_mfile(self, mdid: str, address: str) -> Any:
|
|
183
|
+
"""
|
|
184
|
+
Download a file by its mfile DID.
|
|
185
|
+
|
|
186
|
+
GET /mfile/download
|
|
187
|
+
"""
|
|
188
|
+
return await self._request("GET", f"/mfile/download?mdid={mdid}&address={address}")
|
|
189
|
+
|
|
190
|
+
# ── Internal helpers ─────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
async def _request(
|
|
193
|
+
self,
|
|
194
|
+
method: str,
|
|
195
|
+
path: str,
|
|
196
|
+
body: Optional[dict[str, Any]] = None,
|
|
197
|
+
) -> Any:
|
|
198
|
+
url = f"{self._base_url}{path}"
|
|
199
|
+
headers: dict[str, str] = {}
|
|
200
|
+
if body is not None:
|
|
201
|
+
headers["Content-Type"] = "application/json"
|
|
202
|
+
|
|
203
|
+
async with httpx.AsyncClient() as http:
|
|
204
|
+
response = await http.request(method, url, headers=headers, json=body)
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
json_body = response.json()
|
|
208
|
+
except Exception:
|
|
209
|
+
if not response.is_success:
|
|
210
|
+
raise DataDIDApiError(
|
|
211
|
+
f"HTTP {response.status_code} {response.reason_phrase}",
|
|
212
|
+
response.status_code,
|
|
213
|
+
)
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
if not response.is_success:
|
|
217
|
+
raise DataDIDApiError(
|
|
218
|
+
json_body.get("message")
|
|
219
|
+
or json_body.get("error")
|
|
220
|
+
or f"HTTP {response.status_code}",
|
|
221
|
+
response.status_code,
|
|
222
|
+
json_body,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
return json_body
|
src/did/types.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class DIDClientOptions:
|
|
7
|
+
"""Options for creating a DIDClient."""
|
|
8
|
+
|
|
9
|
+
base_url: str
|
|
10
|
+
"""Base URL of the DID API server."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class SigMsgResponse:
|
|
15
|
+
"""Response from GET /did/createsigmsg and GET /did/deletesigmsg."""
|
|
16
|
+
|
|
17
|
+
msg: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class CreateDIDResponse:
|
|
22
|
+
"""Response from POST /did/create, POST /did/createadmin, POST /did/createton."""
|
|
23
|
+
|
|
24
|
+
did: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class DeleteDIDResponse:
|
|
29
|
+
"""Response from POST /did/delete."""
|
|
30
|
+
|
|
31
|
+
did: str
|
|
32
|
+
status: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class DIDChainInfo:
|
|
37
|
+
"""Chain-specific info within a DID info response."""
|
|
38
|
+
|
|
39
|
+
address: str
|
|
40
|
+
balance: str
|
|
41
|
+
chain: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class DIDInfoResponse:
|
|
46
|
+
"""Response from GET /did/info."""
|
|
47
|
+
|
|
48
|
+
did: str
|
|
49
|
+
info: List[DIDChainInfo] = field(default_factory=list)
|
src/errors.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DataDIDApiError(Exception):
|
|
5
|
+
"""Error raised when a DataDID API call fails."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str, status_code: int, response_body: Any = None) -> None:
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
self.status_code: int = status_code
|
|
10
|
+
"""HTTP status code (e.g. 401, 500)."""
|
|
11
|
+
self.response_body: Any = response_body
|
|
12
|
+
"""Raw response body from the server."""
|
|
13
|
+
|
|
14
|
+
def __repr__(self) -> str:
|
|
15
|
+
return f"DataDIDApiError(message={str(self)!r}, status_code={self.status_code!r})"
|