hippius 0.2.40__py3-none-any.whl → 0.2.42__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.40
3
+ Version: 0.2.42
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=T73Sgflb4AIuVZqLNsFeHDwFziBsqy2lQktpNLezg8I,1474
1
+ hippius_sdk/__init__.py,sha256=69XR6atYPkJPYLkUd6NHE15X_4eNNnaKQAy9yIGA4dc,1474
2
2
  hippius_sdk/cli.py,sha256=aqKOYSBSWt7UhcpFt7wf9yIPJ3bznpsJ6ehOnuZ4usI,18235
3
3
  hippius_sdk/cli_assets.py,sha256=rjH3Z5A1CQr2d5CIAAAb0WMCjoZZlMWcdo0f93KqluE,635
4
4
  hippius_sdk/cli_handlers.py,sha256=HkZldE8ZDS6WHu8aSoeS_rYZ4kp3F-Kdzu-weY1c0vU,128258
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=FU6OWvuZjqlizXONixDjggpEEvnp4KkaG8DYvdxRiBY,23411
7
+ hippius_sdk/client.py,sha256=6W50r7-WcMyZNI1j3NOKMpcSMlB819AMKM9w06YqMx0,24302
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
@@ -13,12 +13,12 @@ hippius_sdk/db/migrations/20241202000001_switch_to_subaccount_encryption.sql,sha
13
13
  hippius_sdk/db/setup_database.sh,sha256=STp03qxkp2RmIVr6YZIcvQQm-_LLUOb6Jobh-52HWmg,3115
14
14
  hippius_sdk/db_utils.py,sha256=-x0rbN0as7Tn3PJPZBYCgreZe52FLH40ppA1TLxsg90,1851
15
15
  hippius_sdk/errors.py,sha256=LScJJmawVAx7aRzqqQguYSkf9iazSjEQEBNlD_GXZ6Y,1589
16
- hippius_sdk/ipfs.py,sha256=QG2BzLSK2dkiD_wVj04DPiVmaWVLn2hUBoRj32AvrBY,97252
16
+ hippius_sdk/ipfs.py,sha256=CpVSEKVrHrZAXrhJsCPPZcADSx2tpE_ktA1TLlMiUQc,103749
17
17
  hippius_sdk/ipfs_core.py,sha256=WG7bGLk-threOvmumizwh1dnd5zqbIkTXy1y-BRGayI,12789
18
18
  hippius_sdk/key_storage.py,sha256=SXFd6aGQw9MDLGX2vSBuAY7rdX-k5EvFm63z7_n-8yQ,8148
19
19
  hippius_sdk/substrate.py,sha256=4a7UIE4UqGcDW7luKTBgSDqfb2OIZusB39G1UiRs_YU,50158
20
20
  hippius_sdk/utils.py,sha256=rJ611yvwKSyiBpYU3w-SuyQxoghMGU-ePuslrPv5H5g,7388
21
- hippius-0.2.40.dist-info/METADATA,sha256=hxmHtVzaMN4l6YwrEeeP8WV20IWEVHa9bq6AkidvD0I,30088
22
- hippius-0.2.40.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
23
- hippius-0.2.40.dist-info/entry_points.txt,sha256=bFAZjW3vndretf9-8s587jA2ebMVI7puhn_lVs8jPc8,149
24
- hippius-0.2.40.dist-info/RECORD,,
21
+ hippius-0.2.42.dist-info/METADATA,sha256=kSv4QdslPlxwgECpO3_UAq8-CAQGhtK8JEZ-zi7xKgU,30088
22
+ hippius-0.2.42.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
23
+ hippius-0.2.42.dist-info/entry_points.txt,sha256=bFAZjW3vndretf9-8s587jA2ebMVI7puhn_lVs8jPc8,149
24
+ hippius-0.2.42.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.40"
29
+ __version__ = "0.2.42"
30
30
  __all__ = [
31
31
  "HippiusClient",
32
32
  "IPFSClient",
hippius_sdk/client.py CHANGED
@@ -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
@@ -519,7 +520,7 @@ class HippiusClient:
519
520
 
520
521
  async def s3_publish(
521
522
  self,
522
- file_path: str,
523
+ content: Union[str, bytes, os.PathLike],
523
524
  encrypt: bool,
524
525
  seed_phrase: str,
525
526
  subaccount_id: str,
@@ -531,15 +532,15 @@ class HippiusClient:
531
532
  publish: bool = True,
532
533
  ) -> Union[S3PublishResult, S3PublishPin]:
533
534
  """
534
- Publish a file to IPFS and the Hippius marketplace in one operation.
535
+ Publish content to IPFS and the Hippius marketplace in one operation.
535
536
 
536
537
  Uses a two-node architecture for optimal performance:
537
538
  1. Uploads to store_node (local) for immediate availability
538
539
  2. Pins to pin_node (remote) for persistence and backup
539
540
 
540
541
  Args:
541
- file_name: The original file name.
542
- 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)
543
544
  encrypt: Whether to encrypt the file before uploading
544
545
  seed_phrase: Seed phrase for blockchain transaction signing
545
546
  subaccount_id: The subaccount/account identifier
@@ -560,47 +561,51 @@ class HippiusClient:
560
561
  ValueError: If encryption is requested but not available
561
562
  """
562
563
  return await self.ipfs_client.s3_publish(
563
- file_path,
564
- encrypt,
565
- seed_phrase,
566
- subaccount_id,
567
- bucket_name,
568
- store_node,
569
- pin_node,
570
- substrate_url,
571
- 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,
572
573
  file_name=file_name,
573
574
  )
574
575
 
575
576
  async def s3_download(
576
577
  self,
577
578
  cid: str,
578
- output_path: str,
579
- subaccount_id: str,
580
- bucket_name: str,
579
+ output_path: Optional[str] = None,
580
+ subaccount_id: Optional[str] = None,
581
+ bucket_name: Optional[str] = None,
581
582
  auto_decrypt: bool = True,
582
583
  download_node: str = "http://localhost:5001",
583
- ) -> S3DownloadResult:
584
+ return_bytes: bool = False,
585
+ streaming: bool = False,
586
+ ) -> Union[S3DownloadResult, bytes, AsyncIterator[bytes]]:
584
587
  """
585
- Download a file from IPFS with automatic decryption.
588
+ Download content from IPFS with flexible output options and automatic decryption.
586
589
 
587
- This method uses the download_node for immediate availability and automatically
588
- manages decryption keys per account+bucket combination:
589
- - Downloads the file from the specified download_node (local by default)
590
- - If auto_decrypt=True, attempts to decrypt using stored keys for the account+bucket
591
- - Falls back to client encryption key if key storage is not available
592
- - 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)
593
594
 
594
595
  Args:
595
596
  cid: Content Identifier (CID) of the file to download
596
- output_path: Path where the downloaded file will be saved
597
- subaccount_id: The subaccount/account identifier
598
- 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)
599
600
  auto_decrypt: Whether to attempt automatic decryption (default: True)
600
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 decrypted bytes when auto_decrypt=True, or raw streaming iterator when auto_decrypt=False
601
604
 
602
605
  Returns:
603
- 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 or streaming=True with auto_decrypt=True
608
+ AsyncIterator[bytes]: Raw streaming iterator when streaming=True and auto_decrypt=False
604
609
 
605
610
  Raises:
606
611
  HippiusIPFSError: If IPFS download fails
@@ -608,5 +613,12 @@ class HippiusClient:
608
613
  ValueError: If decryption fails
609
614
  """
610
615
  return await self.ipfs_client.s3_download(
611
- 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,
612
624
  )
hippius_sdk/ipfs.py CHANGED
@@ -13,7 +13,7 @@ import shutil
13
13
  import tempfile
14
14
  import time
15
15
  import uuid
16
- from typing import Any, Callable, Dict, List, Optional, Union
16
+ from typing import Any, Callable, Dict, List, Optional, Union, AsyncIterator
17
17
 
18
18
  import httpx
19
19
  from pydantic import BaseModel
@@ -69,6 +69,7 @@ class S3PublishPin(BaseModel):
69
69
  cid: str
70
70
  subaccount: str
71
71
  file_path: str
72
+ file_name: str
72
73
  pin_node: str
73
74
  substrate_url: str
74
75
 
@@ -1967,7 +1968,7 @@ class IPFSClient:
1967
1968
 
1968
1969
  async def s3_publish(
1969
1970
  self,
1970
- file_path: str,
1971
+ content: Union[str, bytes, os.PathLike],
1971
1972
  encrypt: bool,
1972
1973
  seed_phrase: str,
1973
1974
  subaccount_id: str,
@@ -1979,7 +1980,7 @@ class IPFSClient:
1979
1980
  file_name: str = None,
1980
1981
  ) -> Union[S3PublishResult, S3PublishPin]:
