hippius 0.2.7__tar.gz → 0.2.9__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.
- {hippius-0.2.7 → hippius-0.2.9}/PKG-INFO +1 -1
- {hippius-0.2.7 → hippius-0.2.9}/hippius_sdk/__init__.py +1 -1
- {hippius-0.2.7 → hippius-0.2.9}/hippius_sdk/cli_handlers.py +80 -33
- {hippius-0.2.7 → hippius-0.2.9}/hippius_sdk/ipfs_core.py +46 -126
- {hippius-0.2.7 → hippius-0.2.9}/pyproject.toml +1 -1
- {hippius-0.2.7 → hippius-0.2.9}/README.md +0 -0
- {hippius-0.2.7 → hippius-0.2.9}/hippius_sdk/cli.py +0 -0
- {hippius-0.2.7 → hippius-0.2.9}/hippius_sdk/cli_assets.py +0 -0
- {hippius-0.2.7 → hippius-0.2.9}/hippius_sdk/cli_parser.py +0 -0
- {hippius-0.2.7 → hippius-0.2.9}/hippius_sdk/cli_rich.py +0 -0
- {hippius-0.2.7 → hippius-0.2.9}/hippius_sdk/client.py +0 -0
- {hippius-0.2.7 → hippius-0.2.9}/hippius_sdk/config.py +0 -0
- {hippius-0.2.7 → hippius-0.2.9}/hippius_sdk/errors.py +0 -0
- {hippius-0.2.7 → hippius-0.2.9}/hippius_sdk/ipfs.py +0 -0
- {hippius-0.2.7 → hippius-0.2.9}/hippius_sdk/substrate.py +0 -0
- {hippius-0.2.7 → hippius-0.2.9}/hippius_sdk/utils.py +0 -0
@@ -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
|
375
|
-
|
376
|
-
|
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
|
-
|
382
|
-
|
383
|
-
|
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
|
-
|
387
|
-
|
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
|
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,
|
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
|
750
|
-
|
751
|
-
if
|
752
|
-
account_address =
|
753
|
-
|
754
|
-
|
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
|
-
|
885
|
+
# Fetch the content
|
886
|
+
cat_result = await client.ipfs_client.cat(cid)
|
866
887
|
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
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
|
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:
|
@@ -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
|
-
|
63
|
-
|
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
|
-
|
79
|
-
|
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
|
162
|
+
Dict with links information and is_directory flag
|
153
163
|
"""
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
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
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
if (
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
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.
|
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.
|
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.
|
295
|
+
await self.download_directory(link_hash, target_path)
|
376
296
|
else:
|
377
297
|
# It's a file - download it
|
378
298
|
try:
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|