hippius 0.2.5__tar.gz → 0.2.6__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hippius
3
- Version: 0.2.5
3
+ Version: 0.2.6
4
4
  Summary: Python SDK and CLI for Hippius blockchain storage
5
5
  Home-page: https://github.com/thenervelab/hippius-sdk
6
6
  Author: Dubs
@@ -26,7 +26,7 @@ from hippius_sdk.config import (
26
26
  from hippius_sdk.ipfs import IPFSClient
27
27
  from hippius_sdk.utils import format_cid, format_size, hex_to_ipfs_cid
28
28
 
29
- __version__ = "0.2.5"
29
+ __version__ = "0.2.6"
30
30
  __all__ = [
31
31
  "HippiusClient",
32
32
  "IPFSClient",
@@ -152,6 +152,7 @@ def main():
152
152
  args.file_path,
153
153
  miner_ids,
154
154
  encrypt=encrypt,
155
+ publish=not args.no_publish if hasattr(args, "no_publish") else True,
155
156
  )
156
157
 
157
158
  elif args.command == "store-dir":
@@ -51,6 +51,7 @@ from hippius_sdk.errors import (
51
51
  HippiusFailedSubstrateDelete,
52
52
  HippiusMetadataError,
53
53
  )
54
+ from hippius_sdk.substrate import FileInput
54
55
 
55
56
  try:
56
57
  import nacl.secret
@@ -246,6 +247,7 @@ async def handle_store(
246
247
  file_path: str,
247
248
  miner_ids: Optional[List[str]] = None,
248
249
  encrypt: Optional[bool] = None,
250
+ publish: bool = True,
249
251
  ) -> int:
250
252
  """Handle the store command (upload file to IPFS and store on Substrate)"""
251
253
  if not os.path.exists(file_path):
@@ -256,6 +258,16 @@ async def handle_store(
256
258
  error(f"[bold]{file_path}[/bold] is not a file")
257
259
  return 1
258
260
 
261
+ # If publishing is enabled, ensure we have a valid substrate client by accessing it
262
+ # This will trigger password prompts if needed right at the beginning
263
+ if publish and hasattr(client, "substrate_client") and client.substrate_client:
264
+ try:
265
+ # Force keypair initialization - this will prompt for password if needed
266
+ _ = client.substrate_client._ensure_keypair()
267
+ except Exception as e:
268
+ warning(f"Failed to initialize blockchain client: {str(e)}")
269
+ warning("Will continue with upload but blockchain publishing may fail")
270
+
259
271
  # Get file size for display
260
272
  file_size = os.path.getsize(file_path)
261
273
  file_name = os.path.basename(file_path)
@@ -282,6 +294,19 @@ async def handle_store(
282
294
  "[bold yellow]Encryption: Using default setting[/bold yellow]"
283
295
  )
284
296
 
297
+ # Add publishing status
298
+ if not publish:
299
+ upload_info.append(
300
+ "[bold yellow]Publishing: Disabled (local upload only)[/bold yellow]"
301
+ )
302
+ log(
303
+ "\nUpload will be local only - not publishing to blockchain or pinning to IPFS"
304
+ )
305
+ else:
306
+ upload_info.append(
307
+ "[bold green]Publishing: Enabled (publishing to blockchain)[/bold green]"
308
+ )
309
+
285
310
  # Parse miner IDs if provided
286
311
  miner_id_list = None
287
312
  if miner_ids:
@@ -318,13 +343,51 @@ async def handle_store(
318
343
  updater = asyncio.create_task(update_progress())
319
344
 
320
345
  try:
321
- # Use the store_file method
346
+ # Use the upload_file method to get the CID
322
347
  result = await client.upload_file(
323
348
  file_path=file_path,
324
349
  encrypt=encrypt,
325
- # miner_ids=miner_id_list
326
350
  )
327
351
 
352
+ # If publishing is enabled, store on blockchain
353
+ if publish and result.get("cid"):
354
+ try:
355
+ # Pin and publish the file globally
356
+ # First pin in IPFS (essential step for publishing)
357
+ await client.ipfs_client.pin(result["cid"])
358
+
359
+ # Then publish globally to make available across network
360
+ publish_result = await client.ipfs_client.publish_global(
361
+ result["cid"]
362
+ )
363
+
364
+ log(
365
+ "\n[green]File has been pinned to IPFS and published to the network[/green]"
366
+ )
367
+
368
+ # Add gateway URL to the result for use in output
369
+ if "cid" in result:
370
+ result[
371
+ "gateway_url"
372
+ ] = f"{client.ipfs_client.gateway}/ipfs/{result['cid']}"
373
+
374
+ # Store on blockchain if miners are provided
375
+ if miner_ids:
376
+ # Create a file input for blockchain storage
377
+ file_input = FileInput(
378
+ file_hash=result["cid"], file_name=file_name
379
+ )
380
+
381
+ # Submit storage request
382
+ tx_hash = await client.substrate_client.storage_request(
383
+ files=[file_input], miner_ids=miner_id_list
384
+ )
385
+
386
+ # Add transaction hash to result
387
+ result["transaction_hash"] = tx_hash
388
+ except Exception as e:
389
+ warning(f"Failed to publish file globally: {str(e)}")
390
+
328
391
  progress.update(task, completed=100)
329
392
  updater.cancel()
330
393
 
@@ -336,16 +399,24 @@ async def handle_store(
336
399
  f"IPFS CID: [bold cyan]{result['cid']}[/bold cyan]",
337
400
  ]
338
401
 
339
- if result.get("gateway_url"):
340
- success_info.append(
341
- f"Gateway URL: [link]{result['gateway_url']}[/link]"
342
- )
402
+ # Always add the gateway URL
403
+ gateway_url = result.get("gateway_url")
404
+ if not gateway_url and "cid" in result:
405
+ gateway_url = f"{client.ipfs_client.gateway}/ipfs/{result['cid']}"
406
+
407
+ if gateway_url:
408
+ success_info.append(f"Gateway URL: [link]{gateway_url}[/link]")
343
409
 
344
410
  if result.get("encrypted"):
345
411
  success_info.append(
346
412
  "[bold yellow]File was encrypted during upload[/bold yellow]"
347
413
  )
348
414
 
415
+ if not publish:
416
+ success_info.append(
417
+ "[bold yellow]File was uploaded locally only (not published to blockchain)[/bold yellow]"
418
+ )
419
+
349
420
  print_panel("\n".join(success_info), title="Upload Successful")
350
421
 
351
422
  # If we stored in the marketplace
@@ -382,6 +453,16 @@ async def handle_store_dir(
382
453
  error(f"[bold]{dir_path}[/bold] is not a directory")
383
454
  return 1
384
455
 
456
+ # If publishing is enabled, ensure we have a valid substrate client by accessing it
457
+ # This will trigger password prompts if needed right at the beginning
458
+ if publish and hasattr(client, "substrate_client") and client.substrate_client:
459
+ try:
460
+ # Force keypair initialization - this will prompt for password if needed
461
+ _ = client.substrate_client._ensure_keypair()
462
+ except Exception as e:
463
+ warning(f"Failed to initialize blockchain client: {str(e)}")
464
+ warning("Will continue with upload but blockchain publishing may fail")
465
+
385
466
  # Upload information panel
386
467
  upload_info = [f"Directory: [bold]{dir_path}[/bold]"]
387
468
 
@@ -460,14 +541,62 @@ async def handle_store_dir(
460
541
  del result["transaction_hash"]
461
542
  else:
462
543
  # If we want to publish, make sure files are pinned globally
463
- for file_info in result.get("files", []):
464
- if "cid" in file_info:
465
- try:
466
- await client.ipfs_client.publish_global(file_info["cid"])
467
- except Exception as e:
468
- warning(
469
- f"Failed to publish file {file_info['name']} globally: {str(e)}"
470
- )
544
+ try:
545
+ # Add gateway URL to the result for use in output
546
+ if "cid" in result:
547
+ result[
548
+ "gateway_url"
549
+ ] = f"{client.ipfs_client.gateway}/ipfs/{result['cid']}"
550
+
551
+ # Pin and publish the directory root CID globally
552
+ # First pin in IPFS (essential step for publishing)
553
+ await client.ipfs_client.pin(result["cid"])
554
+
555
+ # Then publish globally to make available across network
556
+ await client.ipfs_client.publish_global(result["cid"])
557
+
558
+ log(
559
+ "\n[green]Directory has been pinned to IPFS and published to the network[/green]"
560
+ )
561
+
562
+ # Also pin and publish individual files if available
563
+ for file_info in result.get("files", []):
564
+ if "cid" in file_info:
565
+ try:
566
+ # Pin each file to ensure availability
567
+ await client.ipfs_client.pin(file_info["cid"])
568
+
569
+ # Then publish globally
570
+ await client.ipfs_client.publish_global(
571
+ file_info["cid"]
572
+ )
573
+ except Exception as e:
574
+ warning(
575
+ f"Failed to publish file {file_info['name']} globally: {str(e)}"
576
+ )
577
+
578
+ # Store on blockchain if miners are provided - this is what requires a password
579
+ if (
580
+ miner_ids
581
+ and hasattr(client, "substrate_client")
582
+ and client.substrate_client
583
+ ):
584
+ # Create a file input for blockchain storage
585
+ file_input = FileInput(
586
+ file_hash=result["cid"],
587
+ file_name=os.path.basename(dir_path),
588
+ )
589
+
590
+ # This will prompt for a password if needed
591
+ tx_hash = await client.substrate_client.storage_request(
592
+ files=[file_input], miner_ids=miner_id_list
593
+ )
594
+
595
+ # Add transaction hash to result
596
+ result["transaction_hash"] = tx_hash
597
+
598
+ except Exception as e:
599
+ warning(f"Failed to publish directory globally: {str(e)}")
471
600
 
472
601
  # Complete the progress
473
602
  progress.update(task, completed=100)
@@ -482,9 +611,23 @@ async def handle_store_dir(
482
611
  f"Directory CID: [bold cyan]{result['cid']}[/bold cyan]",
483
612
  ]
484
613
 
485
- if result.get("gateway_url"):
614
+ # Always add the gateway URL
615
+ gateway_url = result.get("gateway_url")
616
+ if not gateway_url and "cid" in result:
617
+ gateway_url = f"{client.ipfs_client.gateway}/ipfs/{result['cid']}"
618
+
619
+ if gateway_url:
620
+ success_info.append(f"Gateway URL: [link]{gateway_url}[/link]")
621
+
622
+ # Add encryption and publish status to success info
623
+ if result.get("encrypted"):
624
+ success_info.append(
625
+ "[bold yellow]Directory was encrypted during upload[/bold yellow]"
626
+ )
627
+
628
+ if not publish:
486
629
  success_info.append(
487
- f"Gateway URL: [link]{result['gateway_url']}[/link]"
630
+ "[bold yellow]Directory was uploaded locally only (not published to blockchain)[/bold yellow]"
488
631
  )
489
632
 
490
633
  print_panel("\n".join(success_info), title="Directory Upload Successful")
@@ -508,10 +651,15 @@ async def handle_store_dir(
508
651
  )
509
652
 
510
653
  # If publishing is enabled and we stored in the marketplace
511
- if publish and "transaction_hash" in result:
512
- log(
513
- f"\nStored in marketplace. Transaction hash: [bold]{result['transaction_hash']}[/bold]"
514
- )
654
+ if publish:
655
+ # We only include transaction hash stuff if we actually created a blockchain transaction
656
+ if "transaction_hash" in result:
657
+ log(
658
+ f"\nStored in marketplace. Transaction hash: [bold]{result['transaction_hash']}[/bold]"
659
+ )
660
+ else:
661
+ # If publish is true but no transaction hash, just indicate files were published to IPFS
662
+ log("\n[green]Directory was published to IPFS network.[/green]")
515
663
  elif not publish:
516
664
  log(
517
665
  "\n[yellow]Files were uploaded locally only. No blockchain publication or IPFS pinning.[/yellow]"
@@ -443,6 +443,7 @@ class IPFSClient:
443
443
  output_path: str,
444
444
  decrypt: Optional[bool] = None,
445
445
  max_retries: int = 3,
446
+ skip_directory_check: bool = False,
446
447
  ) -> Dict[str, Any]:
447
448
  """
448
449
  Download a file from IPFS with optional decryption.
@@ -453,6 +454,7 @@ class IPFSClient:
453
454
  output_path: Path where the downloaded file/directory will be saved
454
455
  decrypt: Whether to decrypt the file (overrides default)
455
456
  max_retries: Maximum number of retry attempts (default: 3)
457
+ skip_directory_check: If True, skips directory check (treats as file)
456
458
 
457
459
  Returns:
458
460
  Dict[str, Any]: Dictionary containing download results:
@@ -470,15 +472,17 @@ class IPFSClient:
470
472
  """
471
473
  start_time = time.time()
472
474
 
473
- # Use the improved ls function to properly detect directories
475
+ # Skip directory check if requested (important for erasure code chunks)
474
476
  is_directory = False
475
- try:
476
- # The ls function now properly detects directories
477
- ls_result = await self.client.ls(cid)
478
- is_directory = ls_result.get("is_directory", False)
479
- except Exception:
480
- # If ls fails, we'll proceed as if it's a file
481
- pass
477
+ if not skip_directory_check:
478
+ # Use the improved ls function to properly detect directories
479
+ try:
480
+ # The ls function now properly detects directories
481
+ ls_result = await self.client.ls(cid)
482
+ is_directory = ls_result.get("is_directory", False)
483
+ except Exception:
484
+ # If ls fails, we'll proceed as if it's a file
485
+ pass
482
486
 
483
487
  # If it's a directory, handle it differently
484
488
  if is_directory:
@@ -530,7 +534,10 @@ class IPFSClient:
530
534
  else:
531
535
  download_path = output_path
532
536
 
533
- await self.client.download_file(cid, download_path)
537
+ # Pass the skip_directory_check parameter to the core client
538
+ await self.client.download_file(
539
+ cid, download_path, skip_directory_check=skip_directory_check
540
+ )
534
541
  download_success = True
535
542
 
536
543
  if not download_success:
@@ -1225,8 +1232,12 @@ class IPFSClient:
1225
1232
  async def download_chunk(cid, path, chunk_info):
1226
1233
  async with encoded_chunks_semaphore:
1227
1234
  try:
1235
+ # Always skip directory check for erasure code chunks
1228
1236
  await self.download_file(
1229
- cid, path, max_retries=max_retries
1237
+ cid,
1238
+ path,
1239
+ max_retries=max_retries,
1240
+ skip_directory_check=True,
1230
1241
  )
1231
1242
 
1232
1243
  # Read chunk data
@@ -219,7 +219,9 @@ class AsyncIPFSClient:
219
219
  except httpx.HTTPError:
220
220
  return False
221
221
 
222
- async def download_file(self, cid: str, output_path: str) -> str:
222
+ async def download_file(
223
+ self, cid: str, output_path: str, skip_directory_check: bool = False
224
+ ) -> str:
223
225
  """
224
226
  Download content from IPFS to a file.
225
227
  If the CID is a directory, it will create a directory and download all files.
@@ -227,19 +229,22 @@ class AsyncIPFSClient:
227
229
  Args:
228
230
  cid: Content identifier
229
231
  output_path: Path where to save the file/directory
232
+ skip_directory_check: If True, skip directory check (useful for erasure code chunks)
230
233
 
231
234
  Returns:
232
235
  Path to the saved file/directory
233
236
  """
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
237
+ # Skip directory check if requested (useful for erasure code chunks)
238
+ if not skip_directory_check:
239
+ # First, check if this is a directory using the improved ls function
240
+ try:
241
+ ls_result = await self.ls(cid)
242
+ if ls_result.get("is_directory", False):
243
+ # It's a directory, use the get command to download it properly
244
+ return await self.download_directory_with_get(cid, output_path)
245
+ except Exception:
246
+ # If ls check fails, continue with regular file download
247
+ pass
243
248
 
244
249
  # If we reached here, treat it as a regular file
245
250
  try:
@@ -251,13 +256,14 @@ class AsyncIPFSClient:
251
256
  f.write(content)
252
257
  return output_path
253
258
  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
259
+ # Only try directory fallback if not skipping directory check
260
+ if not skip_directory_check:
261
+ try:
262
+ return await self.download_directory_with_get(cid, output_path)
263
+ except Exception:
264
+ pass
265
+ # Raise the original error
266
+ raise e
261
267
 
262
268
  async def download_directory(self, cid: str, output_path: str) -> str:
263
269
  """
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "hippius"
3
- version = "0.2.5"
3
+ version = "0.2.6"
4
4
  description = "Python SDK and CLI for Hippius blockchain storage"
5
5
  authors = ["Dubs <dubs@dubs.rs>"]
6
6
  readme = "README.md"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes