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.
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})"