hippius 0.1.14__py3-none-any.whl → 0.2.1__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.
- {hippius-0.1.14.dist-info → hippius-0.2.1.dist-info}/METADATA +3 -2
- hippius-0.2.1.dist-info/RECORD +12 -0
- hippius_sdk/__init__.py +5 -1
- hippius_sdk/cli.py +810 -89
- hippius_sdk/client.py +20 -22
- hippius_sdk/config.py +19 -26
- hippius_sdk/ipfs.py +186 -428
- hippius_sdk/ipfs_core.py +216 -0
- hippius_sdk/substrate.py +699 -67
- hippius_sdk/utils.py +152 -0
- hippius-0.1.14.dist-info/RECORD +0 -10
- {hippius-0.1.14.dist-info → hippius-0.2.1.dist-info}/WHEEL +0 -0
- {hippius-0.1.14.dist-info → hippius-0.2.1.dist-info}/entry_points.txt +0 -0
hippius_sdk/ipfs_core.py
ADDED
@@ -0,0 +1,216 @@
|
|
1
|
+
import json
|
2
|
+
import os
|
3
|
+
from typing import Any, Dict
|
4
|
+
|
5
|
+
import httpx
|
6
|
+
|
7
|
+
|
8
|
+
class AsyncIPFSClient:
|
9
|
+
"""
|
10
|
+
Asynchronous IPFS client using httpx.
|
11
|
+
"""
|
12
|
+
|
13
|
+
def __init__(
|
14
|
+
self, api_url: str = "http://localhost:5001", gateway: str = "https://ipfs.io"
|
15
|
+
):
|
16
|
+
# Handle multiaddr format
|
17
|
+
if api_url and api_url.startswith("/"):
|
18
|
+
# Extract host and port from multiaddr
|
19
|
+
try:
|
20
|
+
parts = api_url.split("/")
|
21
|
+
# Handle /ip4/127.0.0.1/tcp/5001
|
22
|
+
if len(parts) >= 5 and parts[1] in ["ip4", "ip6"]:
|
23
|
+
host = parts[2]
|
24
|
+
port = parts[4]
|
25
|
+
api_url = f"https://{host}:{port}"
|
26
|
+
print(f"Converted multiaddr {api_url} to HTTP URL {api_url}")
|
27
|
+
else:
|
28
|
+
print(f"Warning: Unsupported multiaddr format: {api_url}")
|
29
|
+
print("Falling back to default: http://localhost:5001")
|
30
|
+
api_url = "http://localhost:5001"
|
31
|
+
except Exception as e:
|
32
|
+
print(f"Error parsing multiaddr: {e}")
|
33
|
+
print("Falling back to default: http://localhost:5001")
|
34
|
+
api_url = "http://localhost:5001"
|
35
|
+
self.api_url = api_url
|
36
|
+
self.gateway = gateway
|
37
|
+
self.client = httpx.AsyncClient(timeout=60.0)
|
38
|
+
|
39
|
+
async def close(self):
|
40
|
+
"""Close the httpx client."""
|
41
|
+
await self.client.aclose()
|
42
|
+
|
43
|
+
async def __aenter__(self):
|
44
|
+
return self
|
45
|
+
|
46
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
47
|
+
await self.close()
|
48
|
+
|
49
|
+
async def add_file(self, file_path: str) -> Dict[str, Any]:
|
50
|
+
"""
|
51
|
+
Add a file to IPFS.
|
52
|
+
|
53
|
+
Args:
|
54
|
+
file_path: Path to the file to add
|
55
|
+
|
56
|
+
Returns:
|
57
|
+
Dict containing the CID and other information
|
58
|
+
"""
|
59
|
+
with open(file_path, "rb") as f:
|
60
|
+
files = {"file": f}
|
61
|
+
response = await self.client.post(f"{self.api_url}/api/v0/add", files=files)
|
62
|
+
response.raise_for_status()
|
63
|
+
return response.json()
|
64
|
+
|
65
|
+
async def add_bytes(self, data: bytes, filename: str = "file") -> Dict[str, Any]:
|
66
|
+
"""
|
67
|
+
Add bytes to IPFS.
|
68
|
+
|
69
|
+
Args:
|
70
|
+
data: Bytes to add
|
71
|
+
filename: Name to give the file (default: "file")
|
72
|
+
|
73
|
+
Returns:
|
74
|
+
Dict containing the CID and other information
|
75
|
+
"""
|
76
|
+
files = {"file": (filename, data)}
|
77
|
+
response = await self.client.post(f"{self.api_url}/api/v0/add", files=files)
|
78
|
+
response.raise_for_status()
|
79
|
+
return response.json()
|
80
|
+
|
81
|
+
async def add_str(self, content: str, filename: str = "file") -> Dict[str, Any]:
|
82
|
+
"""
|
83
|
+
Add a string to IPFS.
|
84
|
+
|
85
|
+
Args:
|
86
|
+
content: String to add
|
87
|
+
filename: Name to give the file (default: "file")
|
88
|
+
|
89
|
+
Returns:
|
90
|
+
Dict containing the CID and other information
|
91
|
+
"""
|
92
|
+
return await self.add_bytes(content.encode(), filename)
|
93
|
+
|
94
|
+
async def cat(self, cid: str) -> bytes:
|
95
|
+
"""
|
96
|
+
Retrieve content from IPFS by its CID.
|
97
|
+
|
98
|
+
Args:
|
99
|
+
cid: Content Identifier to retrieve
|
100
|
+
|
101
|
+
Returns:
|
102
|
+
Content as bytes
|
103
|
+
"""
|
104
|
+
response = await self.client.post(f"{self.api_url}/api/v0/cat?arg={cid}")
|
105
|
+
response.raise_for_status()
|
106
|
+
return response.content
|
107
|
+
|
108
|
+
async def pin(self, cid: str) -> Dict[str, Any]:
|
109
|
+
"""
|
110
|
+
Pin content by CID.
|
111
|
+
|
112
|
+
Args:
|
113
|
+
cid: Content Identifier to pin
|
114
|
+
|
115
|
+
Returns:
|
116
|
+
Response from the IPFS node
|
117
|
+
"""
|
118
|
+
response = await self.client.post(f"{self.api_url}/api/v0/pin/add?arg={cid}")
|
119
|
+
response.raise_for_status()
|
120
|
+
return response.json()
|
121
|
+
|
122
|
+
async def ls(self, cid: str) -> Dict[str, Any]:
|
123
|
+
"""
|
124
|
+
List objects linked to the specified CID.
|
125
|
+
|
126
|
+
Args:
|
127
|
+
cid: Content Identifier
|
128
|
+
|
129
|
+
Returns:
|
130
|
+
Dict with links information
|
131
|
+
"""
|
132
|
+
response = await self.client.post(f"{self.api_url}/api/v0/ls?arg={cid}")
|
133
|
+
response.raise_for_status()
|
134
|
+
return response.json()
|
135
|
+
|
136
|
+
async def exists(self, cid: str) -> bool:
|
137
|
+
"""
|
138
|
+
Check if content exists.
|
139
|
+
|
140
|
+
Args:
|
141
|
+
cid: Content Identifier to check
|
142
|
+
|
143
|
+
Returns:
|
144
|
+
True if content exists, False otherwise
|
145
|
+
"""
|
146
|
+
try:
|
147
|
+
await self.client.head(f"{self.gateway}/ipfs/{cid}")
|
148
|
+
return True
|
149
|
+
except httpx.HTTPError:
|
150
|
+
return False
|
151
|
+
|
152
|
+
async def download_file(self, cid: str, output_path: str) -> str:
|
153
|
+
"""
|
154
|
+
Download content from IPFS to a file.
|
155
|
+
|
156
|
+
Args:
|
157
|
+
cid: Content identifier
|
158
|
+
output_path: Path where to save the file
|
159
|
+
|
160
|
+
Returns:
|
161
|
+
Path to the saved file
|
162
|
+
"""
|
163
|
+
content = await self.cat(cid)
|
164
|
+
with open(output_path, "wb") as f:
|
165
|
+
f.write(content)
|
166
|
+
return output_path
|
167
|
+
|
168
|
+
async def add_directory(
|
169
|
+
self, dir_path: str, recursive: bool = True
|
170
|
+
) -> Dict[str, Any]:
|
171
|
+
"""
|
172
|
+
Add a directory to IPFS.
|
173
|
+
|
174
|
+
Args:
|
175
|
+
dir_path: Path to the directory to add
|
176
|
+
|
177
|
+
Returns:
|
178
|
+
Dict containing the CID and other information about the directory
|
179
|
+
|
180
|
+
Raises:
|
181
|
+
FileNotFoundError: If the directory doesn't exist
|
182
|
+
httpx.HTTPError: If the IPFS API request fails
|
183
|
+
"""
|
184
|
+
if not os.path.isdir(dir_path):
|
185
|
+
raise FileNotFoundError(f"Directory {dir_path} not found")
|
186
|
+
|
187
|
+
# Collect all files in the directory
|
188
|
+
files = []
|
189
|
+
for root, _, filenames in os.walk(dir_path):
|
190
|
+
for filename in filenames:
|
191
|
+
file_path = os.path.join(root, filename)
|
192
|
+
rel_path = os.path.relpath(file_path, dir_path)
|
193
|
+
|
194
|
+
with open(file_path, "rb") as f:
|
195
|
+
file_content = f.read()
|
196
|
+
|
197
|
+
# Add the file to the multipart request
|
198
|
+
files.append(
|
199
|
+
("file", (rel_path, file_content, "application/octet-stream"))
|
200
|
+
)
|
201
|
+
|
202
|
+
# Make the request with directory flags
|
203
|
+
response = await self.client.post(
|
204
|
+
f"{self.api_url}/api/v0/add?recursive=true&wrap-with-directory=true",
|
205
|
+
files=files,
|
206
|
+
timeout=300.0, # 5 minute timeout for directory uploads
|
207
|
+
)
|
208
|
+
response.raise_for_status()
|
209
|
+
|
210
|
+
# The IPFS API returns a JSON object for each file, one per line
|
211
|
+
# The last one should be the directory itself
|
212
|
+
lines = response.text.strip().split("\n")
|
213
|
+
if not lines:
|
214
|
+
raise ValueError("Empty response from IPFS API")
|
215
|
+
|
216
|
+
return json.loads(lines[-1])
|