hypercli-sdk 0.8.7__tar.gz → 0.8.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.
- {hypercli_sdk-0.8.7 → hypercli_sdk-0.8.9}/PKG-INFO +1 -1
- {hypercli_sdk-0.8.7 → hypercli_sdk-0.8.9}/hypercli/__init__.py +4 -2
- {hypercli_sdk-0.8.7 → hypercli_sdk-0.8.9}/hypercli/x402.py +124 -17
- {hypercli_sdk-0.8.7 → hypercli_sdk-0.8.9}/pyproject.toml +1 -1
- {hypercli_sdk-0.8.7 → hypercli_sdk-0.8.9}/.gitignore +0 -0
- {hypercli_sdk-0.8.7 → hypercli_sdk-0.8.9}/README.md +0 -0
- {hypercli_sdk-0.8.7 → hypercli_sdk-0.8.9}/hypercli/billing.py +0 -0
- {hypercli_sdk-0.8.7 → hypercli_sdk-0.8.9}/hypercli/claw.py +0 -0
- {hypercli_sdk-0.8.7 → hypercli_sdk-0.8.9}/hypercli/client.py +0 -0
- {hypercli_sdk-0.8.7 → hypercli_sdk-0.8.9}/hypercli/config.py +0 -0
- {hypercli_sdk-0.8.7 → hypercli_sdk-0.8.9}/hypercli/files.py +0 -0
- {hypercli_sdk-0.8.7 → hypercli_sdk-0.8.9}/hypercli/http.py +0 -0
- {hypercli_sdk-0.8.7 → hypercli_sdk-0.8.9}/hypercli/instances.py +0 -0
- {hypercli_sdk-0.8.7 → hypercli_sdk-0.8.9}/hypercli/job/__init__.py +0 -0
- {hypercli_sdk-0.8.7 → hypercli_sdk-0.8.9}/hypercli/job/base.py +0 -0
- {hypercli_sdk-0.8.7 → hypercli_sdk-0.8.9}/hypercli/job/comfyui.py +0 -0
- {hypercli_sdk-0.8.7 → hypercli_sdk-0.8.9}/hypercli/job/gradio.py +0 -0
- {hypercli_sdk-0.8.7 → hypercli_sdk-0.8.9}/hypercli/jobs.py +0 -0
- {hypercli_sdk-0.8.7 → hypercli_sdk-0.8.9}/hypercli/keys.py +0 -0
- {hypercli_sdk-0.8.7 → hypercli_sdk-0.8.9}/hypercli/logs.py +0 -0
- {hypercli_sdk-0.8.7 → hypercli_sdk-0.8.9}/hypercli/renders.py +0 -0
- {hypercli_sdk-0.8.7 → hypercli_sdk-0.8.9}/hypercli/user.py +0 -0
- {hypercli_sdk-0.8.7 → hypercli_sdk-0.8.9}/tests/test_apply_params.py +0 -0
- {hypercli_sdk-0.8.7 → hypercli_sdk-0.8.9}/tests/test_claw.py +0 -0
- {hypercli_sdk-0.8.7 → hypercli_sdk-0.8.9}/tests/test_graph_to_api.py +0 -0
|
@@ -5,13 +5,13 @@ from .http import APIError, AsyncHTTPClient
|
|
|
5
5
|
from .instances import GPUType, GPUConfig, Region, GPUPricing, PricingTier
|
|
6
6
|
from .jobs import Job, JobMetrics, GPUMetrics, find_job, find_by_id, find_by_hostname, find_by_ip
|
|
7
7
|
from .renders import Render, RenderStatus
|
|
8
|
-
from .x402 import X402Client, X402JobLaunch, X402RenderCreate
|
|
8
|
+
from .x402 import X402Client, X402JobLaunch, X402FlowCreate, X402RenderCreate, FlowCatalogItem
|
|
9
9
|
from .files import File, AsyncFiles
|
|
10
10
|
from .job import BaseJob, ComfyUIJob, GradioJob, apply_params, apply_graph_modes, find_node, find_nodes, load_template, graph_to_api, expand_subgraphs, DEFAULT_OBJECT_INFO
|
|
11
11
|
from .logs import LogStream, stream_logs, fetch_logs
|
|
12
12
|
from .claw import Claw, ClawKey, ClawPlan, ClawModel
|
|
13
13
|
|
|
14
|
-
__version__ = "0.8.
|
|
14
|
+
__version__ = "0.8.9"
|
|
15
15
|
__all__ = [
|
|
16
16
|
"HyperCLI",
|
|
17
17
|
"configure",
|
|
@@ -35,7 +35,9 @@ __all__ = [
|
|
|
35
35
|
# x402 API
|
|
36
36
|
"X402Client",
|
|
37
37
|
"X402JobLaunch",
|
|
38
|
+
"X402FlowCreate",
|
|
38
39
|
"X402RenderCreate",
|
|
40
|
+
"FlowCatalogItem",
|
|
39
41
|
# Files API
|
|
40
42
|
"File",
|
|
41
43
|
"AsyncFiles",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""x402 payment helpers for pay-per-use job and
|
|
1
|
+
"""x402 payment helpers for pay-per-use job and flow launches."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import base64
|
|
@@ -35,8 +35,8 @@ class X402JobLaunch:
|
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
@dataclass
|
|
38
|
-
class
|
|
39
|
-
"""Response payload for x402 render creation."""
|
|
38
|
+
class X402FlowCreate:
|
|
39
|
+
"""Response payload for x402 flow render creation."""
|
|
40
40
|
|
|
41
41
|
render: Render
|
|
42
42
|
access_key: str
|
|
@@ -44,7 +44,7 @@ class X402RenderCreate:
|
|
|
44
44
|
cancel_url: str
|
|
45
45
|
|
|
46
46
|
@classmethod
|
|
47
|
-
def from_dict(cls, data: dict[str, Any]) -> "
|
|
47
|
+
def from_dict(cls, data: dict[str, Any]) -> "X402FlowCreate":
|
|
48
48
|
return cls(
|
|
49
49
|
render=Render.from_dict(data.get("render", {})),
|
|
50
50
|
access_key=data.get("access_key", ""),
|
|
@@ -53,6 +53,34 @@ class X402RenderCreate:
|
|
|
53
53
|
)
|
|
54
54
|
|
|
55
55
|
|
|
56
|
+
@dataclass
|
|
57
|
+
class FlowCatalogItem:
|
|
58
|
+
"""Public flow catalog entry."""
|
|
59
|
+
|
|
60
|
+
flow_type: str
|
|
61
|
+
price_usd: float
|
|
62
|
+
template: str | None = None
|
|
63
|
+
type: str = "comfyui"
|
|
64
|
+
regions: dict[str, str] | None = None
|
|
65
|
+
interruptible: bool | None = None
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_dict(cls, data: dict[str, Any]) -> "FlowCatalogItem":
|
|
69
|
+
flow_type = str(data.get("flow_type") or data.get("name") or "")
|
|
70
|
+
return cls(
|
|
71
|
+
flow_type=flow_type,
|
|
72
|
+
price_usd=float(data.get("price_usd", 0)),
|
|
73
|
+
template=data.get("template"),
|
|
74
|
+
type=str(data.get("type", "comfyui")),
|
|
75
|
+
regions=data.get("regions") if isinstance(data.get("regions"), dict) else {},
|
|
76
|
+
interruptible=data.get("interruptible"),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# Backward-compat alias. Raw x402 render endpoint was replaced by flow endpoints.
|
|
81
|
+
X402RenderCreate = X402FlowCreate
|
|
82
|
+
|
|
83
|
+
|
|
56
84
|
def _require_x402_deps():
|
|
57
85
|
try:
|
|
58
86
|
from x402 import x402ClientSync
|
|
@@ -62,7 +90,7 @@ def _require_x402_deps():
|
|
|
62
90
|
return x402ClientSync, x402HTTPClientSync, EthAccountSigner, register_exact_evm_client
|
|
63
91
|
except ImportError as exc:
|
|
64
92
|
raise RuntimeError(
|
|
65
|
-
"x402 dependencies missing. Install with: pip install
|
|
93
|
+
"x402 dependencies missing. Install with: pip install x402[httpx,evm] eth-account"
|
|
66
94
|
) from exc
|
|
67
95
|
|
|
68
96
|
|
|
@@ -111,13 +139,77 @@ def _x402_post(
|
|
|
111
139
|
return data
|
|
112
140
|
|
|
113
141
|
|
|
142
|
+
def _json_get(base_url: str, path: str, timeout: float) -> Any:
|
|
143
|
+
endpoint = f"{base_url.rstrip('/')}{path}"
|
|
144
|
+
with httpx.Client(timeout=timeout) as client:
|
|
145
|
+
response = client.get(endpoint)
|
|
146
|
+
if response.status_code >= 400:
|
|
147
|
+
raise APIError(response.status_code, _error_detail(response))
|
|
148
|
+
return response.json()
|
|
149
|
+
|
|
150
|
+
|
|
114
151
|
class X402Client:
|
|
115
|
-
"""x402 pay-per-use client for launching jobs and renders without a full API account."""
|
|
152
|
+
"""x402 pay-per-use client for launching jobs and flow renders without a full API account."""
|
|
116
153
|
|
|
117
154
|
def __init__(self, api_url: str | None = None, timeout: float = 30.0):
|
|
118
155
|
self.api_url = (api_url or get_api_url()).rstrip("/")
|
|
119
156
|
self.timeout = timeout
|
|
120
157
|
|
|
158
|
+
def get_flow_catalog(self) -> list[FlowCatalogItem]:
|
|
159
|
+
data = _json_get(self.api_url, "/flows", self.timeout)
|
|
160
|
+
|
|
161
|
+
rows: list[dict[str, Any]] = []
|
|
162
|
+
if isinstance(data, list):
|
|
163
|
+
# Backward compatibility with old /api/flow/public response shape
|
|
164
|
+
rows = [row for row in data if isinstance(row, dict)]
|
|
165
|
+
elif isinstance(data, dict):
|
|
166
|
+
flows = data.get("flows")
|
|
167
|
+
if isinstance(flows, list):
|
|
168
|
+
rows = [row for row in flows if isinstance(row, dict)]
|
|
169
|
+
|
|
170
|
+
if not rows:
|
|
171
|
+
raise APIError(500, "Malformed flow catalog response")
|
|
172
|
+
|
|
173
|
+
expanded_rows: list[dict[str, Any]] = []
|
|
174
|
+
for row in rows:
|
|
175
|
+
flow_name = str(row.get("flow_type") or row.get("name") or "")
|
|
176
|
+
if not flow_name:
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
has_details = any(k in row for k in ("template", "regions", "type"))
|
|
180
|
+
if has_details:
|
|
181
|
+
expanded_rows.append(row)
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
flow_path = row.get("path")
|
|
185
|
+
if not isinstance(flow_path, str) or not flow_path:
|
|
186
|
+
flow_path = f"/flows/{flow_name}"
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
detail = _json_get(self.api_url, flow_path, self.timeout)
|
|
190
|
+
except APIError:
|
|
191
|
+
detail = {}
|
|
192
|
+
|
|
193
|
+
if isinstance(detail, dict):
|
|
194
|
+
# Preserve index price/path while augmenting with detail fields.
|
|
195
|
+
merged = {**detail, **row}
|
|
196
|
+
expanded_rows.append(merged)
|
|
197
|
+
else:
|
|
198
|
+
expanded_rows.append(row)
|
|
199
|
+
|
|
200
|
+
items = [FlowCatalogItem.from_dict(row) for row in expanded_rows]
|
|
201
|
+
return [item for item in items if item.flow_type]
|
|
202
|
+
|
|
203
|
+
def get_flow_price(self, flow_type: str) -> float:
|
|
204
|
+
if not flow_type:
|
|
205
|
+
raise ValueError("flow_type is required")
|
|
206
|
+
for item in self.get_flow_catalog():
|
|
207
|
+
if item.flow_type == flow_type:
|
|
208
|
+
if item.price_usd <= 0:
|
|
209
|
+
raise APIError(500, f"Flow {flow_type} has invalid configured price")
|
|
210
|
+
return item.price_usd
|
|
211
|
+
raise APIError(404, f"Flow {flow_type} not found in flow catalog")
|
|
212
|
+
|
|
121
213
|
def create_job(
|
|
122
214
|
self,
|
|
123
215
|
*,
|
|
@@ -159,25 +251,40 @@ class X402Client:
|
|
|
159
251
|
data = _x402_post(self.api_url, "/api/x402/job", payload, account, self.timeout)
|
|
160
252
|
return X402JobLaunch.from_dict(data)
|
|
161
253
|
|
|
162
|
-
def
|
|
254
|
+
def create_flow(
|
|
163
255
|
self,
|
|
164
256
|
*,
|
|
257
|
+
flow_type: str,
|
|
165
258
|
amount: float,
|
|
166
259
|
account: Any,
|
|
167
|
-
params: dict[str, Any],
|
|
168
|
-
render_type: str = "comfyui",
|
|
260
|
+
params: dict[str, Any] | None = None,
|
|
169
261
|
notify_url: str | None = None,
|
|
170
|
-
) ->
|
|
262
|
+
) -> X402FlowCreate:
|
|
171
263
|
if amount <= 0:
|
|
172
264
|
raise ValueError("amount must be greater than 0")
|
|
265
|
+
if not flow_type:
|
|
266
|
+
raise ValueError("flow_type is required")
|
|
173
267
|
|
|
174
|
-
|
|
175
|
-
"
|
|
176
|
-
"params": params,
|
|
268
|
+
payload: dict[str, Any] = {
|
|
269
|
+
"amount": amount,
|
|
270
|
+
"params": params or {},
|
|
177
271
|
}
|
|
178
272
|
if notify_url:
|
|
179
|
-
|
|
273
|
+
payload["notify_url"] = notify_url
|
|
180
274
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
275
|
+
data = _x402_post(self.api_url, f"/api/x402/flow/{flow_type}", payload, account, self.timeout)
|
|
276
|
+
return X402FlowCreate.from_dict(data)
|
|
277
|
+
|
|
278
|
+
def create_render(
|
|
279
|
+
self,
|
|
280
|
+
*,
|
|
281
|
+
amount: float,
|
|
282
|
+
account: Any,
|
|
283
|
+
params: dict[str, Any],
|
|
284
|
+
render_type: str = "comfyui",
|
|
285
|
+
notify_url: str | None = None,
|
|
286
|
+
) -> X402RenderCreate:
|
|
287
|
+
del amount, account, params, render_type, notify_url
|
|
288
|
+
raise RuntimeError(
|
|
289
|
+
"x402 render endpoint has been removed. Use create_flow(flow_type=..., amount=..., params=...) instead."
|
|
290
|
+
)
|
|
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
|
|
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
|