hippius 0.2.3__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
@@ -20,6 +20,13 @@ from hippius_sdk.config import (
20
20
  set_active_account,
21
21
  set_seed_phrase,
22
22
  )
23
+ from hippius_sdk.errors import (
24
+ HippiusAlreadyDeletedError,
25
+ HippiusFailedSubstrateDelete,
26
+ HippiusNotFoundError,
27
+ HippiusSubstrateAuthError,
28
+ HippiusSubstrateConnectionError,
29
+ )
23
30
  from hippius_sdk.utils import (
24
31
  format_size,
25
32
  hex_to_ipfs_cid,
@@ -155,7 +162,6 @@ class SubstrateClient:
155
162
  try:
156
163
  self._keypair = Keypair.create_from_mnemonic(self._seed_phrase)
157
164
  self._account_address = self._keypair.ss58_address
158
- print(f"Keypair created for account: {self._keypair.ss58_address}")
159
165
  self._read_only = False
160
166
  return True
161
167
  except Exception as e:
@@ -163,22 +169,14 @@ class SubstrateClient:
163
169
  return False
164
170
 
165
171
  # Otherwise, try to get the seed phrase from config
166
- try:
167
- config_seed = get_seed_phrase(
168
- self._seed_phrase_password, self._account_name
169
- )
170
- if config_seed:
171
- self._seed_phrase = config_seed
172
- self._keypair = Keypair.create_from_mnemonic(self._seed_phrase)
173
- self._account_address = self._keypair.ss58_address
174
- print(f"Keypair created for account: {self._keypair.ss58_address}")
175
- self._read_only = False
176
- return True
177
- else:
178
- print("No seed phrase available. Cannot sign transactions.")
179
- return False
180
- except Exception as e:
181
- print(f"Warning: Could not get seed phrase from config: {e}")
172
+ config_seed = get_seed_phrase(self._seed_phrase_password, self._account_name)
173
+ if config_seed:
174
+ self._seed_phrase = config_seed
175
+ self._keypair = Keypair.create_from_mnemonic(self._seed_phrase)
176
+ self._account_address = self._keypair.ss58_address
177
+ self._read_only = False
178
+ return True
179
+ else:
182
180
  return False
183
181
 
184
182
  def generate_mnemonic(self) -> str:
@@ -194,6 +192,15 @@ class SubstrateClient:
194
192
  except Exception as e:
195
193
  raise ValueError(f"Error generating mnemonic: {e}")
196
194
 
195
+ def generate_seed_phrase(self) -> str:
196
+ """
197
+ Generate a new random seed phrase (alias for generate_mnemonic).
198
+
199
+ Returns:
200
+ str: A 12-word mnemonic seed phrase
201
+ """
202
+ return self.generate_mnemonic()
203
+
197
204
  def create_account(
198
205
  self, name: str, encode: bool = False, password: Optional[str] = None
199
206
  ) -> Dict[str, Any]:
@@ -482,9 +489,8 @@ class SubstrateClient:
482
489
  try:
483
490
  self._keypair = Keypair.create_from_mnemonic(self._seed_phrase)
484
491
  self._account_address = self._keypair.ss58_address
485
- print(f"Keypair created for account: {self._keypair.ss58_address}")
486
492
  except Exception as e:
487
- print(f"Warning: Could not create keypair from seed phrase: {e}")
493
+ raise ValueError(f"Could not create keypair from seed phrase: {e}")
488
494
 
489
495
  async def storage_request(
490
496
  self, files: List[Union[FileInput, Dict[str, str]]], miner_ids: List[str] = None
@@ -1134,6 +1140,50 @@ class SubstrateClient:
1134
1140
  """
1135
1141
  return hex_to_ipfs_cid(hex_string)
1136
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
+
1137
1187
  async def cancel_storage_request(self, cid: str) -> str:
1138
1188
  """
1139
1189
  Cancel a storage request by CID from the Hippius blockchain.
@@ -1142,10 +1192,30 @@ class SubstrateClient:
1142
1192
  cid: Content Identifier (CID) of the file to cancel
1143
1193
 
1144
1194
  Returns:
1145
- str: Transaction hash
1195
+ str: Transaction hash or status message
1146
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
1147
1215
  if not self._ensure_keypair():
1148
- raise ValueError("Seed phrase must be set before making transactions")
1216
+ raise HippiusSubstrateAuthError(
1217
+ "Seed phrase must be set before making transactions"
1218
+ )
1149
1219
 
1150
1220
  substrate, _ = initialize_substrate_connection(self)
1151
1221
 
@@ -1166,9 +1236,17 @@ class SubstrateClient:
1166
1236
  fee_tokens = fee / 10**12 if fee > 0 else 0
1167
1237
  print(f"Estimated transaction fee: {fee} ({fee_tokens:.10f} tokens)")
1168
1238
 
1169
- extrinsic = self._substrate.create_signed_extrinsic(
1170
- call=call, keypair=self._keypair
1171
- )
1172
- response = self._substrate.submit_extrinsic(extrinsic, wait_for_inclusion=True)
1173
- print(f"Transaction hash: {response.extrinsic_hash}")
1174
- 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=usXyUpz3sVTOfYuhYSbxsVL0TjbwtffjLDELEKDpEo4,1391
2
- hippius_sdk/cli.py,sha256=Auy9eIlQYMpmIOIRfMBILiT5edoaW4NK6R5ETABCQ5k,16987
3
- hippius_sdk/cli_assets.py,sha256=XUQyiyswa2-9t5MsW_oGrWytG0Mz8geXGLnA3w0jyv8,781
4
- hippius_sdk/cli_handlers.py,sha256=BGpAAYph83kPsEDzf5t-0B-z6zV75_AtYjwDOk7z7dU,87803
5
- hippius_sdk/cli_parser.py,sha256=vZUGpi-NoYm4Bv9NrkjrnEVZJ3NMwhHVyV-7cEYZ7Gg,18737
6
- hippius_sdk/cli_rich.py,sha256=_jTBYMdHi2--fIVwoeNi-EtkdOb6Zy_O2TUiGvU3O7s,7324
7
- hippius_sdk/client.py,sha256=a-jr3_RAin_laT2qGvMm8taUb_5yYlNMwi7JVQSFj0w,16689
8
- hippius_sdk/config.py,sha256=JDI0Vb4s-FfJsOJ25wEA6rTiCS0tzfDmzlGGOy77Zko,22962
9
- hippius_sdk/ipfs.py,sha256=fld4YUZFrBnsq69gQNpEMCsvowE1QwOFhhSxF7Aed-k,72700
10
- hippius_sdk/ipfs_core.py,sha256=AS5jjpJ9lbC5wbDP_asB3-pZBEpHsE9VjL8aQA5VUxM,7291
11
- hippius_sdk/substrate.py,sha256=RIm2acNAxMJCxywXQJ-9V_usqs01g5CcaEfdSFwhVfo,44734
12
- hippius_sdk/utils.py,sha256=Ur7P_7iVnXXYvbg7a0aVrdN_8NkVxjhdngn8NzR_zpc,7066
13
- hippius-0.2.3.dist-info/METADATA,sha256=zrxR5YdTOb0bt_9aX0Waby76v8Pg8MyjA1_kI7NO_78,28143
14
- hippius-0.2.3.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
15
- hippius-0.2.3.dist-info/entry_points.txt,sha256=b1lo60zRXmv1ud-c5BC-cJcAfGE5FD4qM_nia6XeQtM,98
16
- hippius-0.2.3.dist-info/RECORD,,