hippius 0.2.2__py3-none-any.whl → 0.2.3__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.py CHANGED
@@ -10,7 +10,7 @@ import shutil
10
10
  import tempfile
11
11
  import time
12
12
  import uuid
13
- from typing import Any, Dict, List, Optional
13
+ from typing import Any, Callable, Dict, List, Optional
14
14
 
15
15
  import httpx
16
16
  import requests
@@ -66,7 +66,7 @@ class IPFSClient:
66
66
  """
67
67
  # Load configuration values if not explicitly provided
68
68
  if gateway is None:
69
- gateway = get_config_value("ipfs", "gateway", "https://ipfs.io")
69
+ gateway = get_config_value("ipfs", "gateway", "https://get.hippius.network")
70
70
 
71
71
  if api_url is None:
72
72
  api_url = get_config_value(
@@ -84,11 +84,12 @@ class IPFSClient:
84
84
  self.base_url = api_url
85
85
 
86
86
  try:
87
- self.client = AsyncIPFSClient(api_url)
87
+ self.client = AsyncIPFSClient(api_url=api_url, gateway=self.gateway)
88
88
  except httpx.ConnectError as e:
89
- print(f"Warning: Could not connect to IPFS node at {api_url}: {e}")
90
- # Try to connect to local IPFS daemon as fallback
91
- self.client = AsyncIPFSClient()
89
+ print(
90
+ f"Warning: Falling back to local IPFS daemon, but still using gateway={self.gateway}"
91
+ )
92
+ self.client = AsyncIPFSClient(gateway=self.gateway)
92
93
 
93
94
  self._initialize_encryption(encrypt_by_default, encryption_key)
94
95
 
@@ -483,8 +484,6 @@ class IPFSClient:
483
484
 
484
485
  # Download the file with retry logic
485
486
  retries = 0
486
- last_error = None
487
-
488
487
  while retries < max_retries:
489
488
  try:
490
489
  # Download the file
@@ -505,7 +504,6 @@ class IPFSClient:
505
504
 
506
505
  except (requests.exceptions.RequestException, IOError) as e:
507
506
  # Save the error and retry
508
- last_error = e
509
507
  retries += 1
510
508
 
511
509
  if retries < max_retries:
@@ -742,6 +740,7 @@ class IPFSClient:
742
740
  encrypt: Optional[bool] = None,
743
741
  max_retries: int = 3,
744
742
  verbose: bool = True,
743
+ progress_callback: Optional[Callable[[str, int, int], None]] = None,
745
744
  ) -> Dict[str, Any]:
746
745
  """
747
746
  Split a file using erasure coding, then upload the chunks to IPFS.
@@ -759,6 +758,8 @@ class IPFSClient:
759
758
  encrypt: Whether to encrypt the file before encoding (defaults to self.encrypt_by_default)
760
759
  max_retries: Maximum number of retry attempts for IPFS uploads
761
760
  verbose: Whether to print progress information
761
+ progress_callback: Optional callback function for progress updates
762
+ Function receives (stage_name, current, total)
762
763
 
763
764
  Returns:
764
765
  dict: Metadata including the original file info and chunk information
@@ -977,6 +978,16 @@ class IPFSClient:
977
978
  # Create a semaphore to limit concurrent uploads
978
979
  semaphore = asyncio.Semaphore(batch_size)
979
980
 
981
+ # Track total uploads for progress reporting
982
+ total_chunks = len(all_chunk_info)
983
+
984
+ # Initialize progress tracking if callback provided
985
+ if progress_callback:
986
+ progress_callback("upload", 0, total_chunks)
987
+
988
+ if verbose:
989
+ print(f"Uploading {total_chunks} erasure-coded chunks to IPFS...")
990
+
980
991
  # Define upload task for a single chunk
981
992
  async def upload_chunk(chunk_info):
982
993
  nonlocal chunk_uploads
@@ -988,13 +999,19 @@ class IPFSClient:
988
999
  )
989
1000
  chunk_info["cid"] = chunk_cid
990
1001
  chunk_uploads += 1
1002
+
1003
+ # Update progress through callback
1004
+ if progress_callback:
1005
+ progress_callback("upload", chunk_uploads, total_chunks)
1006
+
991
1007
  if verbose and chunk_uploads % 10 == 0:
992
- print(
993
- f" Uploaded {chunk_uploads}/{len(chunks) * m} chunks"
994
- )
1008
+ print(f" Uploaded {chunk_uploads}/{total_chunks} chunks")
995
1009
  return chunk_info
996
1010
  except Exception as e:
997
- print(f"Error uploading chunk {chunk_info['name']}: {str(e)}")
1011
+ if verbose:
1012
+ print(
1013
+ f"Error uploading chunk {chunk_info['name']}: {str(e)}"
1014
+ )
998
1015
  return None
999
1016
 
1000
1017
  # Create tasks for all chunk uploads
@@ -1042,7 +1059,7 @@ class IPFSClient:
1042
1059
  temp_dir: str = None,
1043
1060
  max_retries: int = 3,
1044
1061
  verbose: bool = True,
1045
- ) -> str:
1062
+ ) -> Dict:
1046
1063
  """
1047
1064
  Reconstruct a file from erasure-coded chunks using its metadata.
1048
1065
 
@@ -1054,7 +1071,7 @@ class IPFSClient:
1054
1071
  verbose: Whether to print progress information
1055
1072
 
1056
1073
  Returns:
1057
- str: Path to the reconstructed file
1074
+ Dict: containing file reconstruction info.
1058
1075
 
1059
1076
  Raises:
1060
1077
  ValueError: If reconstruction fails
@@ -1347,7 +1364,10 @@ class IPFSClient:
1347
1364
  print(f"Reconstruction complete in {total_time:.2f} seconds!")
1348
1365
  print(f"File saved to: {output_file}")
1349
1366
 
1350
- return output_file
1367
+ return {
1368
+ "output_path": output_file,
1369
+ "size_bytes": size_processed,
1370
+ }
1351
1371
 
1352
1372
  finally:
1353
1373
  # Clean up temporary directory if we created it
@@ -1365,6 +1385,7 @@ class IPFSClient:
1365
1385
  substrate_client=None,
1366
1386
  max_retries: int = 3,
1367
1387
  verbose: bool = True,
1388
+ progress_callback: Optional[Callable[[str, int, int], None]] = None,
1368
1389
  ) -> Dict[str, Any]:
1369
1390
  """
1370
1391
  Erasure code a file, upload the chunks to IPFS, and store in the Hippius marketplace.
@@ -1381,6 +1402,8 @@ class IPFSClient:
1381
1402
  substrate_client: SubstrateClient to use (or None to create one)
1382
1403
  max_retries: Maximum number of retry attempts
1383
1404
  verbose: Whether to print progress information
1405
+ progress_callback: Optional callback function for progress updates
1406
+ Function receives (stage_name, current, total)
1384
1407
 
1385
1408
  Returns:
1386
1409
  dict: Result including metadata CID and transaction hash
@@ -1398,6 +1421,7 @@ class IPFSClient:
1398
1421
  encrypt=encrypt,
1399
1422
  max_retries=max_retries,
1400
1423
  verbose=verbose,
1424
+ progress_callback=progress_callback,
1401
1425
  )
1402
1426
 
1403
1427
  # Step 2: Create substrate client if we need it
@@ -1472,3 +1496,335 @@ class IPFSClient:
1472
1496
  print(f"Error storing files in marketplace: {str(e)}")
1473
1497
  # Return the metadata even if storage fails
1474
1498
  return {"metadata": metadata, "metadata_cid": metadata_cid, "error": str(e)}
