hippius 0.2.18__py3-none-any.whl → 0.2.20__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.18
3
+ Version: 0.2.20
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=y1hFaAKG77GAKjps2c3Og69ZCn1anXFIa65PlYYj9Lg,1392
1
+ hippius_sdk/__init__.py,sha256=9rPlh9Ob5xHhR9owW4IxihHaanA0vxuMrDidpW5l0Fw,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=W1aVyvgMXRXVqjwSmRl03kZKB96PHMPcpq3cj1equ-8,20179
7
+ hippius_sdk/client.py,sha256=k8K3__zDtWJhCbHPAPYu9FopSNXsrw1HpVAX6fV7pnI,21462
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=by45dH1c2IxzkfU7B9-ZRkI5gwEAxE0bUB9KUhszlL8,84460
15
+ hippius_sdk/ipfs.py,sha256=Bp-f4QjrPeLKy61SxNsmB_YWvVjcYVBJlTt4NN6mGWo,90907
16
16
  hippius_sdk/ipfs_core.py,sha256=eOOgLoyP9mvwndnCjldnTc7z94ImYCXY3nm7JU3e_Mo,12676
17
- hippius_sdk/key_storage.py,sha256=7q5SL3Ls6Ho0xEwYSBpdci7e-bBMJnUMz2j7ROEBSVc,8947
17
+ hippius_sdk/key_storage.py,sha256=-f4pEUvkczY_yw3lYJ_kM2iDDOAtKOgrexWxVgIo78w,9486
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.18.dist-info/METADATA,sha256=bR-7aqi5G5o34ajG969VMj_VL-vWPYQIznyrBEfKcBM,30088
21
- hippius-0.2.18.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
22
- hippius-0.2.18.dist-info/entry_points.txt,sha256=bFAZjW3vndretf9-8s587jA2ebMVI7puhn_lVs8jPc8,149
23
- hippius-0.2.18.dist-info/RECORD,,
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,,
hippius_sdk/__init__.py CHANGED
@@ -23,13 +23,15 @@ from hippius_sdk.config import (
23
23
  set_encryption_key,
24
24
  set_seed_phrase,
25
25
  )
26
- from hippius_sdk.ipfs import IPFSClient
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.18"
29
+ __version__ = "0.2.20"
30
30
  __all__ = [
31
31
  "HippiusClient",
32
32
  "IPFSClient",
33
+ "S3PublishResult",
34
+ "S3DownloadResult",
33
35
  "get_config_value",
34
36
  "set_config_value",
35
37
  "get_encryption_key",
hippius_sdk/client.py CHANGED
@@ -9,7 +9,7 @@ import nacl.secret
9
9
  import nacl.utils
10
10
 
11
11
  from hippius_sdk.config import get_config_value, get_encryption_key
12
- from hippius_sdk.ipfs import IPFSClient, S3PublishResult
12
+ from hippius_sdk.ipfs import IPFSClient, S3PublishResult, S3DownloadResult
13
13
  from hippius_sdk.substrate import SubstrateClient
14
14
 
15
15
 
@@ -532,3 +532,31 @@ class HippiusClient:
532
532
  ValueError: If encryption is requested but not available
533
533
  """
534
534
  return await self.ipfs_client.s3_publish(file_path, encrypt, seed_phrase)
535
+
536
+ async def s3_download(
537
+ self, cid: str, output_path: str, seed_phrase: str, auto_decrypt: bool = True
538
+ ) -> S3DownloadResult:
539
+ """
540
+ Download a file from IPFS with automatic decryption.
541
+
542
+ This method automatically manages decryption keys per seed phrase:
543
+ - Downloads the file from IPFS
544
+ - If auto_decrypt=True, attempts to decrypt using stored keys for the seed phrase
545
+ - Falls back to client encryption key if key storage is not available
546
+ - Returns the file in decrypted form if decryption succeeds
547
+
548
+ Args:
549
+ cid: Content Identifier (CID) of the file to download
550
+ output_path: Path where the downloaded file will be saved
551
+ seed_phrase: Seed phrase to use for retrieving decryption keys
552
+ auto_decrypt: Whether to attempt automatic decryption (default: True)
553
+
554
+ Returns:
555
+ S3DownloadResult: Object containing download info and decryption status
556
+
557
+ Raises:
558
+ HippiusIPFSError: If IPFS download fails
559
+ FileNotFoundError: If the output directory doesn't exist
560
+ ValueError: If decryption fails
561
+ """
562
+ return await self.ipfs_client.s3_download(cid, output_path, seed_phrase, auto_decrypt)
hippius_sdk/ipfs.py CHANGED
@@ -13,7 +13,11 @@ import tempfile
13
13
  import time
14
14
  import uuid
15
15
  from typing import Any, Callable, Dict, List, Optional
16
-
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
+ )
17
21
  import httpx
18
22
  from pydantic import BaseModel
19
23
 
@@ -57,6 +61,18 @@ class S3PublishResult(BaseModel):
57
61
  tx_hash: str
58
62
 
59
63
 
64
+ class S3DownloadResult(BaseModel):
65
+ """Result model for s3_download method."""
66
+
67
+ cid: str
68
+ output_path: str
69
+ size_bytes: int
70
+ size_formatted: str
71
+ elapsed_seconds: float
72
+ decrypted: bool
73
+ encryption_key: Optional[str]
74
+
75
+
60
76
  # Set up logger for this module
61
77
  logger = logging.getLogger(__name__)
62
78
 
@@ -1949,14 +1965,7 @@ class IPFSClient:
1949
1965
  encryption_key_used = None
1950
1966
  if encrypt:
1951
1967
  # Check if key storage is enabled and available
1952
- key_storage_available = False
1953
1968
  try:
1954
- from hippius_sdk.key_storage import (
1955
- generate_and_store_key_for_seed,
1956
- get_key_for_seed,
1957
- is_key_storage_enabled,
1958
- )
1959
-
1960
1969
  key_storage_available = is_key_storage_enabled()
1961
1970
  logger.debug(f"Key storage enabled: {key_storage_available}")
1962
1971
  except ImportError:
@@ -1964,9 +1973,6 @@ class IPFSClient:
1964
1973
  key_storage_available = False
1965
1974
 
1966
1975
  if key_storage_available:
1967
- # Use PostgreSQL-backed key storage
1968
- logger.info(f"Using PostgreSQL key storage for seed phrase")
1969
-
1970
1976
  # Try to get existing key for this seed phrase
1971
1977
  existing_key_b64 = await get_key_for_seed(seed_phrase)
1972
1978
 
@@ -2079,3 +2085,153 @@ class IPFSClient:
2079
2085
  encryption_key=encryption_key_used,
2080
2086
  tx_hash=tx_hash,
2081
2087
  )
2088
+
2089
+ async def s3_download(
2090
+ self, cid: str, output_path: str, seed_phrase: str, auto_decrypt: bool = True
2091
+ ) -> S3DownloadResult:
2092
+ """
2093
+ Download a file from IPFS with automatic decryption.
2094
+
2095
+ This method automatically manages decryption keys per seed phrase:
2096
+ - Downloads the file from IPFS
2097
+ - If auto_decrypt=True, attempts to decrypt using stored keys for the seed phrase
2098
+ - Falls back to client encryption key if key storage is not available
2099
+ - Returns the file in decrypted form if decryption succeeds
2100
+
2101
+ Args:
2102
+ cid: Content Identifier (CID) of the file to download
2103
+ output_path: Path where the downloaded file will be saved
2104
+ seed_phrase: Seed phrase to use for retrieving decryption keys
2105
+ auto_decrypt: Whether to attempt automatic decryption (default: True)
2106
+
2107
+ Returns:
2108
+ S3DownloadResult: Object containing download info and decryption status
2109
+
2110
+ Raises:
2111
+ HippiusIPFSError: If IPFS download fails
2112
+ FileNotFoundError: If the output directory doesn't exist
2113
+ ValueError: If decryption fails
2114
+ """
2115
+ start_time = time.time()
2116
+
2117
+ # Download the file first using the existing download_file method
2118
+ try:
2119
+ # Create parent directories if they don't exist
2120
+ os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
2121
+
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
+ )
2129
+
2130
+ if not download_result.get("success", False):
2131
+ raise HippiusIPFSError("Download failed")
2132
+
2133
+ except Exception as e:
2134
+ raise HippiusIPFSError(f"Failed to download file from IPFS: {str(e)}")
2135
+
2136
+ # Get file info after download
2137
+ size_bytes = os.path.getsize(output_path)
2138
+ elapsed_time = time.time() - start_time
2139
+
2140
+ # Attempt automatic decryption if requested
2141
+ decrypted = False
2142
+ encryption_key_used = None
2143
+
2144
+ if auto_decrypt:
2145
+ # Check if key storage is enabled and available
2146
+ try:
2147
+ key_storage_available = is_key_storage_enabled()
2148
+ logger.debug(f"Key storage enabled: {key_storage_available}")
2149
+ except ImportError:
2150
+ logger.debug("Key storage module not available")
2151
+ key_storage_available = False
2152
+
2153
+ # Read the downloaded file content
2154
+ with open(output_path, "rb") as f:
2155
+ file_data = f.read()
2156
+
2157
+ decryption_attempted = False
2158
+ decryption_successful = False
2159
+
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)
2164
+
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
2169
+
2170
+ # Attempt decryption with the stored key
2171
+ try:
2172
+ import nacl.secret
2173
+
2174
+ encryption_key_bytes = base64.b64decode(existing_key_b64)
2175
+ box = nacl.secret.SecretBox(encryption_key_bytes)
2176
+ decrypted_data = box.decrypt(file_data)
2177
+
2178
+ # Write the decrypted data back to the file
2179
+ with open(output_path, "wb") as f:
2180
+ f.write(decrypted_data)
2181
+
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")
2186
+
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")
2192
+
2193
+ except Exception as e:
2194
+ logger.debug(f"Error retrieving key from storage: {e}")
2195
+
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
2200
+
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")
2219
+
2220
+ except Exception as decrypt_error:
2221
+ logger.debug(f"Decryption failed with client key: {decrypt_error}")
2222
+
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")
2228
+
2229
+ return S3DownloadResult(
2230
+ cid=cid,
2231
+ output_path=output_path,
2232
+ size_bytes=size_bytes,
2233
+ size_formatted=self.format_size(size_bytes),
2234
+ elapsed_seconds=round(elapsed_time, 2),
2235
+ decrypted=decrypted,
2236
+ encryption_key=encryption_key_used,
2237
+ )
@@ -232,8 +232,25 @@ _default_storage = None
232
232
 
233
233
 
234
234
  def is_key_storage_enabled() -> bool:
235
- """Check if key storage is enabled in configuration."""
236
- return get_config_value("key_storage", "enabled", False)
235
+ """
236
+ Check if key storage is enabled and available.
237
+
238
+ Returns True if:
239
+ 1. Explicitly enabled in config, OR
240
+ 2. asyncpg is available (key_storage extra installed) AND not explicitly disabled
241
+ """
242
+ # Check if explicitly disabled
243
+ config_value = get_config_value("key_storage", "enabled", None)
244
+ if config_value is False:
245
+ return False
246
+
247
+ # If explicitly enabled, return True
248
+ if config_value is True:
249
+ return True
250
+
251
+ # If not set in config, auto-detect based on asyncpg availability
252
+ # This allows users who install [key_storage] extra to use it without manual config
253
+ return ASYNCPG_AVAILABLE
237
254
 
238
255
 
239
256
  def get_default_storage() -> KeyStorage: