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_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
- response = await self.client.post(f"{self.api_url}/api/v0/ls?arg={cid}")
154
- response.raise_for_status()
155
- return response.json()
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 file
271
+ Path to the saved directory
183
272
  """
184
- content = await self.cat(cid)
185
- with open(output_path, "wb") as f:
186
- f.write(content)
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 (get_account_address, get_active_account,
15
- get_all_config, get_config_value,
16
- get_seed_phrase, set_active_account,
17
- set_seed_phrase)
18
- from hippius_sdk.utils import (format_size, hex_to_ipfs_cid,
19
- initialize_substrate_connection)
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 ValueError("Seed phrase must be set before making transactions")
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
- extrinsic = self._substrate.create_signed_extrinsic(
1161
- call=call, keypair=self._keypair
1162
- )
1163
- response = self._substrate.submit_extrinsic(extrinsic, wait_for_inclusion=True)
1164
- print(f"Transaction hash: {response.extrinsic_hash}")
1165
- return response.extrinsic_hash
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
+ )
@@ -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,,