hippius 0.2.7__py3-none-any.whl → 0.2.9__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.7
3
+ Version: 0.2.9
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,17 +1,17 @@
1
- hippius_sdk/__init__.py,sha256=t0Lg5aOSqwtAWhW8wgmVeB4IdJvUTv7Pb77vCTP0KAE,1391
1
+ hippius_sdk/__init__.py,sha256=fqaCct4J_4JlfpFmTtc2TrG3fqQaiDU9Yl4tsgvqcBI,1391
2
2
  hippius_sdk/cli.py,sha256=pzwoa-X5cwdA_pM-fqUyXZKHgcZODrLe4qHZuCqQMtQ,18210
3
3
  hippius_sdk/cli_assets.py,sha256=V3MX63QTiex6mCp0VDXQJ7cagm5v1s4xtsu8c1O4G_k,371
4
- hippius_sdk/cli_handlers.py,sha256=xSFMGrgQRoatFgkk0CpM9h5q6URUzAol3xHGCdFHKxE,125258
4
+ hippius_sdk/cli_handlers.py,sha256=LUS-BPPMfvXCLsHiN225nMTlBEHXKxZmUf1YpC-Xyqc,127905
5
5
  hippius_sdk/cli_parser.py,sha256=Qh2wgkFBUTPldvGoTQuoNKQl5Vo0x6fPEsPBU5oymP4,20242
6
6
  hippius_sdk/cli_rich.py,sha256=_jTBYMdHi2--fIVwoeNi-EtkdOb6Zy_O2TUiGvU3O7s,7324
7
7
  hippius_sdk/client.py,sha256=eYURsq_so3WlEt_JY_u7J0iECFVOKDf5vsnGyR9Kngw,16974
8
8
  hippius_sdk/config.py,sha256=sCWD2remLa-FodvxC2O45tiNSJD7gzv9idIStX9sF_k,21240
9
9
  hippius_sdk/errors.py,sha256=LScJJmawVAx7aRzqqQguYSkf9iazSjEQEBNlD_GXZ6Y,1589
10
10
  hippius_sdk/ipfs.py,sha256=xq0y87dyKvbwf52OjEAlqFNrl4Ej1Zp5g2rZa75qfN8,71706
11
- hippius_sdk/ipfs_core.py,sha256=Lxu2-AWEpE-kiJDcP-cOBEeuboK7i9eNZdjgJWPa4sI,16028
11
+ hippius_sdk/ipfs_core.py,sha256=Dzfmty7SzHW1GNjjLpp9u0JSf0f2g2FB-78nDtTUt3I,12820
12
12
  hippius_sdk/substrate.py,sha256=HqR2-_9njZZ5UCKgiaGr5L5TGQ_wtj7oyA3sA5sGGyE,47525
13
13
  hippius_sdk/utils.py,sha256=Ur7P_7iVnXXYvbg7a0aVrdN_8NkVxjhdngn8NzR_zpc,7066
14
- hippius-0.2.7.dist-info/METADATA,sha256=dNUAXbSVYGvEKLsFvTpdF_SIa3B1fM9pnm7FQzJduB8,29992
15
- hippius-0.2.7.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
16
- hippius-0.2.7.dist-info/entry_points.txt,sha256=b1lo60zRXmv1ud-c5BC-cJcAfGE5FD4qM_nia6XeQtM,98
17
- hippius-0.2.7.dist-info/RECORD,,
14
+ hippius-0.2.9.dist-info/METADATA,sha256=Br0pN49dYq3fhG3bjl5lVj-yOjPAHM1pFk7PGxF7wYk,29992
15
+ hippius-0.2.9.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
16
+ hippius-0.2.9.dist-info/entry_points.txt,sha256=b1lo60zRXmv1ud-c5BC-cJcAfGE5FD4qM_nia6XeQtM,98
17
+ hippius-0.2.9.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
27
27
  from hippius_sdk.utils import format_cid, format_size, hex_to_ipfs_cid
28
28
 