1981
1982
  """
1982
- Publish a file to IPFS and the Hippius marketplace in one operation.
1983
+ Publish content to IPFS and the Hippius marketplace in one operation.
1983
1984
 
1984
1985
  This method uses a two-node architecture for optimal performance:
1985
1986
  1. Uploads to store_node (local) for immediate availability
@@ -1992,8 +1993,8 @@ class IPFSClient:
1992
1993
  - Always uses the most recent key for an account+bucket combination
1993
1994
 
1994
1995
  Args:
1995
- file_path: Path to the file to publish
1996
- encrypt: Whether to encrypt the file before uploading
1996
+ content: Either a file path (str/PathLike) or bytes content to publish
1997
+ encrypt: Whether to encrypt the content before uploading
1997
1998
  seed_phrase: Seed phrase for blockchain transaction signing
1998
1999
  subaccount_id: The subaccount/account identifier
1999
2000
  bucket_name: The bucket name for key isolation
@@ -2001,25 +2002,47 @@ class IPFSClient:
2001
2002
  pin_node: IPFS node URL for backup pinning (default: remote service)
2002
2003
  substrate_url: the substrate url to connect to for the storage request
2003
2004
  publish: Whether to publish to blockchain (True) or just upload to IPFS (False)
2004
- file_name: The original file name.
2005
+ file_name: The original file name (required if content is bytes)
2005
2006
 
2006
2007
  Returns:
2007
2008
  S3PublishResult: Object containing CID, file info, and transaction hash when publish=True
2008
- S3PublishPin: Object containing CID, subaccount, file_path, pin_node, substrate_url when publish=False
2009
+ S3PublishPin: Object containing CID, subaccount, content info, pin_node, substrate_url when publish=False
2009
2010
 
2010
2011
  Raises:
2011
2012
  HippiusIPFSError: If IPFS operations (add or pin) fail
2012
2013
  HippiusSubstrateError: If substrate call fails
2013
- FileNotFoundError: If the file doesn't exist
2014
- ValueError: If encryption is requested but not available
2014
+ FileNotFoundError: If the file doesn't exist (when content is a path)
2015
+ ValueError: If encryption is requested but not available, or if file_name is missing for bytes content
2015
2016
  """
2016
- # Check if file exists and get initial info
2017
- if not os.path.exists(file_path):
2018
- raise FileNotFoundError(f"File {file_path} not found")
2017
+ # Determine if content is a file path or bytes
2018
+ is_file_path = isinstance(content, (str, os.PathLike))
2019
+
2020
+ if is_file_path:
2021
+ # Handle file path input
2022
+ file_path = str(content)
2023
+ if not os.path.exists(file_path):
2024
+ raise FileNotFoundError(f"File {file_path} not found")
2025
+
2026
+ # Get file info
2027
+ filename = file_name or os.path.basename(file_path)
2028
+ size_bytes = os.path.getsize(file_path)
2029
+
2030
+ # Read file content into memory
2031
+ with open(file_path, "rb") as f:
2032
+ content_bytes = f.read()
2033
+ else:
2034
+ # Handle bytes input
2035
+ if not isinstance(content, bytes):
2036
+ raise ValueError(
2037
+ f"Content must be str, PathLike, or bytes, got {type(content)}"
2038
+ )
2019
2039
 
2020
- # Get file info
2021
- filename = os.path.basename(file_path)
2022
- size_bytes = os.path.getsize(file_path)
2040
+ if not file_name:
2041
+ raise ValueError("file_name is required when content is bytes")
2042
+
2043
+ filename = file_name
2044
+ content_bytes = content
2045
+ size_bytes = len(content_bytes)
2023
2046
 
2024
2047
  # Handle encryption if requested with automatic key management
2025
2048
  encryption_key_used = None
@@ -2057,19 +2080,11 @@ class IPFSClient:
2057
2080
  encryption_key_bytes = base64.b64decode(new_key_b64)
2058
2081
  encryption_key_used = new_key_b64
2059
2082
 
2060
- # Read file content into memory
2061
- with open(file_path, "rb") as f:
2062
- file_data = f.read()
2063
-
2064
2083
  # Encrypt the data using the key from key storage
2065
2084
  import nacl.secret
2066
2085
 
2067
2086
  box = nacl.secret.SecretBox(encryption_key_bytes)
2068
- encrypted_data = box.encrypt(file_data)
2069
-
2070
- # Overwrite the original file with encrypted data
2071
- with open(file_path, "wb") as f:
2072
- f.write(encrypted_data)
2087
+ content_bytes = box.encrypt(content_bytes)
2073
2088
  else:
