hippius 0.2.4__py3-none-any.whl → 0.2.5__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.
- {hippius-0.2.4.dist-info → hippius-0.2.5.dist-info}/METADATA +1 -1
- hippius-0.2.5.dist-info/RECORD +17 -0
- hippius_sdk/__init__.py +21 -10
- hippius_sdk/cli.py +11 -0
- hippius_sdk/cli_handlers.py +256 -48
- hippius_sdk/cli_parser.py +20 -0
- hippius_sdk/cli_rich.py +8 -2
- hippius_sdk/client.py +5 -3
- hippius_sdk/errors.py +77 -0
- hippius_sdk/ipfs.py +237 -297
- hippius_sdk/ipfs_core.py +209 -9
- hippius_sdk/substrate.py +101 -14
- hippius-0.2.4.dist-info/RECORD +0 -16
- {hippius-0.2.4.dist-info → hippius-0.2.5.dist-info}/WHEEL +0 -0
- {hippius-0.2.4.dist-info → hippius-0.2.5.dist-info}/entry_points.txt +0 -0
hippius_sdk/ipfs_core.py
CHANGED
@@ -143,16 +143,65 @@ class AsyncIPFSClient:
|
|
143
143
|
async def ls(self, cid: str) -> Dict[str, Any]:
|
144
144
|
"""
|
145
145
|
List objects linked to the specified CID.
|
146
|
+
Detects if the CID is a directory and returns links to its contents.
|
146
147
|
|
147
148
|
Args:
|
148
149
|
cid: Content Identifier
|
149
150
|
|
150
151
|
Returns:
|
151
|
-
Dict with links information
|
152
|
+
Dict with links information and a is_directory flag
|
152
153
|
"""
|
153
|
-
|
154
|
-
|
155
|
-
|
154
|
+
try:
|
155
|
+
# Try using the direct IPFS API first (most reliable)
|
156
|
+
response = await self.client.post(f"{self.api_url}/api/v0/ls?arg={cid}")
|
157
|
+
response.raise_for_status()
|
158
|
+
result = response.json()
|
159
|
+
|
160
|
+
# Add a flag to indicate if this is a directory
|
161
|
+
# A directory has Links and typically more than one or has Type=1
|
162
|
+
is_directory = False
|
163
|
+
if "Objects" in result and len(result["Objects"]) > 0:
|
164
|
+
obj = result["Objects"][0]
|
165
|
+
if "Links" in obj and len(obj["Links"]) > 0:
|
166
|
+
# It has links, likely a directory
|
167
|
+
is_directory = True
|
168
|
+
# Check if any links have Type=1 (directory)
|
169
|
+
for link in obj["Links"]:
|
170
|
+
if link.get("Type") == 1:
|
171
|
+
is_directory = True
|
172
|
+
break
|
173
|
+
|
174
|
+
# Add the flag to the result
|
175
|
+
result["is_directory"] = is_directory
|
176
|
+
return result
|
177
|
+
|
178
|
+
except Exception as e:
|
179
|
+
# If the IPFS API fails, try to get info from the gateway
|
180
|
+
try:
|
181
|
+
# Try to get a small sample of the content to check if it's a directory listing
|
182
|
+
content_sample = await self.cat(cid)
|
183
|
+
is_directory = False
|
184
|
+
|
185
|
+
# If it starts with HTML doctype and has IPFS title, it's probably a directory listing
|
186
|
+
if (
|
187
|
+
content_sample.startswith(b"<!DOCTYPE html>")
|
188
|
+
and b"<title>/ipfs/" in content_sample
|
189
|
+
):
|
190
|
+
is_directory = True
|
191
|
+
|
192
|
+
# Return a simplified result similar to what the ls API would return
|
193
|
+
return {
|
194
|
+
"is_directory": is_directory,
|
195
|
+
"Objects": [
|
196
|
+
{
|
197
|
+
"Hash": cid,
|
198
|
+
"Links": [], # We can't get links from HTML content easily
|
199
|
+
}
|
200
|
+
],
|
201
|
+
}
|
202
|
+
except Exception as fallback_error:
|
203
|
+
# Re-raise the original error
|
204
|
+
raise e
|
156
205
|
|
157
206
|
async def exists(self, cid: str) -> bool:
|
158
207
|
"""
|
@@ -173,19 +222,170 @@ class AsyncIPFSClient:
|
|
173
222
|
async def download_file(self, cid: str, output_path: str) -> str:
|
174
223
|
"""
|
175
224
|
Download content from IPFS to a file.
|
225
|
+
If the CID is a directory, it will create a directory and download all files.
|
176
226
|
|
177
227
|
Args:
|
178
228
|
cid: Content identifier
|
179
|
-
output_path: Path where to save the file
|
229
|
+
output_path: Path where to save the file/directory
|
230
|
+
|
231
|
+
Returns:
|
232
|
+
Path to the saved file/directory
|
233
|
+
"""
|
234
|
+
# First, check if this is a directory using the improved ls function
|
235
|
+
try:
|
236
|
+
ls_result = await self.ls(cid)
|
237
|
+
if ls_result.get("is_directory", False):
|
238
|
+
# It's a directory, use the get command to download it properly
|
239
|
+
return await self.download_directory_with_get(cid, output_path)
|
240
|
+
except Exception:
|
241
|
+
# If ls check fails, continue with regular file download
|
242
|
+
pass
|
243
|
+
|
244
|
+
# If we reached here, treat it as a regular file
|
245
|
+
try:
|
246
|
+
# Regular file download
|
247
|
+
content = await self.cat(cid)
|
248
|
+
# Ensure the parent directory exists
|
249
|
+
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
|
250
|
+
with open(output_path, "wb") as f:
|
251
|
+
f.write(content)
|
252
|
+
return output_path
|
253
|
+
except Exception as e:
|
254
|
+
# As a last resort, try using the get command anyway
|
255
|
+
# This is helpful if the CID is a directory but we failed to detect it
|
256
|
+
try:
|
257
|
+
return await self.download_directory_with_get(cid, output_path)
|
258
|
+
except Exception:
|
259
|
+
# If all methods fail, re-raise the original error
|
260
|
+
raise e
|
261
|
+
|
262
|
+
async def download_directory(self, cid: str, output_path: str) -> str:
|
263
|
+
"""
|
264
|
+
Download a directory from IPFS.
|
265
|
+
|
266
|
+
Args:
|
267
|
+
cid: Content identifier of the directory
|
268
|
+
output_path: Path where to save the directory
|
180
269
|
|
181
270
|
Returns:
|
182
|
-
Path to the saved
|
271
|
+
Path to the saved directory
|
183
272
|
"""
|
184
|
-
|
185
|
-
|
186
|
-
|
273
|
+
# Try the more reliable get command first
|
274
|
+
try:
|
275
|
+
return await self.download_directory_with_get(cid, output_path)
|
276
|
+
except Exception as e:
|
277
|
+
# If get command fails, fall back to ls/cat method
|
278
|
+
try:
|
279
|
+
# Get directory listing
|
280
|
+
ls_result = await self.ls(cid)
|
281
|
+
if not ls_result.get("is_directory", False):
|
282
|
+
raise ValueError(f"CID {cid} is not a directory")
|
283
|
+
|
284
|
+
# Create the directory if it doesn't exist
|
285
|
+
os.makedirs(output_path, exist_ok=True)
|
286
|
+
|
287
|
+
# Extract links from the updated response format
|
288
|
+
links = []
|
289
|
+
# The ls result format is: { "Objects": [ { "Hash": "...", "Links": [...] } ] }
|
290
|
+
if "Objects" in ls_result and len(ls_result["Objects"]) > 0:
|
291
|
+
for obj in ls_result["Objects"]:
|
292
|
+
if "Links" in obj:
|
293
|
+
links.extend(obj["Links"])
|
294
|
+
|
295
|
+
# Download each file in the directory
|
296
|
+
for link in links:
|
297
|
+
file_name = link.get("Name")
|
298
|
+
file_cid = link.get("Hash")
|
299
|
+
file_type = link.get("Type")
|
300
|
+
|
301
|
+
# Skip entries without required data
|
302
|
+
if not (file_name and file_cid):
|
303
|
+
continue
|
304
|
+
|
305
|
+
# Build the path for this file/directory
|
306
|
+
file_path = os.path.join(output_path, file_name)
|
307
|
+
|
308
|
+
if file_type == 1 or file_type == "dir": # Directory type
|
309
|
+
# Recursively download the subdirectory
|
310
|
+
await self.download_directory(file_cid, file_path)
|
311
|
+
else: # File type
|
312
|
+
# Download the file
|
313
|
+
content = await self.cat(file_cid)
|
314
|
+
os.makedirs(
|
315
|
+
os.path.dirname(os.path.abspath(file_path)), exist_ok=True
|
316
|
+
)
|
317
|
+
with open(file_path, "wb") as f:
|
318
|
+
f.write(content)
|
319
|
+
|
320
|
+
return output_path
|
321
|
+
except Exception as fallback_error:
|
322
|
+
# If both methods fail, raise a more detailed error
|
323
|
+
raise RuntimeError(
|
324
|
+
f"Failed to download directory: get error: {e}, ls/cat error: {fallback_error}"
|
325
|
+
)
|
326
|
+
|
187
327
|
return output_path
|
188
328
|
|
329
|
+
async def download_directory_with_get(self, cid: str, output_path: str) -> str:
|
330
|
+
"""
|
331
|
+
Download a directory from IPFS by recursively fetching its contents.
|
332
|
+
|
333
|
+
Args:
|
334
|
+
cid: Content identifier of the directory
|
335
|
+
output_path: Path where to save the directory
|
336
|
+
|
337
|
+
Returns:
|
338
|
+
Path to the saved directory
|
339
|
+
"""
|
340
|
+
# First, get the directory listing to find all contents
|
341
|
+
try:
|
342
|
+
ls_result = await self.ls(cid)
|
343
|
+
|
344
|
+
# Create target directory
|
345
|
+
os.makedirs(output_path, exist_ok=True)
|
346
|
+
|
347
|
+
# Extract all links from the directory listing
|
348
|
+
links = []
|
349
|
+
if "Objects" in ls_result and ls_result["Objects"]:
|
350
|
+
for obj in ls_result["Objects"]:
|
351
|
+
if "Links" in obj:
|
352
|
+
links.extend(obj["Links"])
|
353
|
+
|
354
|
+
# Download each item (file or directory)
|
355
|
+
for link in links:
|
356
|
+
link_name = link.get("Name")
|
357
|
+
link_hash = link.get("Hash")
|
358
|
+
link_type = link.get("Type")
|
359
|
+
link_size = link.get("Size", 0)
|
360
|
+
|
361
|
+
if not (link_name and link_hash):
|
362
|
+
continue # Skip if missing essential data
|
363
|
+
|
364
|
+
# Build the target path
|
365
|
+
target_path = os.path.join(output_path, link_name)
|
366
|
+
|
367
|
+
if link_type == 1 or str(link_type) == "1" or link_type == "dir":
|
368
|
+
# It's a directory - recursively download
|
369
|
+
await self.download_directory_with_get(link_hash, target_path)
|
370
|
+
else:
|
371
|
+
# It's a file - download it
|
372
|
+
try:
|
373
|
+
content = await self.cat(link_hash)
|
374
|
+
os.makedirs(
|
375
|
+
os.path.dirname(os.path.abspath(target_path)), exist_ok=True
|
376
|
+
)
|
377
|
+
with open(target_path, "wb") as f:
|
378
|
+
f.write(content)
|
379
|
+
except Exception as file_error:
|
380
|
+
print(f"Failed to download file {link_name}: {str(file_error)}")
|
381
|
+
|
382
|
+
return output_path
|
383
|
+
|
384
|
+
except Exception as e:
|
385
|
+
raise RuntimeError(
|
386
|
+
f"Failed to download directory using 'get' command: {str(e)}"
|
387
|
+
)
|
388
|
+
|
189
389
|
async def add_directory(
|
190
390
|
self, dir_path: str, recursive: bool = True
|
191
391
|
) -> Dict[str, Any]:
|
hippius_sdk/substrate.py
CHANGED
@@ -11,12 +11,27 @@ from dotenv import load_dotenv
|
|
11
11
|
from mnemonic import Mnemonic
|
12
12
|
from substrateinterface import Keypair, SubstrateInterface
|
13
13
|
|
14
|
-
from hippius_sdk.config import (
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
14
|
+
from hippius_sdk.config import (
|
15
|
+
get_account_address,
|
16
|
+
get_active_account,
|
17
|
+
get_all_config,
|
18
|
+
get_config_value,
|
19
|
+
get_seed_phrase,
|
20
|
+
set_active_account,
|
21
|
+
set_seed_phrase,
|
22
|
+
)
|
23
|
+
from hippius_sdk.errors import (
|
24
|
+
HippiusAlreadyDeletedError,
|
25
|
+
HippiusFailedSubstrateDelete,
|
26
|
+
HippiusNotFoundError,
|
27
|
+
HippiusSubstrateAuthError,
|
28
|
+
HippiusSubstrateConnectionError,
|
29
|
+
)
|
30
|
+
from hippius_sdk.utils import (
|
31
|
+
format_size,
|
32
|
+
hex_to_ipfs_cid,
|
33
|
+
initialize_substrate_connection,
|
34
|
+
)
|
20
35
|
|
21
36
|
# Load environment variables
|
22
37
|
load_dotenv()
|
@@ -1125,6 +1140,50 @@ class SubstrateClient:
|
|
1125
1140
|
"""
|
1126
1141
|
return hex_to_ipfs_cid(hex_string)
|
1127
1142
|
|
1143
|
+
async def check_storage_request_exists(self, cid: str) -> bool:
|
1144
|
+
"""
|
1145
|
+
Check if a storage request exists for the given CID in the user's storage requests.
|
1146
|
+
|
1147
|
+
Args:
|
1148
|
+
cid: Content Identifier (CID) to check
|
1149
|
+
|
1150
|
+
Returns:
|
1151
|
+
bool: True if the CID exists in the user's storage requests, False otherwise
|
1152
|
+
"""
|
1153
|
+
substrate, derived_address = initialize_substrate_connection(self)
|
1154
|
+
|
1155
|
+
if not derived_address:
|
1156
|
+
# If we don't have a derived address, try to get the keypair
|
1157
|
+
if not self._ensure_keypair():
|
1158
|
+
raise ValueError("No account address available")
|
1159
|
+
derived_address = self._keypair.ss58_address
|
1160
|
+
|
1161
|
+
# Get user storage requests to check if this CID is still stored
|
1162
|
+
try:
|
1163
|
+
# Get all user storage requests
|
1164
|
+
user_files = await self.get_user_files(derived_address)
|
1165
|
+
|
1166
|
+
# Check if the CID is in the list
|
1167
|
+
for file in user_files:
|
1168
|
+
if file.get("cid") == cid or file.get("file_hash") == cid:
|
1169
|
+
return True
|
1170
|
+
|
1171
|
+
# If we didn't find it, try one more approach by querying pinning status
|
1172
|
+
try:
|
1173
|
+
pinning_status = self.get_pinning_status(derived_address)
|
1174
|
+
for request in pinning_status:
|
1175
|
+
if request.get("cid") == cid:
|
1176
|
+
return True
|
1177
|
+
except:
|
1178
|
+
# If pinning status check fails, assume it doesn't exist
|
1179
|
+
pass
|
1180
|
+
|
1181
|
+
# If we get here, the CID was not found
|
1182
|
+
return False
|
1183
|
+
except Exception:
|
1184
|
+
# If we encounter an error checking, we'll assume it exists to be safe
|
1185
|
+
return True
|
1186
|
+
|
1128
1187
|
async def cancel_storage_request(self, cid: str) -> str:
|
1129
1188
|
"""
|
1130
1189
|
Cancel a storage request by CID from the Hippius blockchain.
|
@@ -1133,10 +1192,30 @@ class SubstrateClient:
|
|
1133
1192
|
cid: Content Identifier (CID) of the file to cancel
|
1134
1193
|
|
1135
1194
|
Returns:
|
1136
|
-
str: Transaction hash
|
1195
|
+
str: Transaction hash or status message
|
1137
1196
|
"""
|
1197
|
+
# First check if this CID exists in the user's storage requests
|
1198
|
+
try:
|
1199
|
+
cid_exists = await self.check_storage_request_exists(cid)
|
1200
|
+
if not cid_exists:
|
1201
|
+
raise HippiusAlreadyDeletedError(
|
1202
|
+
f"CID {cid} is not found in storage requests - may already be deleted"
|
1203
|
+
)
|
1204
|
+
except Exception as e:
|
1205
|
+
if not isinstance(e, HippiusAlreadyDeletedError):
|
1206
|
+
# If there was an error checking, but not our custom exception, wrap it
|
1207
|
+
raise HippiusSubstrateConnectionError(
|
1208
|
+
f"Error checking if CID exists: {str(e)}"
|
1209
|
+
)
|
1210
|
+
else:
|
1211
|
+
# Re-raise our custom exception
|
1212
|
+
raise
|
1213
|
+
|
1214
|
+
# Continue with cancellation if it exists
|
1138
1215
|
if not self._ensure_keypair():
|
1139
|
-
raise
|
1216
|
+
raise HippiusSubstrateAuthError(
|
1217
|
+
"Seed phrase must be set before making transactions"
|
1218
|
+
)
|
1140
1219
|
|
1141
1220
|
substrate, _ = initialize_substrate_connection(self)
|
1142
1221
|
|
@@ -1157,9 +1236,17 @@ class SubstrateClient:
|
|
1157
1236
|
fee_tokens = fee / 10**12 if fee > 0 else 0
|
1158
1237
|
print(f"Estimated transaction fee: {fee} ({fee_tokens:.10f} tokens)")
|
1159
1238
|
|
1160
|
-
|
1161
|
-
|
1162
|
-
|
1163
|
-
|
1164
|
-
|
1165
|
-
|
1239
|
+
try:
|
1240
|
+
extrinsic = self._substrate.create_signed_extrinsic(
|
1241
|
+
call=call, keypair=self._keypair
|
1242
|
+
)
|
1243
|
+
response = self._substrate.submit_extrinsic(
|
1244
|
+
extrinsic, wait_for_inclusion=True
|
1245
|
+
)
|
1246
|
+
print(f"Transaction hash: {response.extrinsic_hash}")
|
1247
|
+
return response.extrinsic_hash
|
1248
|
+
except Exception as e:
|
1249
|
+
# If the transaction failed, raise our custom exception
|
1250
|
+
raise HippiusFailedSubstrateDelete(
|
1251
|
+
f"Failed to cancel storage request: {str(e)}"
|
1252
|
+
)
|
hippius-0.2.4.dist-info/RECORD
DELETED
@@ -1,16 +0,0 @@
|
|
1
|
-
hippius_sdk/__init__.py,sha256=WB2RYx8ds2QhT-f4WG6MJOzTNgwLySizDm2l44HCC-o,1572
|
2
|
-
hippius_sdk/cli.py,sha256=9pEbs84-I7XTzTEdlJYwy48ziciomwj37SdJTg4fRVQ,17294
|
3
|
-
hippius_sdk/cli_assets.py,sha256=V3MX63QTiex6mCp0VDXQJ7cagm5v1s4xtsu8c1O4G_k,371
|
4
|
-
hippius_sdk/cli_handlers.py,sha256=UAQN-qHeIIa_x_wIfgQnfReijFstQTLLCyL7TpX-EMY,104306
|
5
|
-
hippius_sdk/cli_parser.py,sha256=uh3JX6Jh9HiCHxyzNupjwYYayMwr6mzSKMabsAFTL_w,18892
|
6
|
-
hippius_sdk/cli_rich.py,sha256=Z8bH3c9r7zWIWOKUoRUfz-d8rdgtIFP4s33sBwWCDYw,7324
|
7
|
-
hippius_sdk/client.py,sha256=69g3OH280ulwHwdlaUGSlIVHkVWMgg_fPG5YPk1sN6k,16828
|
8
|
-
hippius_sdk/config.py,sha256=sCWD2remLa-FodvxC2O45tiNSJD7gzv9idIStX9sF_k,21240
|
9
|
-
hippius_sdk/ipfs.py,sha256=LOVXItshcH7QFnT3ijOWc3byuWgSFPTrtl9d-CzGINs,73734
|
10
|
-
hippius_sdk/ipfs_core.py,sha256=AS5jjpJ9lbC5wbDP_asB3-pZBEpHsE9VjL8aQA5VUxM,7291
|
11
|
-
hippius_sdk/substrate.py,sha256=XJ5ONcUOifyiYisKpaZhuapVn4tPBBtKa6aX3Twylig,44530
|
12
|
-
hippius_sdk/utils.py,sha256=Ur7P_7iVnXXYvbg7a0aVrdN_8NkVxjhdngn8NzR_zpc,7066
|
13
|
-
hippius-0.2.4.dist-info/METADATA,sha256=23L-M4rBxki07nCRwvwSvQiA_6knAGP8BDFniNJxTx8,29992
|
14
|
-
hippius-0.2.4.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
|
15
|
-
hippius-0.2.4.dist-info/entry_points.txt,sha256=b1lo60zRXmv1ud-c5BC-cJcAfGE5FD4qM_nia6XeQtM,98
|
16
|
-
hippius-0.2.4.dist-info/RECORD,,
|
File without changes
|
File without changes
|