1499
+
1500
+ async def delete_file(
1501
+ self, cid: str, cancel_from_blockchain: bool = True
1502
+ ) -> Dict[str, Any]:
1503
+ """
1504
+ Delete a file from IPFS and optionally cancel its storage on the blockchain.
1505
+
1506
+ Args:
1507
+ cid: Content Identifier (CID) of the file to delete
1508
+ cancel_from_blockchain: Whether to also cancel the storage request from the blockchain
1509
+
1510
+ Returns:
1511
+ Dict containing the result of the operation
1512
+ """
1513
+ result = {
1514
+ "cid": cid,
1515
+ "unpin_result": None,
1516
+ "blockchain_result": None,
1517
+ "timing": {
1518
+ "start_time": time.time(),
1519
+ "end_time": None,
1520
+ "duration_seconds": None,
1521
+ },
1522
+ }
1523
+
1524
+ # First, unpin from IPFS
1525
+ try:
1526
+ print(f"Unpinning file from IPFS: {cid}")
1527
+ try:
1528
+ # Try to check if file exists in IPFS before unpinning
1529
+ await self.exists(cid)
1530
+ except Exception as exists_e:
1531
+ print(f"ERROR: Error checking file existence: {exists_e}")
1532
+
1533
+ unpin_result = await self.client.unpin(cid)
1534
+ result["unpin_result"] = unpin_result
1535
+ print("Successfully unpinned from IPFS")
1536
+ except Exception as e:
1537
+ print(f"Warning: Failed to unpin file from IPFS: {e}")
1538
+ raise
1539
+
1540
+ # Then, if requested, cancel from blockchain
1541
+ if cancel_from_blockchain:
1542
+ try:
1543
+ # Create a substrate client
1544
+ print(f"DEBUG: Creating SubstrateClient for blockchain cancellation...")
1545
+ substrate_client = SubstrateClient()
1546
+ print(
1547
+ f"DEBUG: Substrate client created with URL: {substrate_client.url}"
1548
+ )
1549
+ print(f"DEBUG: Calling cancel_storage_request with CID: {cid}")
1550
+
1551
+ tx_hash = await substrate_client.cancel_storage_request(cid)
1552
+ print(f"DEBUG: Received transaction hash: {tx_hash}")
1553
+
1554
+ # Check the return value - special cases for when blockchain cancellation isn't available
1555
+ if tx_hash == "no-blockchain-cancellation-available":
1556
+ print(
1557
+ "Blockchain cancellation not available, but IPFS unpinning was successful"
1558
+ )
1559
+ result["blockchain_result"] = {
1560
+ "status": "not_available",
1561
+ "message": "Blockchain cancellation not available, but IPFS unpinning was successful",
1562
+ }
1563
+ elif tx_hash.startswith("ipfs-unpinned-only"):
1564
+ error_msg = tx_hash.replace("ipfs-unpinned-only-", "")
1565
+ print(
1566
+ f"IPFS unpinning successful, but blockchain cancellation failed: {error_msg}"
1567
+ )
1568
+ result["blockchain_result"] = {
1569
+ "status": "failed",
1570
+ "error": error_msg,
1571
+ "message": "IPFS unpinning successful, but blockchain cancellation failed",
1572
+ }
1573
+ else:
1574
+ # Standard successful transaction
1575
+ result["blockchain_result"] = {
1576
+ "transaction_hash": tx_hash,
1577
+ "status": "success",
1578
+ }
1579
+ print(f"Successfully canceled storage request from blockchain")
1580
+ print(
1581
+ f"DEBUG: Blockchain cancellation succeeded with transaction hash: {tx_hash}"
1582
+ )
1583
+ except Exception as e:
1584
+ print(f"Warning: Failed to cancel storage from blockchain: {e}")
1585
+ print(
1586
+ f"DEBUG: Blockchain cancellation exception: {type(e).__name__}: {str(e)}"
1587
+ )
1588
+ if hasattr(e, "__dict__"):
1589
+ print(f"DEBUG: Exception attributes: {e.__dict__}")
1590
+ result["blockchain_error"] = str(e)
1591
+
1592
+ # Calculate timing
1593
+ result["timing"]["end_time"] = time.time()
1594
+ result["timing"]["duration_seconds"] = (
1595
+ result["timing"]["end_time"] - result["timing"]["start_time"]
1596
+ )
1597
+
1598
+ return result
1599
+
1600
+ async def delete_ec_file(
1601
+ self,
1602
+ metadata_cid: str,
1603
+ cancel_from_blockchain: bool = True,
1604
+ parallel_limit: int = 20,
1605
+ ) -> Dict[str, Any]:
1606
+ """
1607
+ Delete an erasure-coded file, including all its chunks in parallel.
1608
+
1609
+ Args:
1610
+ metadata_cid: CID of the metadata file for the erasure-coded file
1611
+ cancel_from_blockchain: Whether to cancel storage from blockchain
1612
+ parallel_limit: Maximum number of concurrent deletion operations
1613
+
1614
+ Returns:
1615
+ Dict containing the result of the operation
1616
+ """
1617
+ result = {
1618
+ "metadata_cid": metadata_cid,
1619
+ "deleted_chunks": [],
1620
+ "failed_chunks": [],
1621
+ "blockchain_result": None,
1622
+ "timing": {
1623
+ "start_time": time.time(),
1624
+ "end_time": None,
1625
+ "duration_seconds": None,
1626
+ },
1627
+ }
1628
+
1629
+ # Track deletions for reporting
1630
+ deleted_chunks_lock = asyncio.Lock()
1631
+ failed_chunks_lock = asyncio.Lock()
1632
+
1633
+ # First, get the metadata to find all chunks
1634
+ try:
1635
+ print(f"Downloading metadata file (CID: {metadata_cid})...")
1636
+ start_time = time.time()
1637
+ metadata_content = await self.client.cat(metadata_cid)
1638
+ metadata = json.loads(metadata_content.decode("utf-8"))
1639
+ metadata_download_time = time.time() - start_time
1640
+
1641
+ print(f"Metadata downloaded in {metadata_download_time:.2f} seconds")
1642
+
1643
+ # Extract chunk CIDs
1644
+ chunks = []
1645
+ total_chunks = 0
1646
+
1647
+ for chunk_data in metadata.get("chunks", []):
1648
+ for ec_chunk in chunk_data.get("ec_chunks", []):
1649
+ chunk_cid = ec_chunk.get("cid")
1650
+ if chunk_cid:
1651
+ chunks.append(chunk_cid)
1652
+ total_chunks += 1
1653
+
1654
+ print(f"Found {total_chunks} chunks to delete")
1655
+
1656
+ # Create a semaphore to limit concurrent operations
1657
+ sem = asyncio.Semaphore(parallel_limit)
1658
+
1659
+ # Define the chunk deletion function
1660
+ async def delete_chunk(chunk_cid):
1661
+ async with sem:
1662
+ try:
1663
+ print(f"Unpinning chunk: {chunk_cid}")
1664
+ await self.client.unpin(chunk_cid)
1665
+
1666
+ # Record success
1667
+ async with deleted_chunks_lock:
1668
+ result["deleted_chunks"].append(chunk_cid)
1669
+
1670
+ # Cancel from blockchain if requested
1671
+ if cancel_from_blockchain:
1672
+ try:
1673
+ substrate_client = SubstrateClient()
1674
+ tx_hash = await substrate_client.cancel_storage_request(
1675
+ chunk_cid
1676
+ )
1677
+
1678
+ # Add blockchain result
1679
+ if "chunk_results" not in result["blockchain_result"]:
1680
+ result["blockchain_result"] = {}
1681
+ result["blockchain_result"]["chunk_results"] = []
1682
+
1683
+ # Handle special return values from cancel_storage_request
1684
+ if tx_hash == "no-blockchain-cancellation-available":
1685
+ result["blockchain_result"]["chunk_results"].append(
1686
+ {
1687
+ "cid": chunk_cid,
1688
+ "status": "not_available",
1689
+ "message": "Blockchain cancellation not available",
1690
+ }
1691
+ )
1692
+ elif tx_hash.startswith("ipfs-unpinned-only"):
1693
+ error_msg = tx_hash.replace(
1694
+ "ipfs-unpinned-only-", ""
1695
+ )
1696
+ result["blockchain_result"]["chunk_results"].append(
1697
+ {
1698
+ "cid": chunk_cid,
1699
+ "status": "failed",
1700
+ "error": error_msg,
1701
+ }
1702
+ )
1703
+ else:
1704
+ # Standard successful transaction
1705
+ result["blockchain_result"]["chunk_results"].append(
1706
+ {
1707
+ "cid": chunk_cid,
1708
+ "transaction_hash": tx_hash,
1709
+ "status": "success",
1710
+ }
1711
+ )
1712
+ except Exception as e:
1713
+ print(
1714
+ f"Warning: Failed to cancel blockchain storage for chunk {chunk_cid}: {e}"
1715
+ )
1716
+
1717
+ if "chunk_results" not in result["blockchain_result"]:
1718
+ result["blockchain_result"] = {}
1719
+ result["blockchain_result"]["chunk_results"] = []
1720
+
1721
+ result["blockchain_result"]["chunk_results"].append(
1722
+ {
1723
+ "cid": chunk_cid,
1724
+ "error": str(e),
1725
+ "status": "failed",
1726
+ }
1727
+ )
1728
+
1729
+ return True
1730
+ except Exception as e:
1731
+ error_msg = f"Failed to delete chunk {chunk_cid}: {e}"
1732
+ print(f"Warning: {error_msg}")
1733
+
1734
+ # Record failure
1735
+ async with failed_chunks_lock:
1736
+ result["failed_chunks"].append(
1737
+ {"cid": chunk_cid, "error": str(e)}
1738
+ )
1739
+
1740
+ return False
1741
+
1742
+ # Start deleting chunks in parallel
1743
+ print(
1744
+ f"Starting parallel deletion of {total_chunks} chunks with max {parallel_limit} concurrent operations"
1745
+ )
1746
+ delete_tasks = [delete_chunk(cid) for cid in chunks]
1747
+ await asyncio.gather(*delete_tasks)
1748
+
1749
+ # Delete the metadata file itself
1750
+ print(f"Unpinning metadata file: {metadata_cid}")
1751
+ response = await self.client.unpin(metadata_cid)
1752
+
1753
+ print(">>>", response)
1754
+ raise SystemExit
1755
+
1756
+ # Cancel metadata from blockchain if requested
1757
+ if cancel_from_blockchain:
1758
+ try:
1759
+ print(f"Canceling blockchain storage request for metadata file...")
1760
+ substrate_client = SubstrateClient()
1761
+ tx_hash = await substrate_client.cancel_storage_request(
1762
+ metadata_cid
1763
+ )
1764
+
1765
+ # Handle special return values from cancel_storage_request
1766
+ if tx_hash == "no-blockchain-cancellation-available":
1767
+ print(
1768
+ "Blockchain cancellation not available for metadata, but IPFS unpinning was successful"
1769
+ )
1770
+ result["blockchain_result"] = {
1771
+ "status": "not_available",
1772
+ "message": "Blockchain cancellation not available, but IPFS unpinning was successful",
1773
+ }
1774
+ elif tx_hash.startswith("ipfs-unpinned-only"):
1775
+ error_msg = tx_hash.replace("ipfs-unpinned-only-", "")
1776
+ print(
1777
+ f"IPFS unpinning successful, but blockchain cancellation failed for metadata: {error_msg}"
1778
+ )
1779
+ result["blockchain_result"] = {
1780
+ "status": "failed",
1781
+ "error": error_msg,
1782
+ "message": "IPFS unpinning successful, but blockchain cancellation failed",
1783
+ }
1784
+ else:
1785
+ # Standard successful transaction
1786
+ result["blockchain_result"] = {
1787
+ "metadata_transaction_hash": tx_hash,
1788
+ "status": "success",
1789
+ }
1790
+ print(
1791
+ f"Successfully canceled blockchain storage for metadata file"
1792
+ )
1793
+ except Exception as e:
1794
+ print(
1795
+ f"Warning: Failed to cancel blockchain storage for metadata file: {e}"
1796
+ )
1797
+
1798
+ if not result["blockchain_result"]:
1799
+ result["blockchain_result"] = {}
1800
+
1801
+ result["blockchain_result"]["metadata_error"] = str(e)
1802
+ result["blockchain_result"]["status"] = "failed"
1803
+
1804
+ # Calculate and record timing information
1805
+ end_time = time.time()
1806
+ duration = end_time - result["timing"]["start_time"]
1807
+
1808
+ result["timing"]["end_time"] = end_time
1809
+ result["timing"]["duration_seconds"] = duration
1810
+
1811
+ deleted_count = len(result["deleted_chunks"])
1812
+ failed_count = len(result["failed_chunks"])
1813
+
1814
+ print(f"Deletion complete in {duration:.2f} seconds!")
1815
+ print(f"Successfully deleted: {deleted_count}/{total_chunks} chunks")
1816
+
1817
+ if failed_count > 0:
1818
+ print(f"Failed to delete: {failed_count}/{total_chunks} chunks")
1819
+
1820
+ return result
1821
+ except Exception as e:
1822
+ # Record end time even if there was an error
1823
+ result["timing"]["end_time"] = time.time()
1824
+ result["timing"]["duration_seconds"] = (
1825
+ result["timing"]["end_time"] - result["timing"]["start_time"]
1826
+ )
1827
+
1828
+ error_msg = f"Error deleting erasure-coded file: {e}"
1829
+ print(f"Error: {error_msg}")
1830
+ raise RuntimeError(error_msg)
hippius_sdk/ipfs_core.py CHANGED
@@ -11,7 +11,9 @@ class AsyncIPFSClient:
11
11
  """
12
12
 
13
13
  def __init__(
14
- self, api_url: str = "http://localhost:5001", gateway: str = "https://ipfs.io"
14
+ self,
15
+ api_url: str = "http://localhost:5001",
16
+ gateway: str = "https://get.hippius.network",
15
17
  ):
16
18
  # Handle multiaddr format
17
19
  if api_url and api_url.startswith("/"):
@@ -119,6 +121,25 @@ class AsyncIPFSClient:
119
121
  response.raise_for_status()
120
122
  return response.json()
121
123
 
124
+ async def unpin(self, cid: str) -> Dict[str, Any]:
125
+ """
126
+ Unpin content by CID.
127
+
128
+ Args:
129
+ cid: Content Identifier to unpin
130
+
131
+ Returns:
132
+ Response from the IPFS node
133
+ """
134
+ pin_ls_url = f"{self.api_url}/api/v0/pin/ls?arg={cid}"
135
+ pin_ls_response = await self.client.post(pin_ls_url)
136
+ pin_ls_response.raise_for_status()
137
+ response = await self.client.post(f"{self.api_url}/api/v0/pin/rm?arg={cid}")
138
+
139
+ response.raise_for_status()
140
+ result = response.json()
141
+ return result
142
+
122
143
  async def ls(self, cid: str) -> Dict[str, Any]:
123
144
  """
124
145
  List objects linked to the specified CID.