hypercli-sdk 0.8.1__tar.gz → 0.8.3__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.1 → hypercli_sdk-0.8.3}/PKG-INFO +1 -1
- {hypercli_sdk-0.8.1 → hypercli_sdk-0.8.3}/hypercli/__init__.py +6 -1
- hypercli_sdk-0.8.3/hypercli/x402.py +183 -0
- {hypercli_sdk-0.8.1 → hypercli_sdk-0.8.3}/pyproject.toml +1 -1
- {hypercli_sdk-0.8.1 → hypercli_sdk-0.8.3}/.gitignore +0 -0
- {hypercli_sdk-0.8.1 → hypercli_sdk-0.8.3}/README.md +0 -0
- {hypercli_sdk-0.8.1 → hypercli_sdk-0.8.3}/hypercli/billing.py +0 -0
- {hypercli_sdk-0.8.1 → hypercli_sdk-0.8.3}/hypercli/claw.py +0 -0
- {hypercli_sdk-0.8.1 → hypercli_sdk-0.8.3}/hypercli/client.py +0 -0
- {hypercli_sdk-0.8.1 → hypercli_sdk-0.8.3}/hypercli/config.py +0 -0
- {hypercli_sdk-0.8.1 → hypercli_sdk-0.8.3}/hypercli/files.py +0 -0
- {hypercli_sdk-0.8.1 → hypercli_sdk-0.8.3}/hypercli/http.py +0 -0
- {hypercli_sdk-0.8.1 → hypercli_sdk-0.8.3}/hypercli/instances.py +0 -0
- {hypercli_sdk-0.8.1 → hypercli_sdk-0.8.3}/hypercli/job/__init__.py +0 -0
- {hypercli_sdk-0.8.1 → hypercli_sdk-0.8.3}/hypercli/job/base.py +0 -0
- {hypercli_sdk-0.8.1 → hypercli_sdk-0.8.3}/hypercli/job/comfyui.py +0 -0
- {hypercli_sdk-0.8.1 → hypercli_sdk-0.8.3}/hypercli/job/gradio.py +0 -0
- {hypercli_sdk-0.8.1 → hypercli_sdk-0.8.3}/hypercli/jobs.py +0 -0
- {hypercli_sdk-0.8.1 → hypercli_sdk-0.8.3}/hypercli/keys.py +0 -0
- {hypercli_sdk-0.8.1 → hypercli_sdk-0.8.3}/hypercli/logs.py +0 -0
- {hypercli_sdk-0.8.1 → hypercli_sdk-0.8.3}/hypercli/renders.py +0 -0
- {hypercli_sdk-0.8.1 → hypercli_sdk-0.8.3}/hypercli/user.py +0 -0
- {hypercli_sdk-0.8.1 → hypercli_sdk-0.8.3}/tests/test_apply_params.py +0 -0
- {hypercli_sdk-0.8.1 → hypercli_sdk-0.8.3}/tests/test_claw.py +0 -0
- {hypercli_sdk-0.8.1 → hypercli_sdk-0.8.3}/tests/test_graph_to_api.py +0 -0
|
@@ -5,12 +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
9
|
from .files import File, AsyncFiles
|
|
9
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
|
|
10
11
|
from .logs import LogStream, stream_logs, fetch_logs
|
|
11
12
|
from .claw import Claw, ClawKey, ClawPlan, ClawModel
|
|
12
13
|
|
|
13
|
-
__version__ = "0.
|
|
14
|
+
__version__ = "0.8.3"
|
|
14
15
|
__all__ = [
|
|
15
16
|
"HyperCLI",
|
|
16
17
|
"configure",
|
|
@@ -31,6 +32,10 @@ __all__ = [
|
|
|
31
32
|
# Renders API
|
|
32
33
|
"Render",
|
|
33
34
|
"RenderStatus",
|
|
35
|
+
# x402 API
|
|
36
|
+
"X402Client",
|
|
37
|
+
"X402JobLaunch",
|
|
38
|
+
"X402RenderCreate",
|
|
34
39
|
# Files API
|
|
35
40
|
"File",
|
|
36
41
|
"AsyncFiles",
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""x402 payment helpers for pay-per-use job and render launches."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .config import get_api_url
|
|
11
|
+
from .http import APIError
|
|
12
|
+
from .jobs import Job
|
|
13
|
+
from .renders import Render
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class X402JobLaunch:
|
|
18
|
+
"""Response payload for x402 job launch."""
|
|
19
|
+
|
|
20
|
+
job: Job
|
|
21
|
+
access_key: str
|
|
22
|
+
status_url: str
|
|
23
|
+
logs_url: str
|
|
24
|
+
cancel_url: str
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def from_dict(cls, data: dict[str, Any]) -> "X402JobLaunch":
|
|
28
|
+
return cls(
|
|
29
|
+
job=Job.from_dict(data.get("job", {})),
|
|
30
|
+
access_key=data.get("access_key", ""),
|
|
31
|
+
status_url=data.get("status_url", ""),
|
|
32
|
+
logs_url=data.get("logs_url", ""),
|
|
33
|
+
cancel_url=data.get("cancel_url", ""),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class X402RenderCreate:
|
|
39
|
+
"""Response payload for x402 render creation."""
|
|
40
|
+
|
|
41
|
+
render: Render
|
|
42
|
+
access_key: str
|
|
43
|
+
status_url: str
|
|
44
|
+
cancel_url: str
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_dict(cls, data: dict[str, Any]) -> "X402RenderCreate":
|
|
48
|
+
return cls(
|
|
49
|
+
render=Render.from_dict(data.get("render", {})),
|
|
50
|
+
access_key=data.get("access_key", ""),
|
|
51
|
+
status_url=data.get("status_url", ""),
|
|
52
|
+
cancel_url=data.get("cancel_url", ""),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _require_x402_deps():
|
|
57
|
+
try:
|
|
58
|
+
from x402 import x402ClientSync
|
|
59
|
+
from x402.http import x402HTTPClientSync
|
|
60
|
+
from x402.mechanisms.evm import EthAccountSigner
|
|
61
|
+
from x402.mechanisms.evm.exact.register import register_exact_evm_client
|
|
62
|
+
return x402ClientSync, x402HTTPClientSync, EthAccountSigner, register_exact_evm_client
|
|
63
|
+
except ImportError as exc:
|
|
64
|
+
raise RuntimeError(
|
|
65
|
+
"x402 dependencies missing. Install with: pip install 'x402[httpx,evm]' eth-account"
|
|
66
|
+
) from exc
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _error_detail(response: httpx.Response) -> str:
|
|
70
|
+
try:
|
|
71
|
+
data = response.json()
|
|
72
|
+
if isinstance(data, dict):
|
|
73
|
+
return str(data.get("detail") or data.get("message") or data)
|
|
74
|
+
return str(data)
|
|
75
|
+
except Exception:
|
|
76
|
+
return response.text
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _x402_post(
|
|
80
|
+
base_url: str,
|
|
81
|
+
path: str,
|
|
82
|
+
payload: dict[str, Any],
|
|
83
|
+
account: Any,
|
|
84
|
+
timeout: float,
|
|
85
|
+
) -> dict[str, Any]:
|
|
86
|
+
x402ClientSync, x402HTTPClientSync, EthAccountSigner, register_exact_evm_client = _require_x402_deps()
|
|
87
|
+
|
|
88
|
+
signer = EthAccountSigner(account)
|
|
89
|
+
x402_client = x402ClientSync()
|
|
90
|
+
register_exact_evm_client(x402_client, signer)
|
|
91
|
+
http_client = x402HTTPClientSync(x402_client)
|
|
92
|
+
|
|
93
|
+
endpoint = f"{base_url.rstrip('/')}{path}"
|
|
94
|
+
headers = {"Content-Type": "application/json"}
|
|
95
|
+
|
|
96
|
+
with httpx.Client(timeout=timeout) as client:
|
|
97
|
+
response = client.post(endpoint, headers=headers, json=payload)
|
|
98
|
+
|
|
99
|
+
if response.status_code == 402:
|
|
100
|
+
payment_headers, _ = http_client.handle_402_response(dict(response.headers), response.content)
|
|
101
|
+
retry_headers = {**headers, **payment_headers}
|
|
102
|
+
retry_headers["Access-Control-Expose-Headers"] = "PAYMENT-RESPONSE,X-PAYMENT-RESPONSE"
|
|
103
|
+
response = client.post(endpoint, headers=retry_headers, json=payload)
|
|
104
|
+
|
|
105
|
+
if response.status_code >= 400:
|
|
106
|
+
raise APIError(response.status_code, _error_detail(response))
|
|
107
|
+
|
|
108
|
+
data = response.json()
|
|
109
|
+
if not isinstance(data, dict):
|
|
110
|
+
raise APIError(response.status_code, "Malformed API response")
|
|
111
|
+
return data
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class X402Client:
|
|
115
|
+
"""x402 pay-per-use client for launching jobs and renders without a full API account."""
|
|
116
|
+
|
|
117
|
+
def __init__(self, api_url: str | None = None, timeout: float = 30.0):
|
|
118
|
+
self.api_url = (api_url or get_api_url()).rstrip("/")
|
|
119
|
+
self.timeout = timeout
|
|
120
|
+
|
|
121
|
+
def create_job(
|
|
122
|
+
self,
|
|
123
|
+
*,
|
|
124
|
+
amount: float,
|
|
125
|
+
account: Any,
|
|
126
|
+
image: str,
|
|
127
|
+
command: str | None = None,
|
|
128
|
+
gpu_type: str = "l40s",
|
|
129
|
+
gpu_count: int = 1,
|
|
130
|
+
region: str | None = None,
|
|
131
|
+
interruptible: bool = True,
|
|
132
|
+
env: dict[str, str] | None = None,
|
|
133
|
+
ports: dict[str, int] | None = None,
|
|
134
|
+
auth: bool = False,
|
|
135
|
+
registry_auth: dict[str, str] | None = None,
|
|
136
|
+
) -> X402JobLaunch:
|
|
137
|
+
if amount <= 0:
|
|
138
|
+
raise ValueError("amount must be greater than 0")
|
|
139
|
+
|
|
140
|
+
job_payload: dict[str, Any] = {
|
|
141
|
+
"docker_image": image,
|
|
142
|
+
"gpu_type": gpu_type,
|
|
143
|
+
"gpu_count": gpu_count,
|
|
144
|
+
"interruptible": interruptible,
|
|
145
|
+
"command": base64.b64encode((command or "").encode()).decode(),
|
|
146
|
+
}
|
|
147
|
+
if region:
|
|
148
|
+
job_payload["region"] = region
|
|
149
|
+
if env:
|
|
150
|
+
job_payload["env_vars"] = env
|
|
151
|
+
if ports:
|
|
152
|
+
job_payload["ports"] = ports
|
|
153
|
+
if auth:
|
|
154
|
+
job_payload["auth"] = auth
|
|
155
|
+
if registry_auth:
|
|
156
|
+
job_payload["registry_auth"] = registry_auth
|
|
157
|
+
|
|
158
|
+
payload = {"amount": amount, "job": job_payload}
|
|
159
|
+
data = _x402_post(self.api_url, "/api/x402/job", payload, account, self.timeout)
|
|
160
|
+
return X402JobLaunch.from_dict(data)
|
|
161
|
+
|
|
162
|
+
def create_render(
|
|
163
|
+
self,
|
|
164
|
+
*,
|
|
165
|
+
amount: float,
|
|
166
|
+
account: Any,
|
|
167
|
+
params: dict[str, Any],
|
|
168
|
+
render_type: str = "comfyui",
|
|
169
|
+
notify_url: str | None = None,
|
|
170
|
+
) -> X402RenderCreate:
|
|
171
|
+
if amount <= 0:
|
|
172
|
+
raise ValueError("amount must be greater than 0")
|
|
173
|
+
|
|
174
|
+
render_payload: dict[str, Any] = {
|
|
175
|
+
"type": render_type,
|
|
176
|
+
"params": params,
|
|
177
|
+
}
|
|
178
|
+
if notify_url:
|
|
179
|
+
render_payload["notify_url"] = notify_url
|
|
180
|
+
|
|
181
|
+
payload = {"amount": amount, "render": render_payload}
|
|
182
|
+
data = _x402_post(self.api_url, "/api/x402/render", payload, account, self.timeout)
|
|
183
|
+
return X402RenderCreate.from_dict(data)
|
|
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
|