hippius 0.2.20__py3-none-any.whl → 0.2.21__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hippius
3
- Version: 0.2.20
3
+ Version: 0.2.21
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
@@ -1,10 +1,10 @@
1
- hippius_sdk/__init__.py,sha256=9rPlh9Ob5xHhR9owW4IxihHaanA0vxuMrDidpW5l0Fw,1474
1
+ hippius_sdk/__init__.py,sha256=GR_6lzziCmPq3cvSasIrT3n2Px3l6Jv1OXAEXDZ0fX0,1474
2
2
  hippius_sdk/cli.py,sha256=aqKOYSBSWt7UhcpFt7wf9yIPJ3bznpsJ6ehOnuZ4usI,18235
3
3
  hippius_sdk/cli_assets.py,sha256=V3MX63QTiex6mCp0VDXQJ7cagm5v1s4xtsu8c1O4G_k,371
4
4
  hippius_sdk/cli_handlers.py,sha256=TQNE9os87gRzRKLEO-SIwhFnBtEFMiaSESv-Bu7omfo,128909
5
5
  hippius_sdk/cli_parser.py,sha256=z7UvgWvvy04ey-R56qZiCqYc_9RaNq1rVDkQyXoK3JU,21100
6
6
  hippius_sdk/cli_rich.py,sha256=_jTBYMdHi2--fIVwoeNi-EtkdOb6Zy_O2TUiGvU3O7s,7324
7
- hippius_sdk/client.py,sha256=k8K3__zDtWJhCbHPAPYu9FopSNXsrw1HpVAX6fV7pnI,21462
7
+ hippius_sdk/client.py,sha256=ktlv-s7H53h4zWcdz1EYf6fAqioohcFAxhRDKtljlEg,22252
8
8
  hippius_sdk/config.py,sha256=Hf_aUYzG9ylzqauA_ABUSSB5mBTYbp-VtB36VQt2XDw,21981
9
9
  hippius_sdk/db/README.md,sha256=okDeI1qgkaZqXSlJ8L0xIE4UpuxO-qEGPIbXUvSHQjU,2030
10
10
  hippius_sdk/db/env.db.template,sha256=_6hEC3IvkzCDOAzG1_yJUKRUfCTMciNaJUicZpMCat4,217
@@ -12,12 +12,12 @@ hippius_sdk/db/migrations/20241201000001_create_key_storage_tables.sql,sha256=mi
12
12
  hippius_sdk/db/setup_database.sh,sha256=bDeIiTnMuX0AYCdefAfEI1CyXho6A6kLzufsDZFSXpE,3118
13
13
  hippius_sdk/db_utils.py,sha256=-x0rbN0as7Tn3PJPZBYCgreZe52FLH40ppA1TLxsg90,1851
14
14
  hippius_sdk/errors.py,sha256=LScJJmawVAx7aRzqqQguYSkf9iazSjEQEBNlD_GXZ6Y,1589
15
- hippius_sdk/ipfs.py,sha256=Bp-f4QjrPeLKy61SxNsmB_YWvVjcYVBJlTt4NN6mGWo,90907
15
+ hippius_sdk/ipfs.py,sha256=QjqBYnhqzAiST3CNCCLxwPxEMGaV2iLwk_IBTp740wg,93188
16
16
  hippius_sdk/ipfs_core.py,sha256=eOOgLoyP9mvwndnCjldnTc7z94ImYCXY3nm7JU3e_Mo,12676
17
- hippius_sdk/key_storage.py,sha256=-f4pEUvkczY_yw3lYJ_kM2iDDOAtKOgrexWxVgIo78w,9486
17
+ hippius_sdk/key_storage.py,sha256=oxfRRQXu8XVncUjhKJJ8rsc81JAkaj9MqZEwQa96idc,9474
18
18
  hippius_sdk/substrate.py,sha256=AqfQNl5Qv_s6roOESSdwleX0-VGyu5sh3m5-dYZp5ek,49079
19
19
  hippius_sdk/utils.py,sha256=rJ611yvwKSyiBpYU3w-SuyQxoghMGU-ePuslrPv5H5g,7388
