hippius 0.2.40__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.40 → hippius-0.2.41}/PKG-INFO +1 -1
  2. {hippius-0.2.40 → hippius-0.2.41}/hippius_sdk/__init__.py +1 -1
  3. {hippius-0.2.40 → hippius-0.2.41}/hippius_sdk/client.py +42 -30
  4. {hippius-0.2.40 → hippius-0.2.41}/hippius_sdk/ipfs.py +148 -73
  5. {hippius-0.2.40 → hippius-0.2.41}/pyproject.toml +1 -1
  6. {hippius-0.2.40 → hippius-0.2.41}/README.md +0 -0
  7. {hippius-0.2.40 → hippius-0.2.41}/hippius_sdk/cli.py +0 -0
  8. {hippius-0.2.40 → hippius-0.2.41}/hippius_sdk/cli_assets.py +0 -0
  9. {hippius-0.2.40 → hippius-0.2.41}/hippius_sdk/cli_handlers.py +0 -0
  10. {hippius-0.2.40 → hippius-0.2.41}/hippius_sdk/cli_parser.py +0 -0
  11. {hippius-0.2.40 → hippius-0.2.41}/hippius_sdk/cli_rich.py +0 -0
  12. {hippius-0.2.40 → hippius-0.2.41}/hippius_sdk/config.py +0 -0
  13. {hippius-0.2.40 → hippius-0.2.41}/hippius_sdk/db/README.md +0 -0
  14. {hippius-0.2.40 → hippius-0.2.41}/hippius_sdk/db/env.db.template +0 -0
  15. {hippius-0.2.40 → hippius-0.2.41}/hippius_sdk/db/migrations/20241201000001_create_key_storage_tables.sql +0 -0
  16. {hippius-0.2.40 → hippius-0.2.41}/hippius_sdk/db/migrations/20241202000001_switch_to_subaccount_encryption.sql +0 -0
  17. {hippius-0.2.40 → hippius-0.2.41}/hippius_sdk/db/setup_database.sh +0 -0
  18. {hippius-0.2.40 → hippius-0.2.41}/hippius_sdk/db_utils.py +0 -0
  19. {hippius-0.2.40 → hippius-0.2.41}/hippius_sdk/errors.py +0 -0
  20. {hippius-0.2.40 → hippius-0.2.41}/hippius_sdk/ipfs_core.py +0 -0
  21. {hippius-0.2.40 → hippius-0.2.41}/hippius_sdk/key_storage.py +0 -0
  22. {hippius-0.2.40 → hippius-0.2.41}/hippius_sdk/substrate.py +0 -0
  23. {hippius-0.2.40 → 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.40
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.40"
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
@@ -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 raw streaming iterator from IPFS (no decryption)
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
608
+ AsyncIterator[bytes]: Raw streaming iterator when streaming=True
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
  )
@@ -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
@@ -1967,7 +1967,7 @@ class IPFSClient:
1967
1967
 
1968
1968
  async def s3_publish(
1969
1969
  self,
1970
- file_path: str,
1970
+ content: Union[str, bytes, os.PathLike],
1971
1971
  encrypt: bool,
1972
1972
  seed_phrase: str,
1973
1973
  subaccount_id: str,
@@ -1979,7 +1979,7 @@ class IPFSClient:
1979
1979
  file_name: str = None,
1980
1980
  ) -> Union[S3PublishResult, S3PublishPin]:
1981
1981
  """
1982
- Publish a file to IPFS and the Hippius marketplace in one operation.
1982
+ Publish content to IPFS and the Hippius marketplace in one operation.
1983
1983
 
1984
1984
  This method uses a two-node architecture for optimal performance:
1985
1985
  1. Uploads to store_node (local) for immediate availability
@@ -1992,8 +1992,8 @@ class IPFSClient:
1992
1992
  - Always uses the most recent key for an account+bucket combination
1993
1993
 
1994
1994
  Args:
1995
- file_path: Path to the file to publish
1996
- 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
1997
1997
  seed_phrase: Seed phrase for blockchain transaction signing
1998
1998
  subaccount_id: The subaccount/account identifier
1999
1999
  bucket_name: The bucket name for key isolation
@@ -2001,25 +2001,47 @@ class IPFSClient:
2001
2001
  pin_node: IPFS node URL for backup pinning (default: remote service)
2002
2002
  substrate_url: the substrate url to connect to for the storage request
2003
2003
  publish: Whether to publish to blockchain (True) or just upload to IPFS (False)
2004
- file_name: The original file name.
2004
+ file_name: The original file name (required if content is bytes)
2005
2005
 
2006
2006
  Returns:
2007
2007
  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
2008
+ S3PublishPin: Object containing CID, subaccount, content info, pin_node, substrate_url when publish=False
2009
2009
 
2010
2010
  Raises:
2011
2011
  HippiusIPFSError: If IPFS operations (add or pin) fail
2012
2012
  HippiusSubstrateError: If substrate call fails
2013
- FileNotFoundError: If the file doesn't exist
2014
- 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
2015
2015
  """
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")
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
+ )
2019
2038
 
2020
- # Get file info
2021
- filename = os.path.basename(file_path)
2022
- 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)
2023
2045
 
2024
2046
  # Handle encryption if requested with automatic key management
2025
2047
  encryption_key_used = None
@@ -2057,19 +2079,11 @@ class IPFSClient:
2057
2079
  encryption_key_bytes = base64.b64decode(new_key_b64)
2058
2080
  encryption_key_used = new_key_b64
2059
2081
 
2060
- # Read file content into memory
2061
- with open(file_path, "rb") as f:
2062
- file_data = f.read()
2063
-
2064
2082
  # Encrypt the data using the key from key storage
2065
2083
  import nacl.secret
2066
2084
 
2067
2085
  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)
2086
+ content_bytes = box.encrypt(content_bytes)
2073
2087
  else:
2074
2088
  # Fallback to the original encryption system if key_storage is not available
2075
2089
  if not self.encryption_available:
@@ -2077,16 +2091,8 @@ class IPFSClient:
2077
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'"
2078
2092
  )
2079
2093
 
2080
- # Read file content into memory
2081
- with open(file_path, "rb") as f:
2082
- file_data = f.read()
2083
-
2084
2094
  # 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)
2095
+ content_bytes = self.encrypt_data(content_bytes)
2090
2096
 
2091
2097
  # Store the encryption key for the result
2092
2098
  encryption_key_used = (
@@ -2098,15 +2104,15 @@ class IPFSClient:
2098
2104
  # Step 1: Upload to store_node (local) for immediate availability
2099
2105
  try:
2100
2106
  store_client = AsyncIPFSClient(api_url=store_node)
2101
- result = await store_client.add_file(
2102
- file_path,
2103
- file_name=file_name,
2107
+ result = await store_client.add_bytes(
2108
+ content_bytes,
2109
+ filename=filename,
2104
2110
  )
2105
2111
  cid = result["Hash"]
2106
- 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}")
2107
2113
  except Exception as e:
2108
2114
  raise HippiusIPFSError(
2109
- 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)}"
2110
2116
  )
2111
2117
 
2112
2118
  # Step 2: Pin to pin_node (remote) for persistence and backup
@@ -2116,7 +2122,7 @@ class IPFSClient:
2116
2122
  logger.info(f"File pinned to backup node {pin_node}")
2117
2123
  except Exception as e:
2118
2124
  raise HippiusIPFSError(
2119
- 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)}"
2120
2126
  )
2121
2127
 
2122
2128
  # Conditionally publish to substrate marketplace based on publish flag
@@ -2174,7 +2180,7 @@ class IPFSClient:
2174
2180
  return S3PublishPin(
2175
2181
  cid=cid,
2176
2182
  subaccount=subaccount_id,
2177
- file_path=file_path,
2183
+ file_path=filename,
2178
2184
  pin_node=pin_node,
2179
2185
  substrate_url=substrate_url,
2180
2186
  )
@@ -2182,44 +2188,75 @@ class IPFSClient:
2182
2188
  async def s3_download(
2183
2189
  self,
2184
2190
  cid: str,
2185
- output_path: str,
2186
- subaccount_id: str,
2187
- bucket_name: str,
2191
+ output_path: Optional[str] = None,
2192
+ subaccount_id: Optional[str] = None,
2193
+ bucket_name: Optional[str] = None,
2188
2194
  auto_decrypt: bool = True,
2189
2195
  download_node: str = "http://localhost:5001",
2190
- ) -> S3DownloadResult:
2196
+ return_bytes: bool = False,
2197
+ streaming: bool = False,
2198
+ ) -> Union[S3DownloadResult, bytes, AsyncIterator[bytes]]:
2191
2199
  """
2192
- Download a file from IPFS with automatic decryption.
2200
+ Download content from IPFS with flexible output options and automatic decryption.
2193
2201
 
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
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)
2200
2206
 
2201
2207
  Args:
2202
2208
  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
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)
2206
2212
  auto_decrypt: Whether to attempt automatic decryption (default: True)
2207
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)
2208
2216
 
2209
2217
  Returns:
2210
- 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
2211
2221
 
2212
2222
  Raises:
2213
2223
  HippiusIPFSError: If IPFS download fails
2214
2224
  FileNotFoundError: If the output directory doesn't exist
2215
- ValueError: If decryption fails
2225
+ ValueError: If decryption fails or invalid parameter combinations
2216
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
+
2217
2244
  start_time = time.time()
2218
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
+
2219
2255
  # Download the file directly into memory from the specified download_node
2220
2256
  try:
2221
- # Create parent directories if they don't exist
2222
- 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)
2223
2260
 
2224
2261
  download_client = AsyncIPFSClient(api_url=download_node)
2225
2262
 
@@ -2357,20 +2394,58 @@ class IPFSClient:
2357
2394
  elif not decryption_attempted:
2358
2395
  logger.debug("No decryption attempted - no keys available")
2359
2396
 
2360
- # Write the final data (encrypted or decrypted) to disk once
2361
- with open(output_path, "wb") as f:
2362
- 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)
2363
2405
 
2364
- # Get final file info
2365
- size_bytes = len(final_data)
2366
- elapsed_time = time.time() - start_time
2406
+ # Get final file info
2407
+ size_bytes = len(final_data)
2408
+ elapsed_time = time.time() - start_time
2367
2409
 
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
- )
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
+ )
2419
+
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)}")
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "hippius"
3
- version = "0.2.40"
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