hippius 0.2.39__tar.gz → 0.2.41__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.
Files changed (23) hide show
  1. {hippius-0.2.39 → hippius-0.2.41}/PKG-INFO +1 -1
  2. {hippius-0.2.39 → hippius-0.2.41}/hippius_sdk/__init__.py +1 -1
  3. {hippius-0.2.39 → hippius-0.2.41}/hippius_sdk/client.py +47 -30
  4. {hippius-0.2.39 → hippius-0.2.41}/hippius_sdk/ipfs.py +174 -85
  5. {hippius-0.2.39 → hippius-0.2.41}/hippius_sdk/ipfs_core.py +9 -4
  6. {hippius-0.2.39 → hippius-0.2.41}/pyproject.toml +1 -1
  7. {hippius-0.2.39 → hippius-0.2.41}/README.md +0 -0
  8. {hippius-0.2.39 → hippius-0.2.41}/hippius_sdk/cli.py +0 -0
  9. {hippius-0.2.39 → hippius-0.2.41}/hippius_sdk/cli_assets.py +0 -0
  10. {hippius-0.2.39 → hippius-0.2.41}/hippius_sdk/cli_handlers.py +0 -0
  11. {hippius-0.2.39 → hippius-0.2.41}/hippius_sdk/cli_parser.py +0 -0
  12. {hippius-0.2.39 → hippius-0.2.41}/hippius_sdk/cli_rich.py +0 -0
  13. {hippius-0.2.39 → hippius-0.2.41}/hippius_sdk/config.py +0 -0
  14. {hippius-0.2.39 → hippius-0.2.41}/hippius_sdk/db/README.md +0 -0
  15. {hippius-0.2.39 → hippius-0.2.41}/hippius_sdk/db/env.db.template +0 -0
  16. {hippius-0.2.39 → hippius-0.2.41}/hippius_sdk/db/migrations/20241201000001_create_key_storage_tables.sql +0 -0
  17. {hippius-0.2.39 → hippius-0.2.41}/hippius_sdk/db/migrations/20241202000001_switch_to_subaccount_encryption.sql +0 -0
  18. {hippius-0.2.39 → hippius-0.2.41}/hippius_sdk/db/setup_database.sh +0 -0
  19. {hippius-0.2.39 → hippius-0.2.41}/hippius_sdk/db_utils.py +0 -0
  20. {hippius-0.2.39 → hippius-0.2.41}/hippius_sdk/errors.py +0 -0
  21. {hippius-0.2.39 → hippius-0.2.41}/hippius_sdk/key_storage.py +0 -0
  22. {hippius-0.2.39 → hippius-0.2.41}/hippius_sdk/substrate.py +0 -0
  23. {hippius-0.2.39 → hippius-0.2.41}/hippius_sdk/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hippius
3
- Version: 0.2.39
3
+ Version: 0.2.41
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, S3PublishResult, S3DownloadResult
27
27
  from hippius_sdk.utils import format_cid, format_size, hex_to_ipfs_cid
28
28
 