2074
2089
  # Fallback to the original encryption system if key_storage is not available
2075
2090
  if not self.encryption_available:
@@ -2077,16 +2092,8 @@ class IPFSClient:
2077
2092
  "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'"
2078
2093
  )
2079
2094
 
2080
- # Read file content into memory
2081
- with open(file_path, "rb") as f:
2082
- file_data = f.read()
2083
-
2084
2095
  # Encrypt the data using the client's encryption key
2085
- encrypted_data = self.encrypt_data(file_data)
2086
-
2087
- # Overwrite the original file with encrypted data
2088
- with open(file_path, "wb") as f:
2089
- f.write(encrypted_data)
2096
+ content_bytes = self.encrypt_data(content_bytes)
2090
2097
 
2091
2098
  # Store the encryption key for the result
2092
2099
  encryption_key_used = (
@@ -2098,15 +2105,15 @@ class IPFSClient:
2098
2105
  # Step 1: Upload to store_node (local) for immediate availability
2099
2106
  try:
2100
2107
  store_client = AsyncIPFSClient(api_url=store_node)
2101
- result = await store_client.add_file(
2102
- file_path,
2103
- file_name=file_name,
2108
+ result = await store_client.add_bytes(
2109
+ content_bytes,
2110
+ filename=filename,
2104
2111
  )
2105
2112
  cid = result["Hash"]
2106
- logger.info(f"File uploaded to store node {store_node} with CID: {cid}")
2113
+ logger.info(f"Content uploaded to store node {store_node} with CID: {cid}")
2107
2114
  except Exception as e:
2108
2115
  raise HippiusIPFSError(
2109
- f"Failed to upload file to store node {store_node}: {str(e)}"
2116
+ f"Failed to upload content to store node {store_node}: {str(e)}"
2110
2117
  )
2111
2118
 
2112
2119
  # Step 2: Pin to pin_node (remote) for persistence and backup
@@ -2116,7 +2123,7 @@ class IPFSClient:
2116
2123
  logger.info(f"File pinned to backup node {pin_node}")
2117
2124
  except Exception as e:
2118
2125
  raise HippiusIPFSError(
2119
- f"Failed to pin file to store node {store_node}: {str(e)}"
2126
+ f"Failed to pin content to pin node {pin_node}: {str(e)}"
2120
2127
  )
2121
2128
 
2122
2129
  # Conditionally publish to substrate marketplace based on publish flag
@@ -2174,7 +2181,8 @@ class IPFSClient:
2174
2181
  return S3PublishPin(
2175
2182
  cid=cid,
2176
2183
  subaccount=subaccount_id,
2177
- file_path=file_path,
2184
+ file_path=filename,
2185
+ file_name=filename,
2178
2186
  pin_node=pin_node,
2179
2187
  substrate_url=substrate_url,
2180
2188
  )
@@ -2182,44 +2190,139 @@ class IPFSClient:
2182
2190
  async def s3_download(
2183
2191
  self,
2184
2192
  cid: str,
2185
- output_path: str,
2186
- subaccount_id: str,
2187
- bucket_name: str,
2193
+ output_path: Optional[str] = None,
2194
+ subaccount_id: Optional[str] = None,
2195
+ bucket_name: Optional[str] = None,
2188
2196
  auto_decrypt: bool = True,
2189
2197
  download_node: str = "http://localhost:5001",
2190
- ) -> S3DownloadResult:
2198
+ return_bytes: bool = False,
2199
+ streaming: bool = False,
2200
+ ) -> Union[S3DownloadResult, bytes, AsyncIterator[bytes]]:
2191
2201
  """
2192
- Download a file from IPFS with automatic decryption.
2202
+ Download content from IPFS with flexible output options and automatic decryption.
2193
2203
 
2194
- This method uses the download_node for immediate availability and automatically
2195
- manages decryption keys per account+bucket combination:
2196
- - Downloads the file from the specified download_node (local by default)
2197
- - If auto_decrypt=True, attempts to decrypt using stored keys for the account+bucket
2198
- - Falls back to client encryption key if key storage is not available
2199
- - Returns the file in decrypted form if decryption succeeds
2204
+ This method provides multiple output modes:
2205
+ 1. File output: Downloads to specified path (default mode)
2206
+ 2. Bytes output: Returns decrypted bytes in memory (return_bytes=True)
2207
+ 3. Streaming output: Returns decrypted bytes or raw iterator based on auto_decrypt (streaming=True)
2200
2208
 
2201
2209
  Args:
2202
2210
  cid: Content Identifier (CID) of the file to download
2203
- output_path: Path where the downloaded file will be saved
2204
- subaccount_id: The subaccount/account identifier
2205
- bucket_name: The bucket name for key isolation
2211
+ output_path: Path where the downloaded file will be saved (None for bytes/streaming)
2212
+ subaccount_id: The subaccount/account identifier (required for decryption)
2213
+ bucket_name: The bucket name for key isolation (required for decryption)
2206
2214
  auto_decrypt: Whether to attempt automatic decryption (default: True)
2207
2215
  download_node: IPFS node URL for download (default: local node)
2216
+ return_bytes: If True, return bytes instead of saving to file
2217
+ streaming: If True, return decrypted bytes when auto_decrypt=True, or raw streaming iterator when auto_decrypt=False
2208
2218
 
2209
2219
  Returns:
2210
- S3DownloadResult: Object containing download info and decryption status
2220
+ S3DownloadResult: Download info and decryption status (default)
2221
+ bytes: Raw decrypted content when return_bytes=True or streaming=True with auto_decrypt=True
2222
+ AsyncIterator[bytes]: Raw streaming iterator when streaming=True and auto_decrypt=False
2211
2223
 
2212
2224
  Raises:
2213
2225
  HippiusIPFSError: If IPFS download fails
2214
2226
  FileNotFoundError: If the output directory doesn't exist
2215
- ValueError: If decryption fails
2227
+ ValueError: If decryption fails or invalid parameter combinations
2216
2228
  """
2229
+ # Validate parameter combinations
2230
+ if streaming and return_bytes:
2231
+ raise ValueError("Cannot specify both streaming and return_bytes")
2232
+
2233
+ if streaming:
2234
+ # Validate required parameters for decryption if auto_decrypt is True
2235
+ if auto_decrypt and (not subaccount_id or not bucket_name):
2236
+ raise ValueError(
2237
+ "subaccount_id and bucket_name are required for streaming decryption"
2238
+ )
2239
+
2240
+ if auto_decrypt:
2241
+ # Return decrypted bytes directly (not streaming)
2242
+ try:
2243
+ key_storage_available = is_key_storage_enabled()
2244
+ except ImportError:
2245
+ key_storage_available = False
2246
+
2247
+ encryption_key_bytes = None
2248
+
2249
+ if key_storage_available:
2250
+ # Create combined key identifier from account+bucket
2251
+ account_bucket_key = f"{subaccount_id}:{bucket_name}"
2252
+
2253
+ try:
2254
+ existing_key_b64 = await get_key_for_subaccount(
2255
+ account_bucket_key
2256
+ )
2257
+ if existing_key_b64:
2258
+ encryption_key_bytes = base64.b64decode(existing_key_b64)
2259
+ except Exception as e:
2260
+ logger.debug(f"Failed to get encryption key: {e}")
2261
+
2262
+ # If key storage decryption failed or wasn't available, try client encryption key
2263
+ if not encryption_key_bytes and self.encryption_available:
2264
+ logger.debug("Using client encryption key for streaming decryption")
2265
+ encryption_key_bytes = self.encryption_key
2266
+
2267
+ if not encryption_key_bytes:
2268
+ logger.warning(
2269
+ "No encryption key found - downloading raw encrypted data as bytes"
2270
+ )
2271
+ # Return raw encrypted data as bytes
2272
+ encrypted_data = b""
2273
+ async for chunk in self._get_ipfs_stream(cid, download_node):
2274
+ encrypted_data += chunk
2275
+ return encrypted_data
2276
+
2277
+ # Stream and decrypt the content using hybrid buffered approach
2278
+ import nacl.secret
2279
+
2280
+ # Collect all encrypted data first
2281
+ logger.debug("Buffering encrypted content for decryption")
2282
+ encrypted_data = b""
2283
+ async for chunk in self._get_ipfs_stream(cid, download_node):
2284
+ encrypted_data += chunk
2285
+
2286
+ # Decrypt the complete buffered content and return as bytes
2287
+ try:
2288
+ box = nacl.secret.SecretBox(encryption_key_bytes)
2289
+ decrypted_data = box.decrypt(encrypted_data)
2290
+ logger.info(f"Successfully decrypted {len(decrypted_data)} bytes")
2291
+
2292
+ # Return all decrypted data as bytes
2293
+ return decrypted_data
2294
+
2295
+ except Exception as decrypt_error:
2296
+ logger.error(f"Streaming decryption failed: {decrypt_error}")
2297
+ raise ValueError(
2298
+ f"Failed to decrypt streaming content: {decrypt_error}"
2299
+ )
2300
+ else:
2301
+ # Return raw streaming iterator from IPFS node - no processing
2302
+ async def streaming_wrapper():
2303
+ async for chunk in self._get_ipfs_stream(cid, download_node):
2304
+ yield chunk
2305
+
2306
+ return streaming_wrapper()
2307
+
2217
2308
  start_time = time.time()
2218
2309
 
2310
+ # Validate required parameters for decryption
2311
+ if auto_decrypt and (not subaccount_id or not bucket_name):
2312
+ raise ValueError(
2313
+ "subaccount_id and bucket_name are required when auto_decrypt=True"
2314
+ )
2315
+
2316
+ if not return_bytes and not output_path:
2317
+ raise ValueError("output_path is required when not using return_bytes mode")
2318
+
2219
2319
  # Download the file directly into memory from the specified download_node
2220
2320
  try:
2221
- # Create parent directories if they don't exist
2222
- os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
2321
+ # Create parent directories if they don't exist (only for file output mode)
2322
+ if not return_bytes:
2323
+ os.makedirs(
2324
+ os.path.dirname(os.path.abspath(output_path)), exist_ok=True
2325
+ )
2223
2326
 
2224
2327
  download_client = AsyncIPFSClient(api_url=download_node)
2225
2328
 
@@ -2357,20 +2460,58 @@ class IPFSClient:
2357
2460
  elif not decryption_attempted:
2358
2461
  logger.debug("No decryption attempted - no keys available")
2359
2462
 
2360
- # Write the final data (encrypted or decrypted) to disk once
2361
- with open(output_path, "wb") as f:
2362
- f.write(final_data)
2463
+ # Handle output based on mode
2464
+ if return_bytes:
2465
+ # Return bytes directly
2466
+ return final_data
2467
+ else:
2468
+ # Write the final data (encrypted or decrypted) to disk once
2469
+ with open(output_path, "wb") as f:
2470
+ f.write(final_data)
2363
2471
 
2364
- # Get final file info
2365
- size_bytes = len(final_data)
2366
- elapsed_time = time.time() - start_time
2472
+ # Get final file info
2473
+ size_bytes = len(final_data)
2474
+ elapsed_time = time.time() - start_time
2367
2475
 
2368
- return S3DownloadResult(
2369
- cid=cid,
2370
- output_path=output_path,
2371
- size_bytes=size_bytes,
2372
- size_formatted=self.format_size(size_bytes),
2373
- elapsed_seconds=round(elapsed_time, 2),
2374
- decrypted=decrypted,
2375
- encryption_key=encryption_key_used,
2376
- )
2476
+ return S3DownloadResult(
2477
+ cid=cid,
2478
+ output_path=output_path,
2479
+ size_bytes=size_bytes,
2480
+ size_formatted=self.format_size(size_bytes),
2481
+ elapsed_seconds=round(elapsed_time, 2),
2482
+ decrypted=decrypted,
2483
+ encryption_key=encryption_key_used,
2484
+ )
2485
+
2486
+ async def _get_ipfs_stream(
2487
+ self, cid: str, download_node: str
2488
+ ) -> AsyncIterator[bytes]:
2489
+ """
2490
+ Get a raw streaming iterator from IPFS node.
2491
+
2492
+ This method returns the raw encrypted stream directly from the IPFS node
2493
+ without any processing, decryption, or temporary file operations.
2494
+
2495
+ Args:
2496
+ cid: Content Identifier (CID) of the file to stream
2497
+ download_node: IPFS node URL for download
2498
+
2499
+ Returns:
2500
+ AsyncIterator[bytes]: Raw streaming iterator from IPFS
2501
+
2502
+ Raises:
2503
+ HippiusIPFSError: If IPFS stream fails
2504
+ """
2505
+ try:
2506
+ download_client = AsyncIPFSClient(api_url=download_node)
2507
+ download_url = f"{download_node.rstrip('/')}/api/v0/cat?arg={cid}"
2508
+
2509
+ async with download_client.client.stream("POST", download_url) as response:
2510
+ response.raise_for_status()
2511
+ logger.info(f"Started streaming from {download_node} for CID: {cid}")
2512
+
2513
+ async for chunk in response.aiter_bytes(chunk_size=8192):
2514
+ yield chunk
2515
+
2516
+ except Exception as e:
2517
+ raise HippiusIPFSError(f"Failed to stream from {download_node}: {str(e)}")