20
- hippius-0.2.20.dist-info/METADATA,sha256=-mfl3cVtxZhaYbQvq-yt2aR96T1szZxnCb_R3Xbhx5I,30088
21
- hippius-0.2.20.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
22
- hippius-0.2.20.dist-info/entry_points.txt,sha256=bFAZjW3vndretf9-8s587jA2ebMVI7puhn_lVs8jPc8,149
23
- hippius-0.2.20.dist-info/RECORD,,
20
+ hippius-0.2.21.dist-info/METADATA,sha256=iBEURRYrrokbmCufUVrac5PlnWErQ72on4cycxevlqU,30088
21
+ hippius-0.2.21.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
22
+ hippius-0.2.21.dist-info/entry_points.txt,sha256=bFAZjW3vndretf9-8s587jA2ebMVI7puhn_lVs8jPc8,149
23
+ hippius-0.2.21.dist-info/RECORD,,
hippius_sdk/__init__.py CHANGED
@@ -26,7 +26,7 @@ from hippius_sdk.config import (
26
26
  from hippius_sdk.ipfs import IPFSClient, S3PublishResult, S3DownloadResult
27
27
  from hippius_sdk.utils import format_cid, format_size, hex_to_ipfs_cid
28
28
 
29
- __version__ = "0.2.20"
29
+ __version__ = "0.2.21"
30
30
  __all__ = [
31
31
  "HippiusClient",
32
32
  "IPFSClient",
hippius_sdk/client.py CHANGED
@@ -512,15 +512,26 @@ class HippiusClient:
512
512
  )
513
513
 
514
514
  async def s3_publish(
515
- self, file_path: str, encrypt: bool, seed_phrase: str
515
+ self,
516
+ file_path: str,
517
+ encrypt: bool,
518
+ seed_phrase: str,
519
+ store_node: str = "http://localhost:5001",
520
+ pin_node: str = "https://store.hippius.network"
516
521
  ) -> S3PublishResult:
517
522
  """
518
523
  Publish a file to IPFS and the Hippius marketplace in one operation.
519
524
 
525
+ Uses a two-node architecture for optimal performance:
526
+ 1. Uploads to store_node (local) for immediate availability
527
+ 2. Pins to pin_node (remote) for persistence and backup
528
+
520
529
  Args:
521
530
  file_path: Path to the file to publish
522
531
  encrypt: Whether to encrypt the file before uploading
523
532
  seed_phrase: Seed phrase for blockchain transaction signing
533
+ store_node: IPFS node URL for initial upload (default: local node)
534
+ pin_node: IPFS node URL for backup pinning (default: remote service)
524
535
 
525
536
  Returns:
526
537
  S3PublishResult: Object containing CID, file info, and transaction hash
@@ -531,16 +542,22 @@ class HippiusClient:
531
542
  FileNotFoundError: If the file doesn't exist
532
543
  ValueError: If encryption is requested but not available
533
544
  """
534
- return await self.ipfs_client.s3_publish(file_path, encrypt, seed_phrase)
545
+ return await self.ipfs_client.s3_publish(file_path, encrypt, seed_phrase, store_node, pin_node)
535
546
 
536
547
  async def s3_download(
537
- self, cid: str, output_path: str, seed_phrase: str, auto_decrypt: bool = True
548
+ self,
549
+ cid: str,
550
+ output_path: str,
551
+ seed_phrase: str,
552
+ auto_decrypt: bool = True,
553
+ download_node: str = "http://localhost:5001"
538
554
  ) -> S3DownloadResult:
539
555
  """
540
556
  Download a file from IPFS with automatic decryption.
541
557
 
542
- This method automatically manages decryption keys per seed phrase:
543
- - Downloads the file from IPFS
558
+ This method uses the download_node for immediate availability and automatically
559
+ manages decryption keys per seed phrase:
560
+ - Downloads the file from the specified download_node (local by default)
544
561
  - If auto_decrypt=True, attempts to decrypt using stored keys for the seed phrase
545
562
  - Falls back to client encryption key if key storage is not available
546
563
  - Returns the file in decrypted form if decryption succeeds
@@ -550,6 +567,7 @@ class HippiusClient:
550
567
  output_path: Path where the downloaded file will be saved
551
568
  seed_phrase: Seed phrase to use for retrieving decryption keys
552
569
  auto_decrypt: Whether to attempt automatic decryption (default: True)
570
+ download_node: IPFS node URL for download (default: local node)
553
571
 
554
572
  Returns:
555
573
  S3DownloadResult: Object containing download info and decryption status
@@ -559,4 +577,4 @@ class HippiusClient:
559
577
  FileNotFoundError: If the output directory doesn't exist
560
578
  ValueError: If decryption fails
561
579
  """
562
- return await self.ipfs_client.s3_download(cid, output_path, seed_phrase, auto_decrypt)
580
+ return await self.ipfs_client.s3_download(cid, output_path, seed_phrase, auto_decrypt, download_node)
hippius_sdk/ipfs.py CHANGED
@@ -13,17 +13,18 @@ import tempfile
13
13
  import time
14
14
  import uuid
15
15
  from typing import Any, Callable, Dict, List, Optional
16
- from hippius_sdk.key_storage import (
17
- generate_and_store_key_for_seed,
18
- get_key_for_seed,
19
- is_key_storage_enabled,
20
- )
16
+
21
17
  import httpx
22
18
  from pydantic import BaseModel
23
19
 
24
20
  from hippius_sdk.config import get_config_value, get_encryption_key
25
21
  from hippius_sdk.errors import HippiusIPFSError, HippiusSubstrateError
26
22
  from hippius_sdk.ipfs_core import AsyncIPFSClient
23
+ from hippius_sdk.key_storage import (
24
+ generate_and_store_key_for_seed,
25
+ get_key_for_seed,
26
+ is_key_storage_enabled,
27
+ )
27
28
  from hippius_sdk.substrate import FileInput, SubstrateClient
28
29
  from hippius_sdk.utils import format_cid, format_size
29
30
 
@@ -1805,9 +1806,6 @@ class IPFSClient:
1805
1806
  """
1806
1807
  print(f"Starting deletion process for metadata CID: {metadata_cid}")
1807
1808
 
1808
- # Try to download and process metadata file and chunks
1809
- ipfs_failure = False
1810
- metadata_error = False
1811
1809
  chunks = []
1812
1810
 
1813
1811
  try:
@@ -1929,11 +1927,21 @@ class IPFSClient:
1929
1927
  return True
1930
1928
 
1931
1929
  async def s3_publish(
1932
- self, file_path: str, encrypt: bool, seed_phrase: str
1930
+ self,
1931
+ file_path: str,
1932
+ encrypt: bool,
1933
+ seed_phrase: str,
1934
+ store_node: str = "http://localhost:5001",
1935
+ pin_node: str = "https://store.hippius.network"
1933
1936
  ) -> S3PublishResult:
1934
1937
  """
1935
1938
  Publish a file to IPFS and the Hippius marketplace in one operation.
1936
1939
 
1940
+ This method uses a two-node architecture for optimal performance:
1941
+ 1. Uploads to store_node (local) for immediate availability
1942
+ 2. Pins to pin_node (remote) for persistence and backup
1943
+ 3. Publishes to substrate marketplace
1944
+
1937
1945
  This method automatically manages encryption keys per seed phrase:
1938
1946
  - If encrypt=True, it will get or generate an encryption key for the seed phrase
1939
1947
  - Keys are stored in PostgreSQL and versioned (never deleted)
@@ -1943,6 +1951,8 @@ class IPFSClient:
1943
1951
  file_path: Path to the file to publish
1944
1952
  encrypt: Whether to encrypt the file before uploading
1945
1953
  seed_phrase: Seed phrase for blockchain transaction signing
1954
+ store_node: IPFS node URL for initial upload (default: local node)
1955
+ pin_node: IPFS node URL for backup pinning (default: remote service)
1946
1956
 
1947
1957
  Returns:
1948
1958
  S3PublishResult: Object containing CID, file info, and transaction hash
@@ -2026,18 +2036,22 @@ class IPFSClient:
2026
2036
  else None
2027
2037
  )
2028
2038
 
2029
- # Add file to IPFS
2039
+ # Step 1: Upload to store_node (local) for immediate availability
2030
2040
  try:
2031
- result = await self.client.add_file(file_path)
2041
+ store_client = AsyncIPFSClient(api_url=store_node)
2042
+ result = await store_client.add_file(file_path)
2032
2043
  cid = result["Hash"]
2044
+ logger.info(f"File uploaded to store node {store_node} with CID: {cid}")
2033
2045
  except Exception as e:
2034
- raise HippiusIPFSError(f"Failed to add file to IPFS: {str(e)}")
2046
+ raise HippiusIPFSError(f"Failed to upload file to store node {store_node}: {str(e)}")
2035
2047
 
2036
- # Pin the file to IPFS
2048
+ # Step 2: Pin to pin_node (remote) for persistence and backup
2037
2049
  try:
2038
- pin_result = await self.client.pin(cid)
2050
+ pin_client = AsyncIPFSClient(api_url=pin_node)
2051
+ await pin_client.pin(cid)
2052
+ logger.info(f"File pinned to backup node {pin_node}")
2039
2053
  except Exception as e:
2040
- raise HippiusIPFSError(f"Failed to pin file to IPFS: {str(e)}")
2054
+ raise HippiusIPFSError(f"Failed to pin file to store node {store_node}: {str(e)}")
2041
2055
 
2042
2056
  # Publish to substrate marketplace
2043
2057
  try:
@@ -2087,13 +2101,19 @@ class IPFSClient:
2087
2101
  )
2088
2102
 
2089
2103
  async def s3_download(
2090
- self, cid: str, output_path: str, seed_phrase: str, auto_decrypt: bool = True
2104
+ self,
2105
+ cid: str,
2106
+ output_path: str,
2107
+ seed_phrase: str,
2108
+ auto_decrypt: bool = True,
2109
+ download_node: str = "http://localhost:5001"
2091
2110
  ) -> S3DownloadResult:
2092
2111
  """
2093
2112
  Download a file from IPFS with automatic decryption.
2094
2113
 
2095
- This method automatically manages decryption keys per seed phrase:
2096
- - Downloads the file from IPFS
2114
+ This method uses the download_node for immediate availability and automatically
2115
+ manages decryption keys per seed phrase:
2116
+ - Downloads the file from the specified download_node (local by default)
2097
2117
  - If auto_decrypt=True, attempts to decrypt using stored keys for the seed phrase
2098
2118
  - Falls back to client encryption key if key storage is not available
2099
2119
  - Returns the file in decrypted form if decryption succeeds
@@ -2103,6 +2123,7 @@ class IPFSClient:
2103
2123
  output_path: Path where the downloaded file will be saved
2104
2124
  seed_phrase: Seed phrase to use for retrieving decryption keys
2105
2125
  auto_decrypt: Whether to attempt automatic decryption (default: True)
2126
+ download_node: IPFS node URL for download (default: local node)
2106
2127
 
2107
2128
  Returns:
2108
2129
  S3DownloadResult: Object containing download info and decryption status
@@ -2114,24 +2135,25 @@ class IPFSClient:
2114
2135
  """
2115
2136
  start_time = time.time()
2116
2137
 
2117
- # Download the file first using the existing download_file method
2138
+ # Download the file directly from the specified download_node
2118
2139
  try:
2119
2140
  # Create parent directories if they don't exist
2120
2141
  os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
2121
2142
 
2122
- # Download without decryption first
2123
- download_result = await self.download_file(
2124
- cid=cid,
2125
- output_path=output_path,
2126
- skip_directory_check=False,
2127
- seed_phrase=seed_phrase,
2128
- )
2143
+ download_client = AsyncIPFSClient(api_url=download_node)
2129
2144
 
2130
- if not download_result.get("success", False):
2131
- raise HippiusIPFSError("Download failed")
2145
+ download_url = f"{download_node.rstrip('/')}/api/v0/cat?arg={cid}"
2146
+ async with download_client.client.stream("POST", download_url) as response:
2147
+ response.raise_for_status()
2148
+
2149
+ with open(output_path, "wb") as f:
2150
+ async for chunk in response.aiter_bytes(chunk_size=8192):
2151
+ f.write(chunk)
2152
+
2153
+ logger.info(f"File downloaded from {download_node} with CID: {cid}")
2132
2154
 
2133
2155
  except Exception as e:
2134
- raise HippiusIPFSError(f"Failed to download file from IPFS: {str(e)}")
2156
+ raise HippiusIPFSError(f"Failed to download file from {download_node}: {str(e)}")
2135
2157
 
2136
2158
  # Get file info after download
2137
2159
  size_bytes = os.path.getsize(output_path)
@@ -2154,77 +2176,91 @@ class IPFSClient:
2154
2176
  with open(output_path, "rb") as f:
2155
2177
  file_data = f.read()
2156
2178
 
2157
- decryption_attempted = False
2158
- decryption_successful = False
2179
+ # Check if file is empty - this indicates a problem
2180
+ if len(file_data) == 0:
2181
+ logger.error(f"Downloaded file is empty (0 bytes) for CID: {cid}")
2182
+ raise HippiusIPFSError(
2183
+ f"File not available: Downloaded 0 bytes for CID {cid}. "
2184
+ f"File may not exist on download node {download_node}. "
2185
+ f"Download URL: {download_url}"
2186
+ )
2187
+ elif len(file_data) < 40: # PyNaCl encrypted data is at least 40 bytes (24-byte nonce + 16-byte auth tag + data)
2188
+ logger.info(f"File too small to be encrypted ({len(file_data)} bytes), treating as plaintext")
2189
+ decrypted = False
2190
+ encryption_key_used = None
2191
+ else:
2192
+ # File has content, attempt decryption if requested
2193
+ decryption_attempted = False
2194
+ decryption_successful = False
2159
2195
 
2160
- if key_storage_available:
2161
- # Try to get the encryption key for this seed phrase
2162
- try:
2163
- existing_key_b64 = await get_key_for_seed(seed_phrase)
2196
+ if key_storage_available:
2197
+ # Try to get the encryption key for this seed phrase
2198
+ try:
2199
+ existing_key_b64 = await get_key_for_seed(seed_phrase)
2164
2200
 
2165
- if existing_key_b64:
2166
- logger.debug("Found encryption key for seed phrase, attempting decryption")
2167
- decryption_attempted = True
2168
- encryption_key_used = existing_key_b64
2201
+ if existing_key_b64:
2202
+ logger.debug("Found encryption key for seed phrase, attempting decryption")
2203
+ decryption_attempted = True
2204
+ encryption_key_used = existing_key_b64
2169
2205
 
2170
- # Attempt decryption with the stored key
2171
- try:
2172
- import nacl.secret
2206
+ # Attempt decryption with the stored key
2207
+ try:
2208
+ import nacl.secret
2173
2209
 
2174
- encryption_key_bytes = base64.b64decode(existing_key_b64)
2175
- box = nacl.secret.SecretBox(encryption_key_bytes)
2176
- decrypted_data = box.decrypt(file_data)
2210
+ encryption_key_bytes = base64.b64decode(existing_key_b64)
2211
+ box = nacl.secret.SecretBox(encryption_key_bytes)
2212
+ decrypted_data = box.decrypt(file_data)
2177
2213
 
2178
- # Write the decrypted data back to the file
2179
- with open(output_path, "wb") as f:
2180
- f.write(decrypted_data)
2214
+ # Write the decrypted data back to the file
2215
+ with open(output_path, "wb") as f:
2216
+ f.write(decrypted_data)
2181
2217
 
2182
- decryption_successful = True
2183
- decrypted = True
2184
- size_bytes = len(decrypted_data) # Update size to decrypted size
2185
- logger.info("Successfully decrypted file using stored key")
2218
+ decryption_successful = True
2219
+ decrypted = True
2220
+ size_bytes = len(decrypted_data) # Update size to decrypted size
2221
+ logger.info("Successfully decrypted file using stored key")
2186
2222
 
2187
- except Exception as decrypt_error:
2188
- logger.debug(f"Decryption failed with stored key: {decrypt_error}")
2189
- # Continue to try fallback decryption
2190
- else:
2191
- logger.debug("No encryption key found for seed phrase")
2223
+ except Exception as decrypt_error:
2224
+ logger.debug(f"Decryption failed with stored key: {decrypt_error}")
2225
+ # Continue to try fallback decryption
2226
+ else:
2227
+ logger.debug("No encryption key found for seed phrase")
2192
2228
 
2193
- except Exception as e:
2194
- logger.debug(f"Error retrieving key from storage: {e}")
2229
+ except Exception as e:
2230
+ logger.debug(f"Error retrieving key from storage: {e}")
2195
2231
 
2196
- # If key storage decryption failed or wasn't available, try client encryption key
2197
- if not decryption_successful and self.encryption_available:
2198
- logger.debug("Attempting decryption with client encryption key")
2199
- decryption_attempted = True
2232
+ # If key storage decryption failed or wasn't available, try client encryption key
2233
+ if not decryption_successful and self.encryption_available:
2234
+ logger.debug("Attempting decryption with client encryption key")
2235
+ decryption_attempted = True
2200
2236
 
2201
- try:
2202
- decrypted_data = self.decrypt_data(file_data)
2203
-
2204
- # Write the decrypted data back to the file
2205
- with open(output_path, "wb") as f:
2206
- f.write(decrypted_data)
2207
-
2208
- decryption_successful = True
2209
- decrypted = True
2210
- size_bytes = len(decrypted_data) # Update size to decrypted size
2211
-
2212
- # Store the encryption key for the result
2213
- encryption_key_used = (
2214
- base64.b64encode(self.encryption_key).decode("utf-8")
2215
- if self.encryption_key
2216
- else None
2217
- )
2218
- logger.info("Successfully decrypted file using client encryption key")
2237
+ try:
2238
+ decrypted_data = self.decrypt_data(file_data)
2239
+
2240
+ # Write the decrypted data back to the file
2241
+ with open(output_path, "wb") as f:
2242
+ f.write(decrypted_data)
2243
+
2244
+ decryption_successful = True
2245
+ decrypted = True
2246
+ size_bytes = len(decrypted_data) # Update size to decrypted size
2247
+
2248
+ # Store the encryption key for the result
2249
+ encryption_key_used = (
2250
+ base64.b64encode(self.encryption_key).decode("utf-8")
2251
+ if self.encryption_key
2252
+ else None
2253
+ )
2254
+ logger.info("Successfully decrypted file using client encryption key")
2219
2255
 
2220
- except Exception as decrypt_error:
2221
- logger.debug(f"Decryption failed with client key: {decrypt_error}")
2256
+ except Exception as decrypt_error:
2257
+ logger.debug(f"Decryption failed with client key: {decrypt_error}")
2222
2258
 
2223
- # Log final decryption status
2224
- if decryption_attempted and not decryption_successful:
2225
- logger.info("File may not be encrypted or decryption keys don't match")
2226
- elif not decryption_attempted:
2227
- logger.debug("No decryption attempted - no keys available")
2259
+ # Log final decryption status
2260
+ if decryption_attempted and not decryption_successful:
2261
+ logger.info("File may not be encrypted or decryption keys don't match")
2262
+ elif not decryption_attempted:
2263
+ logger.debug("No decryption attempted - no keys available")
2228
2264
 
2229
2265
  return S3DownloadResult(
2230
2266
  cid=cid,
@@ -234,7 +234,7 @@ _default_storage = None
234
234
  def is_key_storage_enabled() -> bool:
235
235
  """
236
236
  Check if key storage is enabled and available.
237
-
237
+
238
238
  Returns True if:
239
239
  1. Explicitly enabled in config, OR
240
240
  2. asyncpg is available (key_storage extra installed) AND not explicitly disabled
@@ -243,11 +243,11 @@ def is_key_storage_enabled() -> bool:
243
243
  config_value = get_config_value("key_storage", "enabled", None)
244
244
  if config_value is False:
245
245
  return False
246
-
246
+
247
247
  # If explicitly enabled, return True
248
248
  if config_value is True:
249
249
  return True
250
-
250
+
251
251
  # If not set in config, auto-detect based on asyncpg availability
252
252
  # This allows users who install [key_storage] extra to use it without manual config
253
253
  return ASYNCPG_AVAILABLE