29
- __version__ = "0.2.39"
29
+ __version__ = "0.2.41"
30
30
  __all__ = [
31
31
  "HippiusClient",
32
32
  "IPFSClient",
@@ -3,7 +3,8 @@ Main client for the Hippius SDK.
3
3
  """
4
4
 
5
5
  import base64
6
- from typing import Any, Callable, Dict, List, Optional, Union
6
+ import os
7
+ from typing import Any, Callable, Dict, List, Optional, Union, AsyncIterator
7
8
 
8
9
  import nacl.secret
9
10
  import nacl.utils
@@ -118,7 +119,9 @@ class HippiusClient:
118
119
  """
119
120
  # Use the enhanced IPFSClient method directly with encryption parameter
120
121
  return await self.ipfs_client.upload_file(
121
- file_path, encrypt=encrypt, seed_phrase=seed_phrase
122
+ file_path,
123
+ encrypt=encrypt,
124
+ seed_phrase=seed_phrase,
122
125
  )
123
126
 
124
127
  async def upload_directory(
@@ -517,25 +520,27 @@ class HippiusClient:
517
520
 
518
521
  async def s3_publish(
519
522
  self,
520
- file_path: str,
523
+ content: Union[str, bytes, os.PathLike],
521
524
  encrypt: bool,
522
525
  seed_phrase: str,
523
526
  subaccount_id: str,
524
527
  bucket_name: str,
528
+ file_name: str = None,
525
529
  store_node: str = "http://localhost:5001",
526
530
  pin_node: str = "https://store.hippius.network",
527
531
  substrate_url: str = "wss://rpc.hippius.network",
528
532
  publish: bool = True,
529
533
  ) -> Union[S3PublishResult, S3PublishPin]:
530
534
  """
531
- Publish a file to IPFS and the Hippius marketplace in one operation.
535
+ Publish content to IPFS and the Hippius marketplace in one operation.
532
536
 
533
537
  Uses a two-node architecture for optimal performance:
534
538
  1. Uploads to store_node (local) for immediate availability
535
539
  2. Pins to pin_node (remote) for persistence and backup
536
540
 
537
541
  Args:
538
- file_path: Path to the file to publish
542
+ content: Either a file path (str/PathLike) or bytes content to publish
543
+ file_name: The original file name (required if content is bytes)
539
544
  encrypt: Whether to encrypt the file before uploading
540
545
  seed_phrase: Seed phrase for blockchain transaction signing
541
546
  subaccount_id: The subaccount/account identifier
@@ -556,46 +561,51 @@ class HippiusClient:
556
561
  ValueError: If encryption is requested but not available
557
562
  """
558
563
  return await self.ipfs_client.s3_publish(
559
- file_path,
560
- encrypt,
561
- seed_phrase,
562
- subaccount_id,
563
- bucket_name,
564
- store_node,
565
- pin_node,
566
- substrate_url,
567
- publish,
564
+ content=content,
565
+ encrypt=encrypt,
566
+ seed_phrase=seed_phrase,
567
+ subaccount_id=subaccount_id,
568
+ bucket_name=bucket_name,
569
+ store_node=store_node,
570
+ pin_node=pin_node,
571
+ substrate_url=substrate_url,
572
+ publish=publish,
573
+ file_name=file_name,
568
574
  )
569
575
 
570
576
  async def s3_download(
571
577
  self,
572
578
  cid: str,
573
- output_path: str,
574
- subaccount_id: str,
575
- bucket_name: str,
579
+ output_path: Optional[str] = None,
580
+ subaccount_id: Optional[str] = None,
581
+ bucket_name: Optional[str] = None,
576
582
  auto_decrypt: bool = True,
577
583
  download_node: str = "http://localhost:5001",
578
- ) -> S3DownloadResult:
584
+ return_bytes: bool = False,
585
+ streaming: bool = False,
586
+ ) -> Union[S3DownloadResult, bytes, AsyncIterator[bytes]]:
579
587
  """
580
- Download a file from IPFS with automatic decryption.
588
+ Download content from IPFS with flexible output options and automatic decryption.
581
589
 
582
- This method uses the download_node for immediate availability and automatically
583
- manages decryption keys per account+bucket combination:
584
- - Downloads the file from the specified download_node (local by default)
585
- - If auto_decrypt=True, attempts to decrypt using stored keys for the account+bucket
586
- - Falls back to client encryption key if key storage is not available
587
- - Returns the file in decrypted form if decryption succeeds
590
+ This method provides multiple output modes:
591
+ 1. File output: Downloads to specified path (default mode)
592
+ 2. Bytes output: Returns decrypted bytes in memory (return_bytes=True)
593
+ 3. Streaming output: Returns raw streaming iterator from IPFS node (streaming=True)
588
594
 
589
595
  Args:
590
596
  cid: Content Identifier (CID) of the file to download
591
- output_path: Path where the downloaded file will be saved
592
- subaccount_id: The subaccount/account identifier
593
- bucket_name: The bucket name for key isolation
597
+ output_path: Path where the downloaded file will be saved (None for bytes/streaming)
598
+ subaccount_id: The subaccount/account identifier (required for decryption)
599
+ bucket_name: The bucket name for key isolation (required for decryption)
594
600
  auto_decrypt: Whether to attempt automatic decryption (default: True)
595
601
  download_node: IPFS node URL for download (default: local node)
602
+ return_bytes: If True, return bytes instead of saving to file
603
+ streaming: If True, return raw streaming iterator from IPFS (no decryption)
596
604
 
597
605
  Returns:
598
- S3DownloadResult: Object containing download info and decryption status
606
+ S3DownloadResult: Download info and decryption status (default)
607
+ bytes: Raw decrypted content when return_bytes=True
608
+ AsyncIterator[bytes]: Raw streaming iterator when streaming=True
599
609
 
600
610
  Raises:
601
611
  HippiusIPFSError: If IPFS download fails
@@ -603,5 +613,12 @@ class HippiusClient:
603
613
  ValueError: If decryption fails
604
614
  """
605
615
  return await self.ipfs_client.s3_download(
606
- cid, output_path, subaccount_id, bucket_name, auto_decrypt, download_node
616
+ cid=cid,
617
+ output_path=output_path,
618
+ subaccount_id=subaccount_id,
619
+ bucket_name=bucket_name,
620
+ auto_decrypt=auto_decrypt,
621
+ download_node=download_node,
622
+ return_bytes=return_bytes,
623
+ streaming=streaming,
607
624
  )
@@ -1,6 +1,7 @@
1
1
  """
2
2
  IPFS operations for the Hippius SDK.
3
3
  """
4
+
4
5
  import asyncio
5
6
  import base64
6
7
  import hashlib
@@ -12,7 +13,7 @@ import shutil
12
13
  import tempfile
13
14
  import time
14
15
  import uuid
15
- from typing import Any, Callable, Dict, List, Optional, Union
16
+ from typing import Any, Callable, Dict, List, Optional, Union, AsyncIterator
16
17
 
17
18
  import httpx
18
19
  from pydantic import BaseModel
@@ -129,7 +130,7 @@ class IPFSClient:
129
130
 
130
131
  try:
131
132
  self.client = AsyncIPFSClient(api_url=api_url, gateway=self.gateway)
132
- except httpx.ConnectError as e:
133
+ except httpx.ConnectError:
133
134
  print(
134
135
  f"Warning: Falling back to local IPFS daemon, but still using gateway={self.gateway}"
135
136
  )
@@ -241,6 +242,7 @@ class IPFSClient:
241
242
  encrypt: Optional[bool] = None,
242
243
  max_retries: int = 3,
243
244
  seed_phrase: Optional[str] = None,
245
+ file_name: str = None,
244
246
  ) -> Dict[str, Any]:
245
247
  """
246
248
  Upload a file to IPFS with optional encryption.
@@ -251,6 +253,7 @@ class IPFSClient:
251
253
  encrypt: Whether to encrypt the file (overrides default)
252
254
  max_retries: Maximum number of retry attempts (default: 3)
253
255
  seed_phrase: Optional seed phrase to use for blockchain interactions (uses config if None)
256
+ file_name: The original file name
254
257
 
255
258
  Returns:
256
259
  Dict[str, Any]: Dictionary containing:
@@ -278,7 +281,7 @@ class IPFSClient:
278
281
  )
279
282
 
280
283
  # Get file info before upload
281
- filename = os.path.basename(file_path)
284
+ file_name = file_name if file_name else os.path.basename(file_path)
282
285
  size_bytes = os.path.getsize(file_path)
283
286
 
284
287
  # If encryption is requested, encrypt the file first
@@ -303,7 +306,10 @@ class IPFSClient:
303
306
  # Use the original file for upload
304
307
  upload_path = file_path
305
308
 
306
- result = await self.client.add_file(upload_path)
309
+ result = await self.client.add_file(
310
+ upload_path,
311
+ file_name=file_name,
312
+ )
307
313
  cid = result["Hash"]
308
314
 
309
315
  finally:
@@ -314,7 +320,7 @@ class IPFSClient:
314
320
  # Format the result
315
321
  result = {
316
322
  "cid": cid,
317
- "filename": filename,
323
+ "filename": file_name,
318
324
  "size_bytes": size_bytes,
319
325
  "encrypted": should_encrypt,
320
326
  }
@@ -1411,7 +1417,7 @@ class IPFSClient:
1411
1417
 
1412
1418
  for i, chunk in enumerate(reconstructed_chunks):
1413
1419
  if verbose and i % 10 == 0:
1414
- print(f"Processing chunk {i+1}/{len(reconstructed_chunks)}...")
1420
+ print(f"Processing chunk {i + 1}/{len(reconstructed_chunks)}...")
1415
1421
 
1416
1422
  # For all chunks except the last one, use full chunk size
1417
1423
  if i < len(reconstructed_chunks) - 1:
@@ -1750,8 +1756,9 @@ class IPFSClient:
1750
1756
  # Record the child files that were processed
1751
1757
  result["child_files"] = child_files
1752
1758
  except Exception as e:
1753
- print(f"Warning: Failed to check if CID is a directory: {e}")
1754
- # Continue with regular file unpin
1759
+ print(
1760
+ f"Warning: Failed to check if CID is a directory: {e}"
1761
+ ) # Continue with regular file unpin
1755
1762
 
1756
1763
  # Now unpin the main file/directory
1757
1764
  if unpin:
@@ -1821,7 +1828,8 @@ class IPFSClient:
1821
1828
  cancel_from_blockchain: bool = True,
1822
1829
  parallel_limit: int = 20,
1823
1830
  seed_phrase: Optional[str] = None,
1824
- metadata_timeout: int = 30, # Timeout in seconds for metadata fetch
1831
+ metadata_timeout: int = 30,
1832
+ # Timeout in seconds for metadata fetch
1825
1833
  ) -> bool:
1826
1834
  """
1827
1835
  Delete an erasure-coded file, including all its chunks in parallel.
@@ -1863,8 +1871,7 @@ class IPFSClient:
1863
1871
  except asyncio.TimeoutError:
1864
1872
  print(
1865
1873
  f"Timed out after {metadata_timeout}s waiting for metadata download"
1866
- )
1867
- # We'll continue with blockchain cancellation even without metadata
1874
+ ) # We'll continue with blockchain cancellation even without metadata
1868
1875
  except json.JSONDecodeError as e:
1869
1876
  # If we can't parse the metadata JSON, record the error but continue
1870
1877
  print(f"Error parsing metadata JSON: {e}")
@@ -1960,7 +1967,7 @@ class IPFSClient:
1960
1967
 
1961
1968
  async def s3_publish(
1962
1969
  self,
1963
- file_path: str,
1970
+ content: Union[str, bytes, os.PathLike],
1964
1971
  encrypt: bool,
1965
1972
  seed_phrase: str,
1966
1973
  subaccount_id: str,
@@ -1969,9 +1976,10 @@ class IPFSClient:
1969
1976
  pin_node: str = "https://store.hippius.network",
1970
1977
  substrate_url: str = "wss://rpc.hippius.network",
1971
1978
  publish: bool = True,
1979
+ file_name: str = None,
1972
1980
  ) -> Union[S3PublishResult, S3PublishPin]:
1973
1981
  """
1974
- Publish a file to IPFS and the Hippius marketplace in one operation.
1982
+ Publish content to IPFS and the Hippius marketplace in one operation.
1975
1983
 
1976
1984
  This method uses a two-node architecture for optimal performance:
1977
1985
  1. Uploads to store_node (local) for immediate availability
@@ -1984,8 +1992,8 @@ class IPFSClient:
1984
1992
  - Always uses the most recent key for an account+bucket combination
1985
1993
 
1986
1994
  Args:
1987
- file_path: Path to the file to publish
1988
- encrypt: Whether to encrypt the file before uploading
1995
+ content: Either a file path (str/PathLike) or bytes content to publish
1996
+ encrypt: Whether to encrypt the content before uploading
1989
1997
  seed_phrase: Seed phrase for blockchain transaction signing
1990
1998
  subaccount_id: The subaccount/account identifier
1991
1999
  bucket_name: The bucket name for key isolation
@@ -1993,24 +2001,47 @@ class IPFSClient:
1993
2001
  pin_node: IPFS node URL for backup pinning (default: remote service)
1994
2002
  substrate_url: the substrate url to connect to for the storage request
1995
2003
  publish: Whether to publish to blockchain (True) or just upload to IPFS (False)
2004
+ file_name: The original file name (required if content is bytes)
1996
2005
 
1997
2006
  Returns:
1998
2007
  S3PublishResult: Object containing CID, file info, and transaction hash when publish=True
1999
- S3PublishPin: Object containing CID, subaccount, file_path, pin_node, substrate_url when publish=False
2008
+ S3PublishPin: Object containing CID, subaccount, content info, pin_node, substrate_url when publish=False
2000
2009
 
2001
2010
  Raises:
2002
2011
  HippiusIPFSError: If IPFS operations (add or pin) fail
2003
2012
  HippiusSubstrateError: If substrate call fails
2004
- FileNotFoundError: If the file doesn't exist
2005
- ValueError: If encryption is requested but not available
2013
+ FileNotFoundError: If the file doesn't exist (when content is a path)
2014
+ ValueError: If encryption is requested but not available, or if file_name is missing for bytes content
2006
2015
  """
2007
- # Check if file exists and get initial info
2008
- if not os.path.exists(file_path):
2009
- raise FileNotFoundError(f"File {file_path} not found")
2016
+ # Determine if content is a file path or bytes
2017
+ is_file_path = isinstance(content, (str, os.PathLike))
2018
+
2019
+ if is_file_path:
2020
+ # Handle file path input
2021
+ file_path = str(content)
2022
+ if not os.path.exists(file_path):
2023
+ raise FileNotFoundError(f"File {file_path} not found")
2024
+
2025
+ # Get file info
2026
+ filename = file_name or os.path.basename(file_path)
2027
+ size_bytes = os.path.getsize(file_path)
2028
+
2029
+ # Read file content into memory
2030
+ with open(file_path, "rb") as f:
2031
+ content_bytes = f.read()
2032
+ else:
2033
+ # Handle bytes input
2034
+ if not isinstance(content, bytes):
2035
+ raise ValueError(
2036
+ f"Content must be str, PathLike, or bytes, got {type(content)}"
2037
+ )
2010
2038
 
2011
- # Get file info
2012
- filename = os.path.basename(file_path)
2013
- size_bytes = os.path.getsize(file_path)
2039
+ if not file_name:
2040
+ raise ValueError("file_name is required when content is bytes")
2041
+
2042
+ filename = file_name
2043
+ content_bytes = content
2044
+ size_bytes = len(content_bytes)
2014
2045
 
2015
2046
  # Handle encryption if requested with automatic key management
2016
2047
  encryption_key_used = None
@@ -2048,19 +2079,11 @@ class IPFSClient:
2048
2079
  encryption_key_bytes = base64.b64decode(new_key_b64)
2049
2080
  encryption_key_used = new_key_b64
2050
2081
 
2051
- # Read file content into memory
2052
- with open(file_path, "rb") as f:
2053
- file_data = f.read()
2054
-
2055
2082
  # Encrypt the data using the key from key storage
2056
2083
  import nacl.secret
2057
2084
 
2058
2085
  box = nacl.secret.SecretBox(encryption_key_bytes)
2059
- encrypted_data = box.encrypt(file_data)
2060
-
2061
- # Overwrite the original file with encrypted data
2062
- with open(file_path, "wb") as f:
2063
- f.write(encrypted_data)
2086
+ content_bytes = box.encrypt(content_bytes)
2064
2087
  else:
2065
2088
  # Fallback to the original encryption system if key_storage is not available
2066
2089
  if not self.encryption_available:
@@ -2068,16 +2091,8 @@ class IPFSClient:
2068
2091
  "Encryption requested but not available. Either install key storage with 'pip install hippius_sdk[key_storage]' or configure an encryption key with 'hippius keygen --save'"
2069
2092
  )
2070
2093
 
2071
- # Read file content into memory
2072
- with open(file_path, "rb") as f:
2073
- file_data = f.read()
2074
-
2075
2094
  # Encrypt the data using the client's encryption key
2076
- encrypted_data = self.encrypt_data(file_data)
2077
-
2078
- # Overwrite the original file with encrypted data
2079
- with open(file_path, "wb") as f:
2080
- f.write(encrypted_data)
2095
+ content_bytes = self.encrypt_data(content_bytes)
2081
2096
 
2082
2097
  # Store the encryption key for the result
2083
2098
  encryption_key_used = (
@@ -2089,12 +2104,15 @@ class IPFSClient:
2089
2104
  # Step 1: Upload to store_node (local) for immediate availability
2090
2105
  try:
2091
2106
  store_client = AsyncIPFSClient(api_url=store_node)
2092
- result = await store_client.add_file(file_path)
2107
+ result = await store_client.add_bytes(
2108
+ content_bytes,
2109
+ filename=filename,
2110
+ )
2093
2111
  cid = result["Hash"]
2094
- logger.info(f"File uploaded to store node {store_node} with CID: {cid}")
2112
+ logger.info(f"Content uploaded to store node {store_node} with CID: {cid}")
2095
2113
  except Exception as e:
2096
2114
  raise HippiusIPFSError(
2097
- f"Failed to upload file to store node {store_node}: {str(e)}"
2115
+ f"Failed to upload content to store node {store_node}: {str(e)}"
2098
2116
  )
2099
2117
 
2100
2118
  # Step 2: Pin to pin_node (remote) for persistence and backup
@@ -2104,7 +2122,7 @@ class IPFSClient:
2104
2122
  logger.info(f"File pinned to backup node {pin_node}")
2105
2123
  except Exception as e:
2106
2124
  raise HippiusIPFSError(
2107
- f"Failed to pin file to store node {store_node}: {str(e)}"
2125
+ f"Failed to pin content to pin node {pin_node}: {str(e)}"
2108
2126
  )
2109
2127
 
2110
2128
  # Conditionally publish to substrate marketplace based on publish flag
@@ -2112,7 +2130,8 @@ class IPFSClient:
2112
2130
  try:
2113
2131
  # Pass the seed phrase directly to avoid password prompts for encrypted config
2114
2132
  substrate_client = SubstrateClient(
2115
- seed_phrase=seed_phrase, url=substrate_url
2133
+ seed_phrase=seed_phrase,
2134
+ url=substrate_url,
2116
2135
  )
2117
2136
  logger.info(
2118
2137
  f"Submitting storage request to substrate for file: {filename}, CID: {cid}"
@@ -2161,7 +2180,7 @@ class IPFSClient:
2161
2180
  return S3PublishPin(
2162
2181
  cid=cid,
2163
2182
  subaccount=subaccount_id,
2164
- file_path=file_path,
2183
+ file_path=filename,
2165
2184
  pin_node=pin_node,
2166
2185
  substrate_url=substrate_url,
2167
2186
  )
@@ -2169,49 +2188,80 @@ class IPFSClient:
2169
2188
  async def s3_download(
2170
2189
  self,
2171
2190
  cid: str,
2172
- output_path: str,
2173
- subaccount_id: str,
2174
- bucket_name: str,
2191
+ output_path: Optional[str] = None,
2192
+ subaccount_id: Optional[str] = None,
2193
+ bucket_name: Optional[str] = None,
2175
2194
  auto_decrypt: bool = True,
2176
2195
  download_node: str = "http://localhost:5001",
2177
- ) -> S3DownloadResult:
2196
+ return_bytes: bool = False,
2197
+ streaming: bool = False,
2198
+ ) -> Union[S3DownloadResult, bytes, AsyncIterator[bytes]]:
2178
2199
  """
2179
- Download a file from IPFS with automatic decryption.
2200
+ Download content from IPFS with flexible output options and automatic decryption.
2180
2201
 
2181
- This method uses the download_node for immediate availability and automatically
2182
- manages decryption keys per account+bucket combination:
2183
- - Downloads the file from the specified download_node (local by default)
2184
- - If auto_decrypt=True, attempts to decrypt using stored keys for the account+bucket
2185
- - Falls back to client encryption key if key storage is not available
2186
- - Returns the file in decrypted form if decryption succeeds
2202
+ This method provides multiple output modes:
2203
+ 1. File output: Downloads to specified path (default mode)
2204
+ 2. Bytes output: Returns decrypted bytes in memory (return_bytes=True)
2205
+ 3. Streaming output: Returns raw streaming iterator from IPFS node (streaming=True)
2187
2206
 
2188
2207
  Args:
2189
2208
  cid: Content Identifier (CID) of the file to download
2190
- output_path: Path where the downloaded file will be saved
2191
- subaccount_id: The subaccount/account identifier
2192
- bucket_name: The bucket name for key isolation
2209
+ output_path: Path where the downloaded file will be saved (None for bytes/streaming)
2210
+ subaccount_id: The subaccount/account identifier (required for decryption)
2211
+ bucket_name: The bucket name for key isolation (required for decryption)
2193
2212
  auto_decrypt: Whether to attempt automatic decryption (default: True)
2194
2213
  download_node: IPFS node URL for download (default: local node)
2214
+ return_bytes: If True, return bytes instead of saving to file
2215
+ streaming: If True, return raw streaming iterator from IPFS (no decryption)
2195
2216
 
2196
2217
  Returns:
2197
- S3DownloadResult: Object containing download info and decryption status
2218
+ S3DownloadResult: Download info and decryption status (default)
2219
+ bytes: Raw decrypted content when return_bytes=True
2220
+ AsyncIterator[bytes]: Raw streaming iterator when streaming=True
2198
2221
 
2199
2222
  Raises:
2200
2223
  HippiusIPFSError: If IPFS download fails
2201
2224
  FileNotFoundError: If the output directory doesn't exist
2202
- ValueError: If decryption fails
2225
+ ValueError: If decryption fails or invalid parameter combinations
2203
2226
  """
2227
+ # Validate parameter combinations
2228
+ if streaming and return_bytes:
2229
+ raise ValueError("Cannot specify both streaming and return_bytes")
2230
+
2231
+ if streaming and (auto_decrypt or subaccount_id or bucket_name):
2232
+ logger.warning(
2233
+ "streaming=True ignores decryption parameters - returns raw encrypted stream"
2234
+ )
2235
+
2236
+ if streaming:
2237
+ # Return raw streaming iterator from IPFS node - no processing
2238
+ # _get_ipfs_stream is an async generator, return it directly
2239
+ async def streaming_wrapper():
2240
+ async for chunk in self._get_ipfs_stream(cid, download_node):
2241
+ yield chunk
2242
+ return streaming_wrapper()
2243
+
2204
2244
  start_time = time.time()
2205
2245
 
2246
+ # Validate required parameters for decryption
2247
+ if auto_decrypt and (not subaccount_id or not bucket_name):
2248
+ raise ValueError(
2249
+ "subaccount_id and bucket_name are required when auto_decrypt=True"
2250
+ )
2251
+
2252
+ if not return_bytes and not output_path:
2253
+ raise ValueError("output_path is required when not using return_bytes mode")
2254
+
2206
2255
  # Download the file directly into memory from the specified download_node
2207
2256
  try:
2208
- # Create parent directories if they don't exist
2209
- os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
2257
+ # Create parent directories if they don't exist (only for file output mode)
2258
+ if not return_bytes:
2259
+ os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
2210
2260
 
2211
2261
  download_client = AsyncIPFSClient(api_url=download_node)
2212
2262
 
2213
2263
  download_url = f"{download_node.rstrip('/')}/api/v0/cat?arg={cid}"
2214
-
2264
+
2215
2265
  # Download file into memory
2216
2266
  file_data = bytearray()
2217
2267
  async with download_client.client.stream("POST", download_url) as response:
@@ -2221,7 +2271,9 @@ class IPFSClient:
2221
2271
 
2222
2272
  # Convert to bytes for consistency
2223
2273
  file_data = bytes(file_data)
2224
- logger.info(f"File downloaded from {download_node} with CID: {cid} ({len(file_data)} bytes)")
2274
+ logger.info(
2275
+ f"File downloaded from {download_node} with CID: {cid} ({len(file_data)} bytes)"
2276
+ )
2225
2277
 
2226
2278
  except Exception as e:
2227
2279
  raise HippiusIPFSError(
@@ -2299,8 +2351,7 @@ class IPFSClient:
2299
2351
  except Exception as decrypt_error:
2300
2352
  logger.debug(
2301
2353
  f"Decryption failed with stored key: {decrypt_error}"
2302
- )
2303
- # Continue to try fallback decryption
2354
+ ) # Continue to try fallback decryption
2304
2355
  else:
2305
2356
  logger.debug(
2306
2357
  "No encryption key found for account+bucket combination"
@@ -2343,20 +2394,58 @@ class IPFSClient:
2343
2394
  elif not decryption_attempted:
2344
2395
  logger.debug("No decryption attempted - no keys available")
2345
2396
 
2346
- # Write the final data (encrypted or decrypted) to disk once
2347
- with open(output_path, "wb") as f:
2348
- f.write(final_data)
2397
+ # Handle output based on mode
2398
+ if return_bytes:
2399
+ # Return bytes directly
2400
+ return final_data
2401
+ else:
2402
+ # Write the final data (encrypted or decrypted) to disk once
2403
+ with open(output_path, "wb") as f:
2404
+ f.write(final_data)
2405
+
2406
+ # Get final file info
2407
+ size_bytes = len(final_data)
2408
+ elapsed_time = time.time() - start_time
2349
2409
 
2350
- # Get final file info
2351
- size_bytes = len(final_data)
2352
- elapsed_time = time.time() - start_time
2410
+ return S3DownloadResult(
2411
+ cid=cid,
2412
+ output_path=output_path,
2413
+ size_bytes=size_bytes,
2414
+ size_formatted=self.format_size(size_bytes),
2415
+ elapsed_seconds=round(elapsed_time, 2),
2416
+ decrypted=decrypted,
2417
+ encryption_key=encryption_key_used,
2418
+ )
2353
2419
 
2354
- return S3DownloadResult(
2355
- cid=cid,
2356
- output_path=output_path,
2357
- size_bytes=size_bytes,
2358
- size_formatted=self.format_size(size_bytes),
2359
- elapsed_seconds=round(elapsed_time, 2),
2360
- decrypted=decrypted,
2361
- encryption_key=encryption_key_used,
2362
- )
2420
+ async def _get_ipfs_stream(
2421
+ self, cid: str, download_node: str
2422
+ ) -> AsyncIterator[bytes]:
2423
+ """
2424
+ Get a raw streaming iterator from IPFS node.
2425
+
2426
+ This method returns the raw encrypted stream directly from the IPFS node
2427
+ without any processing, decryption, or temporary file operations.
2428
+
2429
+ Args:
2430
+ cid: Content Identifier (CID) of the file to stream
2431
+ download_node: IPFS node URL for download
2432
+
2433
+ Returns:
2434
+ AsyncIterator[bytes]: Raw streaming iterator from IPFS
2435
+
2436
+ Raises:
2437
+ HippiusIPFSError: If IPFS stream fails
2438
+ """
2439
+ try:
2440
+ download_client = AsyncIPFSClient(api_url=download_node)
2441
+ download_url = f"{download_node.rstrip('/')}/api/v0/cat?arg={cid}"
2442
+
2443
+ async with download_client.client.stream("POST", download_url) as response:
2444
+ response.raise_for_status()
2445
+ logger.info(f"Started streaming from {download_node} for CID: {cid}")
2446
+
2447
+ async for chunk in response.aiter_bytes(chunk_size=8192):
2448
+ yield chunk
2449
+
2450
+ except Exception as e:
2451
+ raise HippiusIPFSError(f"Failed to stream from {download_node}: {str(e)}")
@@ -48,11 +48,16 @@ class AsyncIPFSClient:
48
48
  async def __aexit__(self, exc_type, exc_val, exc_tb):
49
49
  await self.close()
50
50
 
51
- async def add_file(self, file_path: str) -> Dict[str, Any]:
51
+ async def add_file(
52
+ self,
53
+ file_path: str,
54
+ file_name: str = None,
55
+ ) -> Dict[str, Any]:
52
56
  """
53
57
  Add a file to IPFS.
54
58
 
55
59
  Args:
60
+ file_name: Name of the file
56
61
  file_path: Path to the file to add
57
62
 
58
63
  Returns:
@@ -60,9 +65,9 @@ class AsyncIPFSClient:
60
65
  """
61
66
  with open(file_path, "rb") as f:
62
67
  file_content = f.read()
63
- filename = os.path.basename(file_path)
64
- # Specify file with name and content type to ensure consistent handling
65
- files = {"file": (filename, file_content, "application/octet-stream")}
68
+ file_name = file_name if file_name else os.path.basename(file_path)
69
+ files = {"file": (file_name, file_content, "application/octet-stream")}
70
+
66
71
  # Explicitly set wrap-with-directory=false to prevent wrapping in directory
67
72
  response = await self.client.post(
68
73
  f"{self.api_url}/api/v0/add?wrap-with-directory=false&cid-version=1",
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "hippius"
3
- version = "0.2.39"
3
+ version = "0.2.41"
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