29
- __version__ = "0.2.7"
29
+ __version__ = "0.2.9"
30
30
  __all__ = [
31
31
  "HippiusClient",
32
32
  "IPFSClient",
@@ -371,20 +371,22 @@ async def handle_store(
371
371
  "gateway_url"
372
372
  ] = f"{client.ipfs_client.gateway}/ipfs/{result['cid']}"
373
373
 
374
- # Store on blockchain if miners are provided
375
- if miner_ids:
376
- # Create a file input for blockchain storage
377
- file_input = FileInput(
378
- file_hash=result["cid"], file_name=file_name
379
- )
374
+ # Store on blockchain - miners are optional
375
+ # Create a file input for blockchain storage
376
+ file_input = FileInput(file_hash=result["cid"], file_name=file_name)
380
377
 
381
- # Submit storage request
382
- tx_hash = await client.substrate_client.storage_request(
383
- files=[file_input], miner_ids=miner_id_list
384
- )
378
+ # Submit storage request
379
+ tx_hash = await client.substrate_client.storage_request(
380
+ files=[file_input], miner_ids=miner_id_list
381
+ )
385
382
 
386
- # Add transaction hash to result
387
- result["transaction_hash"] = tx_hash
383
+ # Add transaction hash to result
384
+ result["transaction_hash"] = tx_hash
385
+
386
+ # Add a note about the pinning status command
387
+ log(
388
+ "\n[bold yellow]Note:[/bold yellow] The pinning-status command will show a different CID (metadata) rather than the direct file CID."
389
+ )
388
390
  except Exception as e:
389
391
  warning(f"Failed to publish file globally: {str(e)}")
390
392
 
@@ -575,12 +577,8 @@ async def handle_store_dir(
575
577
  f"Failed to publish file {file_info['name']} globally: {str(e)}"
576
578
  )
577
579
 
578
- # Store on blockchain if miners are provided - this is what requires a password
579
- if (
580
- miner_ids
581
- and hasattr(client, "substrate_client")
582
- and client.substrate_client
583
- ):
580
+ # Store on blockchain if client is available - miners are optional
581
+ if hasattr(client, "substrate_client") and client.substrate_client:
584
582
  # Create a file input for blockchain storage
585
583
  file_input = FileInput(
586
584
  file_hash=result["cid"],
@@ -595,6 +593,11 @@ async def handle_store_dir(
595
593
  # Add transaction hash to result
596
594
  result["transaction_hash"] = tx_hash
597
595
 
596
+ # Add a note about the pinning status command
597
+ log(
598
+ "\n[bold yellow]Note:[/bold yellow] The pinning-status command will show a different CID (metadata) rather than the direct directory CID."
599
+ )
600
+
598
601
  except Exception as e:
599
602
  warning(f"Failed to publish directory globally: {str(e)}")
600
603
 
@@ -739,20 +742,32 @@ async def handle_files(
739
742
  """Handle the files command"""
740
743
  # Get the account address we're querying
741
744
  if account_address is None:
742
- # If no address provided, first try to get from keypair (if available)
745
+ # If no address provided, try these options in order:
746
+ # 1. Keypair from the client (if available)
747
+ # 2. Address from active account
748
+ # 3. Default address from config
749
+
750
+ # Option 1: Try keypair from client
743
751
  if (
744
752
  hasattr(client.substrate_client, "_keypair")
745
753
  and client.substrate_client._keypair is not None
746
754
  ):
747
755
  account_address = client.substrate_client._keypair.ss58_address
748
756
  else:
749
- # Try to get the default address
750
- default_address = get_default_address()
751
- if default_address:
752
- account_address = default_address
753
- else:
754
- has_default = get_default_address() is not None
757
+ # Option 2: Try to get address from active account
758
+ active_account = get_active_account()
759
+ if active_account:
760
+ account_address = get_account_address(active_account)
761
+
762
+ # Option 3: If still not found, try default address
763
+ if not account_address:
764
+ default_address = get_default_address()
765
+ if default_address:
766
+ account_address = default_address
755
767
 
768
+ # If we still don't have an address, show error
769
+ if not account_address:
770
+ has_default = get_default_address() is not None
756
771
  error("No account address provided, and client has no keypair.")
757
772
 
758
773
  if has_default:
@@ -842,7 +857,7 @@ async def handle_pinning_status(
842
857
  cid = pin.get("cid")
843
858
 
844
859
  # Display pin information
845
- log(f"\n{i}. CID: [bold]{cid}[/bold]")
860
+ log(f"\n{i}. Metadata CID: [bold]{cid}[/bold]")
846
861
  log(f" File Name: {pin['file_name']}")
847
862
  status = "Assigned" if pin["is_assigned"] else "Pending"
848
863
  log(f" Status: {status}")
@@ -861,17 +876,49 @@ async def handle_pinning_status(
861
876
 
862
877
  # Show content info if requested
863
878
  if show_contents:
879
+ # Add gateway URL
880
+ gateway_url = f"{client.ipfs_client.gateway}/ipfs/{cid}"
881
+ log(f" Gateway URL: {gateway_url}")
882
+
883
+ # Try to decode the metadata file to get the original file CID
864
884
  try:
865
- content_info = await client.ipfs_client.get_content_info(cid)
885
+ # Fetch the content
886
+ cat_result = await client.ipfs_client.cat(cid)
866
887
 
867
- if content_info:
868
- if "size_formatted" in content_info:
869
- log(f" Size: {content_info['size_formatted']}")
870
- if "gateway_url" in content_info:
871
- log(f" Gateway URL: {content_info['gateway_url']}")
888
+ # Try to parse as JSON
889
+ try:
890
+ # Decode JSON from content
891
+ metadata_json = json.loads(
892
+ cat_result["content"].decode("utf-8")
893
+ )
894
+
895
+ # This should be an array with one or more file entries
896
+ if (
897
+ isinstance(metadata_json, list)
898
+ and len(metadata_json) > 0
899
+ ):
900
+ log(f"\n [bold cyan]Contained files:[/bold cyan]")
901
+ for idx, file_entry in enumerate(metadata_json, 1):
902
+ if isinstance(file_entry, dict):
903
+ original_cid = file_entry.get("cid")
904
+ original_name = file_entry.get("filename")
905
+ if original_cid:
906
+ log(
907
+ f" {idx}. Original CID: [bold green]{original_cid}[/bold green]"
908
+ )
909
+ if original_name:
910
+ log(f" Name: {original_name}")
911
+ log(
912
+ f" Gateway URL: {client.ipfs_client.gateway}/ipfs/{original_cid}"
913
+ )
914
+ except json.JSONDecodeError:
915
+ if verbose:
916
+ log(
917
+ " [yellow]Could not parse metadata as JSON[/yellow]"
918
+ )
872
919
  except Exception as e:
873
920
  if verbose:
874
- warning(f" Error getting content info: {e}")
921
+ warning(f" Error getting original file CIDs: {e}")
875
922
  except Exception as e:
876
923
  warning(f"Error processing pin {i}: {e}")
877
924
  if verbose:
hippius_sdk/ipfs_core.py CHANGED
@@ -59,8 +59,14 @@ class AsyncIPFSClient:
59
59
  Dict containing the CID and other information
60
60
  """
61
61
  with open(file_path, "rb") as f:
62
- files = {"file": f}
63
- response = await self.client.post(f"{self.api_url}/api/v0/add", files=files)
62
+ 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")}
66
+ # Explicitly set wrap-with-directory=false to prevent wrapping in directory
67
+ response = await self.client.post(
68
+ f"{self.api_url}/api/v0/add?wrap-with-directory=false", files=files
69
+ )
64
70
  response.raise_for_status()
65
71
  return response.json()
66
72
 
@@ -75,8 +81,12 @@ class AsyncIPFSClient:
75
81
  Returns:
76
82
  Dict containing the CID and other information
77
83
  """
78
- files = {"file": (filename, data)}
79
- response = await self.client.post(f"{self.api_url}/api/v0/add", files=files)
84
+ # Specify file with name and content type to ensure consistent handling
85
+ files = {"file": (filename, data, "application/octet-stream")}
86
+ # Explicitly set wrap-with-directory=false to prevent wrapping in directory
87
+ response = await self.client.post(
88
+ f"{self.api_url}/api/v0/add?wrap-with-directory=false", files=files
89
+ )
80
90
  response.raise_for_status()
81
91
  return response.json()
82
92
 
@@ -149,59 +159,30 @@ class AsyncIPFSClient:
149
159
  cid: Content Identifier
150
160
 
151
161
  Returns:
152
- Dict with links information and a is_directory flag
162
+ Dict with links information and is_directory flag
153
163
  """
154
- try:
155
- # Try using the direct IPFS API first (most reliable)
156
- response = await self.client.post(f"{self.api_url}/api/v0/ls?arg={cid}")
157
- response.raise_for_status()
158
- result = response.json()
159
-
160
- # Add a flag to indicate if this is a directory
161
- # A directory has Links and typically more than one or has Type=1
162
- is_directory = False
163
- if "Objects" in result and len(result["Objects"]) > 0:
164
- obj = result["Objects"][0]
165
- if "Links" in obj and len(obj["Links"]) > 0:
166
- # It has links, likely a directory
167
- is_directory = True
168
- # Check if any links have Type=1 (directory)
169
- for link in obj["Links"]:
170
- if link.get("Type") == 1:
171
- is_directory = True
172
- break
173
-
174
- # Add the flag to the result
175
- result["is_directory"] = is_directory
176
- return result
164
+ # Try using the direct IPFS API first (most reliable)
165
+ response = await self.client.post(f"{self.api_url}/api/v0/ls?arg={cid}")
166
+ response.raise_for_status()
167
+ result = response.json()
177
168
 
178
- except Exception as e:
179
- # If the IPFS API fails, try to get info from the gateway
180
- try:
181
- # Try to get a small sample of the content to check if it's a directory listing
182
- content_sample = await self.cat(cid)
183
- is_directory = False
184
-
185
- # If it starts with HTML doctype and has IPFS title, it's probably a directory listing
186
- if (
187
- content_sample.startswith(b"<!DOCTYPE html>")
188
- and b"<title>/ipfs/" in content_sample
189
- ):
190
- is_directory = True
191
-
192
- # Return a simplified result similar to what the ls API would return
193
- return {
194
- "is_directory": is_directory,
195
- "Objects": [
196
- {
197
- "Hash": cid,
198
- "Links": [], # We can't get links from HTML content easily
199
- }
200
- ],
201
- }
202
- except Exception as fallback_error:
203
- # Re-raise the original error
204
- raise e
169
+ # Add a flag to indicate if this is a directory.
170
+ # A directory has Links and typically more than one or has Type=1
171
+ is_directory = False
172
+ if "Objects" in result and len(result["Objects"]) > 0:
173
+ obj = result["Objects"][0]
174
+ if "Links" in obj and len(obj["Links"]) > 0:
175
+ # It has links, likely a directory
176
+ is_directory = True
177
+ # Check if any links have Type=1 (directory)
178
+ for link in obj["Links"]:
179
+ if link.get("Type") == 1:
180
+ is_directory = True
181
+ break
182
+
183
+ # Add the flag to the result
184
+ result["is_directory"] = is_directory
185
+ return result
205
186
 
206
187
  async def exists(self, cid: str) -> bool:
207
188
  """
@@ -241,7 +222,7 @@ class AsyncIPFSClient:
241
222
  ls_result = await self.ls(cid)
242
223
  if ls_result.get("is_directory", False):
243
224
  # It's a directory, use the get command to download it properly
244
- return await self.download_directory_with_get(cid, output_path)
225
+ return await self.download_directory(cid, output_path)
245
226
  except Exception:
246
227
  # If ls check fails, continue with regular file download
247
228
  pass
@@ -259,80 +240,13 @@ class AsyncIPFSClient:
259
240
  # Only try directory fallback if not skipping directory check
260
241
  if not skip_directory_check:
261
242
  try:
262
- return await self.download_directory_with_get(cid, output_path)
243
+ return await self.download_directory(cid, output_path)
263
244
  except Exception:
264
245
  pass
265
246
  # Raise the original error
266
247
  raise e
267
248
 
268
249
  async def download_directory(self, cid: str, output_path: str) -> str:
269
- """
270
- Download a directory from IPFS.
271
-
272
- Args:
273
- cid: Content identifier of the directory
274
- output_path: Path where to save the directory
275
-
276
- Returns:
277
- Path to the saved directory
278
- """
279
- # Try the more reliable get command first
280
- try:
281
- return await self.download_directory_with_get(cid, output_path)
282
- except Exception as e:
283
- # If get command fails, fall back to ls/cat method
284
- try:
285
- # Get directory listing
286
- ls_result = await self.ls(cid)
287
- if not ls_result.get("is_directory", False):
288
- raise ValueError(f"CID {cid} is not a directory")
289
-
290
- # Create the directory if it doesn't exist
291
- os.makedirs(output_path, exist_ok=True)
292
-
293
- # Extract links from the updated response format
294
- links = []
295
- # The ls result format is: { "Objects": [ { "Hash": "...", "Links": [...] } ] }
296
- if "Objects" in ls_result and len(ls_result["Objects"]) > 0:
297
- for obj in ls_result["Objects"]:
298
- if "Links" in obj:
299
- links.extend(obj["Links"])
300
-
301
- # Download each file in the directory
302
- for link in links:
303
- file_name = link.get("Name")
304
- file_cid = link.get("Hash")
305
- file_type = link.get("Type")
306
-
307
- # Skip entries without required data
308
- if not (file_name and file_cid):
309
- continue
310
-
311
- # Build the path for this file/directory
312
- file_path = os.path.join(output_path, file_name)
313
-
314
- if file_type == 1 or file_type == "dir": # Directory type
315
- # Recursively download the subdirectory
316
- await self.download_directory(file_cid, file_path)
317
- else: # File type
318
- # Download the file
319
- content = await self.cat(file_cid)
320
- os.makedirs(
321
- os.path.dirname(os.path.abspath(file_path)), exist_ok=True
322
- )
323
- with open(file_path, "wb") as f:
324
- f.write(content)
325
-
326
- return output_path
327
- except Exception as fallback_error:
328
- # If both methods fail, raise a more detailed error
329
- raise RuntimeError(
330
- f"Failed to download directory: get error: {e}, ls/cat error: {fallback_error}"
331
- )
332
-
333
- return output_path
334
-
335
- async def download_directory_with_get(self, cid: str, output_path: str) -> str:
336
250
  """
337
251
  Download a directory from IPFS by recursively fetching its contents.
338
252
 
@@ -345,6 +259,13 @@ class AsyncIPFSClient:
345
259
  """
346
260
  # First, get the directory listing to find all contents
347
261
  try:
262
+ import uuid
263
+
264
+ # Handle potential file/directory collision
265
+ if os.path.exists(output_path) and not os.path.isdir(output_path):
266
+ # Generate unique path by adding a UUID suffix
267
+ output_path = f"{output_path}_{str(uuid.uuid4())[:8]}"
268
+
348
269
  ls_result = await self.ls(cid)
349
270
 
350
271
  # Create target directory
@@ -362,7 +283,6 @@ class AsyncIPFSClient:
362
283
  link_name = link.get("Name")
363
284
  link_hash = link.get("Hash")
364
285
  link_type = link.get("Type")
365
- link_size = link.get("Size", 0)
366
286
 
367
287
  if not (link_name and link_hash):
368
288
  continue # Skip if missing essential data
@@ -372,7 +292,7 @@ class AsyncIPFSClient:
372
292
 
373
293
  if link_type == 1 or str(link_type) == "1" or link_type == "dir":
374
294
  # It's a directory - recursively download
375
- await self.download_directory_with_get(link_hash, target_path)
295
+ await self.download_directory(link_hash, target_path)
376
296
  else:
377
297
  # It's a file - download it
378
298
  try: