hypercli-sdk 0.4.2__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.4.2/.gitignore +51 -0
- hypercli_sdk-0.4.2/PKG-INFO +141 -0
- hypercli_sdk-0.4.2/README.md +114 -0
- hypercli_sdk-0.4.2/c3/__init__.py +57 -0
- hypercli_sdk-0.4.2/c3/billing.py +72 -0
- hypercli_sdk-0.4.2/c3/client.py +60 -0
- hypercli_sdk-0.4.2/c3/config.py +70 -0
- hypercli_sdk-0.4.2/c3/files.py +386 -0
- hypercli_sdk-0.4.2/c3/http.py +217 -0
- hypercli_sdk-0.4.2/c3/instances.py +211 -0
- hypercli_sdk-0.4.2/c3/job/__init__.py +24 -0
- hypercli_sdk-0.4.2/c3/job/base.py +249 -0
- hypercli_sdk-0.4.2/c3/job/comfyui.py +1469 -0
- hypercli_sdk-0.4.2/c3/jobs.py +285 -0
- hypercli_sdk-0.4.2/c3/logs.py +273 -0
- hypercli_sdk-0.4.2/c3/renders.py +339 -0
- hypercli_sdk-0.4.2/c3/user.py +37 -0
- hypercli_sdk-0.4.2/pyproject.toml +48 -0
- hypercli_sdk-0.4.2/tests/test_apply_params.py +311 -0
- hypercli_sdk-0.4.2/tests/test_graph_to_api.py +631 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
.pnp
|
|
4
|
+
.pnp.js
|
|
5
|
+
|
|
6
|
+
# Python
|
|
7
|
+
__pycache__/
|
|
8
|
+
*.py[cod]
|
|
9
|
+
*$py.class
|
|
10
|
+
*.so
|
|
11
|
+
.Python
|
|
12
|
+
venv/
|
|
13
|
+
env/
|
|
14
|
+
ENV/
|
|
15
|
+
.venv/
|
|
16
|
+
*.egg-info/
|
|
17
|
+
|
|
18
|
+
# Build outputs
|
|
19
|
+
.next
|
|
20
|
+
out
|
|
21
|
+
dist
|
|
22
|
+
build
|
|
23
|
+
|
|
24
|
+
# Testing
|
|
25
|
+
coverage
|
|
26
|
+
|
|
27
|
+
# Misc
|
|
28
|
+
.DS_Store
|
|
29
|
+
*.pem
|
|
30
|
+
|
|
31
|
+
# Debug
|
|
32
|
+
npm-debug.log*
|
|
33
|
+
yarn-debug.log*
|
|
34
|
+
yarn-error.log*
|
|
35
|
+
|
|
36
|
+
# Local env files
|
|
37
|
+
.env
|
|
38
|
+
.env.local
|
|
39
|
+
.env.development.local
|
|
40
|
+
.env.test.local
|
|
41
|
+
.env.production.local
|
|
42
|
+
|
|
43
|
+
# Vercel
|
|
44
|
+
.vercel
|
|
45
|
+
|
|
46
|
+
# TypeScript
|
|
47
|
+
*.tsbuildinfo
|
|
48
|
+
next-env.d.ts
|
|
49
|
+
|
|
50
|
+
# Turbo
|
|
51
|
+
.turbo
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hypercli-sdk
|
|
3
|
+
Version: 0.4.2
|
|
4
|
+
Summary: Python SDK for HyperCLI - GPU orchestration and LLM API
|
|
5
|
+
Project-URL: Homepage, https://hypercli.com
|
|
6
|
+
Project-URL: Documentation, https://docs.hypercli.com
|
|
7
|
+
Project-URL: Repository, https://github.com/hypercli/sdk-python
|
|
8
|
+
Author-email: HyperCLI <support@hypercli.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: httpx>=0.28.1
|
|
18
|
+
Requires-Dist: websockets>=15.0.1
|
|
19
|
+
Provides-Extra: comfyui
|
|
20
|
+
Requires-Dist: comfyui-workflow-templates-media-image>=0.3.0; extra == 'comfyui'
|
|
21
|
+
Requires-Dist: comfyui-workflow-templates>=0.7.0; extra == 'comfyui'
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: ruff>=0.3.0; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# C3 SDK
|
|
29
|
+
|
|
30
|
+
Python SDK for [HyperCLI](https://hypercli.com) - GPU orchestration API.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install c3-sdk
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Setup
|
|
39
|
+
|
|
40
|
+
Set your API key:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
export C3_API_KEY=your_api_key
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or create `~/.c3/config`:
|
|
47
|
+
```
|
|
48
|
+
C3_API_KEY=your_api_key
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or pass directly:
|
|
52
|
+
```python
|
|
53
|
+
c3 = C3(api_key="your_api_key")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from c3 import C3
|
|
60
|
+
|
|
61
|
+
c3 = C3()
|
|
62
|
+
|
|
63
|
+
# Check balance
|
|
64
|
+
balance = c3.billing.balance()
|
|
65
|
+
print(f"Balance: ${balance.total:.2f}")
|
|
66
|
+
print(f"Rewards: ${balance.rewards:.2f}")
|
|
67
|
+
|
|
68
|
+
# List transactions
|
|
69
|
+
for tx in c3.billing.transactions(limit=10):
|
|
70
|
+
print(f"{tx.transaction_type}: ${tx.amount_usd:.4f}")
|
|
71
|
+
|
|
72
|
+
# Create a job
|
|
73
|
+
job = c3.jobs.create(
|
|
74
|
+
image="nvidia/cuda:12.0",
|
|
75
|
+
command="python train.py",
|
|
76
|
+
gpu_type="l40s",
|
|
77
|
+
gpu_count=1,
|
|
78
|
+
)
|
|
79
|
+
print(f"Job ID: {job.job_id}")
|
|
80
|
+
print(f"State: {job.state}")
|
|
81
|
+
|
|
82
|
+
# List jobs
|
|
83
|
+
for job in c3.jobs.list():
|
|
84
|
+
print(f"{job.job_id}: {job.state}")
|
|
85
|
+
|
|
86
|
+
# Get job details
|
|
87
|
+
job = c3.jobs.get("job_id")
|
|
88
|
+
|
|
89
|
+
# Get job logs
|
|
90
|
+
logs = c3.jobs.logs("job_id")
|
|
91
|
+
|
|
92
|
+
# Get GPU metrics
|
|
93
|
+
metrics = c3.jobs.metrics("job_id")
|
|
94
|
+
for gpu in metrics.gpus:
|
|
95
|
+
print(f"GPU {gpu.index}: {gpu.utilization}% util, {gpu.temperature}°C")
|
|
96
|
+
|
|
97
|
+
# Cancel a job
|
|
98
|
+
c3.jobs.cancel("job_id")
|
|
99
|
+
|
|
100
|
+
# Extend runtime
|
|
101
|
+
c3.jobs.extend("job_id", runtime=7200)
|
|
102
|
+
|
|
103
|
+
# Get user info
|
|
104
|
+
user = c3.user.get()
|
|
105
|
+
print(f"User: {user.email}")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## LLM API
|
|
109
|
+
|
|
110
|
+
For LLM access, use the OpenAI SDK with C3's base URL:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from openai import OpenAI
|
|
114
|
+
|
|
115
|
+
client = OpenAI(
|
|
116
|
+
api_key="your_c3_api_key",
|
|
117
|
+
base_url="https://api.hypercli.com/v1"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
response = client.chat.completions.create(
|
|
121
|
+
model="deepseek-v3.1",
|
|
122
|
+
messages=[{"role": "user", "content": "Hello!"}]
|
|
123
|
+
)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Error Handling
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from c3 import C3, APIError
|
|
130
|
+
|
|
131
|
+
c3 = C3()
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
job = c3.jobs.get("invalid_id")
|
|
135
|
+
except APIError as e:
|
|
136
|
+
print(f"Error {e.status_code}: {e.detail}")
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# C3 SDK
|
|
2
|
+
|
|
3
|
+
Python SDK for [HyperCLI](https://hypercli.com) - GPU orchestration API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install c3-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
Set your API key:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
export C3_API_KEY=your_api_key
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or create `~/.c3/config`:
|
|
20
|
+
```
|
|
21
|
+
C3_API_KEY=your_api_key
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or pass directly:
|
|
25
|
+
```python
|
|
26
|
+
c3 = C3(api_key="your_api_key")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from c3 import C3
|
|
33
|
+
|
|
34
|
+
c3 = C3()
|
|
35
|
+
|
|
36
|
+
# Check balance
|
|
37
|
+
balance = c3.billing.balance()
|
|
38
|
+
print(f"Balance: ${balance.total:.2f}")
|
|
39
|
+
print(f"Rewards: ${balance.rewards:.2f}")
|
|
40
|
+
|
|
41
|
+
# List transactions
|
|
42
|
+
for tx in c3.billing.transactions(limit=10):
|
|
43
|
+
print(f"{tx.transaction_type}: ${tx.amount_usd:.4f}")
|
|
44
|
+
|
|
45
|
+
# Create a job
|
|
46
|
+
job = c3.jobs.create(
|
|
47
|
+
image="nvidia/cuda:12.0",
|
|
48
|
+
command="python train.py",
|
|
49
|
+
gpu_type="l40s",
|
|
50
|
+
gpu_count=1,
|
|
51
|
+
)
|
|
52
|
+
print(f"Job ID: {job.job_id}")
|
|
53
|
+
print(f"State: {job.state}")
|
|
54
|
+
|
|
55
|
+
# List jobs
|
|
56
|
+
for job in c3.jobs.list():
|
|
57
|
+
print(f"{job.job_id}: {job.state}")
|
|
58
|
+
|
|
59
|
+
# Get job details
|
|
60
|
+
job = c3.jobs.get("job_id")
|
|
61
|
+
|
|
62
|
+
# Get job logs
|
|
63
|
+
logs = c3.jobs.logs("job_id")
|
|
64
|
+
|
|
65
|
+
# Get GPU metrics
|
|
66
|
+
metrics = c3.jobs.metrics("job_id")
|
|
67
|
+
for gpu in metrics.gpus:
|
|
68
|
+
print(f"GPU {gpu.index}: {gpu.utilization}% util, {gpu.temperature}°C")
|
|
69
|
+
|
|
70
|
+
# Cancel a job
|
|
71
|
+
c3.jobs.cancel("job_id")
|
|
72
|
+
|
|
73
|
+
# Extend runtime
|
|
74
|
+
c3.jobs.extend("job_id", runtime=7200)
|
|
75
|
+
|
|
76
|
+
# Get user info
|
|
77
|
+
user = c3.user.get()
|
|
78
|
+
print(f"User: {user.email}")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## LLM API
|
|
82
|
+
|
|
83
|
+
For LLM access, use the OpenAI SDK with C3's base URL:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from openai import OpenAI
|
|
87
|
+
|
|
88
|
+
client = OpenAI(
|
|
89
|
+
api_key="your_c3_api_key",
|
|
90
|
+
base_url="https://api.hypercli.com/v1"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
response = client.chat.completions.create(
|
|
94
|
+
model="deepseek-v3.1",
|
|
95
|
+
messages=[{"role": "user", "content": "Hello!"}]
|
|
96
|
+
)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Error Handling
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from c3 import C3, APIError
|
|
103
|
+
|
|
104
|
+
c3 = C3()
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
job = c3.jobs.get("invalid_id")
|
|
108
|
+
except APIError as e:
|
|
109
|
+
print(f"Error {e.status_code}: {e.detail}")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""C3 SDK - Python client for HyperCLI API"""
|
|
2
|
+
from .client import C3
|
|
3
|
+
from .config import configure, GHCR_IMAGES, COMFYUI_IMAGE
|
|
4
|
+
from .http import APIError, AsyncHTTPClient
|
|
5
|
+
from .instances import GPUType, GPUConfig, Region, GPUPricing, PricingTier
|
|
6
|
+
from .jobs import Job, JobMetrics, GPUMetrics, find_job, find_by_id, find_by_hostname, find_by_ip
|
|
7
|
+
from .renders import Render, RenderStatus
|
|
8
|
+
from .files import File, AsyncFiles
|
|
9
|
+
from .job import BaseJob, ComfyUIJob, apply_params, apply_graph_modes, find_node, find_nodes, load_template, graph_to_api, DEFAULT_OBJECT_INFO
|
|
10
|
+
from .logs import LogStream, stream_logs, fetch_logs
|
|
11
|
+
|
|
12
|
+
__version__ = "0.2.1"
|
|
13
|
+
__all__ = [
|
|
14
|
+
"C3",
|
|
15
|
+
"configure",
|
|
16
|
+
"APIError",
|
|
17
|
+
# Images
|
|
18
|
+
"GHCR_IMAGES",
|
|
19
|
+
"COMFYUI_IMAGE",
|
|
20
|
+
# Instance types
|
|
21
|
+
"GPUType",
|
|
22
|
+
"GPUConfig",
|
|
23
|
+
"Region",
|
|
24
|
+
"GPUPricing",
|
|
25
|
+
"PricingTier",
|
|
26
|
+
# Jobs API
|
|
27
|
+
"Job",
|
|
28
|
+
"JobMetrics",
|
|
29
|
+
"GPUMetrics",
|
|
30
|
+
# Renders API
|
|
31
|
+
"Render",
|
|
32
|
+
"RenderStatus",
|
|
33
|
+
# Files API
|
|
34
|
+
"File",
|
|
35
|
+
"AsyncFiles",
|
|
36
|
+
"AsyncHTTPClient",
|
|
37
|
+
# Job lookup utils
|
|
38
|
+
"find_job",
|
|
39
|
+
"find_by_id",
|
|
40
|
+
"find_by_hostname",
|
|
41
|
+
"find_by_ip",
|
|
42
|
+
# Job helpers
|
|
43
|
+
"BaseJob",
|
|
44
|
+
"ComfyUIJob",
|
|
45
|
+
# Workflow utils
|
|
46
|
+
"apply_params",
|
|
47
|
+
"apply_graph_modes",
|
|
48
|
+
"find_node",
|
|
49
|
+
"find_nodes",
|
|
50
|
+
"load_template",
|
|
51
|
+
"graph_to_api",
|
|
52
|
+
"DEFAULT_OBJECT_INFO",
|
|
53
|
+
# Log streaming
|
|
54
|
+
"LogStream",
|
|
55
|
+
"stream_logs",
|
|
56
|
+
"fetch_logs",
|
|
57
|
+
]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Billing API"""
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .http import HTTPClient
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Balance:
|
|
11
|
+
total: str
|
|
12
|
+
rewards: str
|
|
13
|
+
paid: str
|
|
14
|
+
available: str
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_dict(cls, data: dict) -> "Balance":
|
|
18
|
+
return cls(
|
|
19
|
+
total=data.get("total_balance", "0"),
|
|
20
|
+
rewards=data.get("rewards_balance", "0"),
|
|
21
|
+
paid=data.get("balance", "0"),
|
|
22
|
+
available=data.get("available_balance", "0"),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Transaction:
|
|
28
|
+
id: str
|
|
29
|
+
user_id: str
|
|
30
|
+
amount: int
|
|
31
|
+
amount_usd: float
|
|
32
|
+
transaction_type: str
|
|
33
|
+
status: str
|
|
34
|
+
rewards: bool
|
|
35
|
+
job_id: str | None
|
|
36
|
+
created_at: str
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_dict(cls, data: dict) -> "Transaction":
|
|
40
|
+
return cls(
|
|
41
|
+
id=data.get("id", ""),
|
|
42
|
+
user_id=data.get("user_id", ""),
|
|
43
|
+
amount=data.get("amount", 0),
|
|
44
|
+
amount_usd=data.get("amount_usd", 0),
|
|
45
|
+
transaction_type=data.get("transaction_type", ""),
|
|
46
|
+
status=data.get("status", ""),
|
|
47
|
+
rewards=data.get("rewards", False),
|
|
48
|
+
job_id=data.get("job_id"),
|
|
49
|
+
created_at=data.get("created_at", ""),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Billing:
|
|
54
|
+
"""Billing API wrapper"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, http: "HTTPClient"):
|
|
57
|
+
self._http = http
|
|
58
|
+
|
|
59
|
+
def balance(self) -> Balance:
|
|
60
|
+
"""Get account balance"""
|
|
61
|
+
data = self._http.get("/api/balance")
|
|
62
|
+
return Balance.from_dict(data)
|
|
63
|
+
|
|
64
|
+
def transactions(self, limit: int = 50, page: int = 1) -> list[Transaction]:
|
|
65
|
+
"""List transactions"""
|
|
66
|
+
data = self._http.get("/api/tx", params={"page": page, "page_size": limit})
|
|
67
|
+
return [Transaction.from_dict(tx) for tx in data.get("transactions", [])]
|
|
68
|
+
|
|
69
|
+
def get_transaction(self, transaction_id: str) -> Transaction:
|
|
70
|
+
"""Get a specific transaction"""
|
|
71
|
+
data = self._http.get(f"/api/tx/{transaction_id}")
|
|
72
|
+
return Transaction.from_dict(data)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Main C3 client"""
|
|
2
|
+
from .config import get_api_key, get_api_url
|
|
3
|
+
from .http import HTTPClient
|
|
4
|
+
from .billing import Billing
|
|
5
|
+
from .jobs import Jobs
|
|
6
|
+
from .user import UserAPI
|
|
7
|
+
from .instances import Instances
|
|
8
|
+
from .renders import Renders
|
|
9
|
+
from .files import Files
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class C3:
|
|
13
|
+
"""
|
|
14
|
+
C3 API Client
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
from c3 import C3
|
|
18
|
+
|
|
19
|
+
c3 = C3() # Uses C3_API_KEY from env or ~/.c3/config
|
|
20
|
+
# or
|
|
21
|
+
c3 = C3(api_key="your_key")
|
|
22
|
+
|
|
23
|
+
# Billing
|
|
24
|
+
balance = c3.billing.balance()
|
|
25
|
+
print(f"Balance: ${balance.total}")
|
|
26
|
+
|
|
27
|
+
# Jobs
|
|
28
|
+
job = c3.jobs.create(
|
|
29
|
+
image="nvidia/cuda:12.0",
|
|
30
|
+
gpu_type="l40s",
|
|
31
|
+
command="python train.py"
|
|
32
|
+
)
|
|
33
|
+
print(f"Job: {job.job_id}")
|
|
34
|
+
|
|
35
|
+
# User
|
|
36
|
+
user = c3.user.get()
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, api_key: str = None, api_url: str = None):
|
|
40
|
+
self._api_key = api_key or get_api_key()
|
|
41
|
+
if not self._api_key:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
"API key required. Set C3_API_KEY env var, "
|
|
44
|
+
"create ~/.c3/config, or pass api_key parameter."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
self._api_url = api_url or get_api_url()
|
|
48
|
+
self._http = HTTPClient(self._api_url, self._api_key)
|
|
49
|
+
|
|
50
|
+
# API namespaces
|
|
51
|
+
self.billing = Billing(self._http)
|
|
52
|
+
self.jobs = Jobs(self._http)
|
|
53
|
+
self.user = UserAPI(self._http)
|
|
54
|
+
self.instances = Instances(self._http)
|
|
55
|
+
self.renders = Renders(self._http)
|
|
56
|
+
self.files = Files(self._http)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def api_url(self) -> str:
|
|
60
|
+
return self._api_url
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Configuration handling"""
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
CONFIG_DIR = Path.home() / ".c3"
|
|
7
|
+
CONFIG_FILE = CONFIG_DIR / "config"
|
|
8
|
+
|
|
9
|
+
DEFAULT_API_URL = "https://api.hypercli.com"
|
|
10
|
+
DEFAULT_WS_URL = "wss://api.hypercli.com"
|
|
11
|
+
WS_LOGS_PATH = "/orchestra/ws/logs" # WebSocket path for job logs: {WS_URL}{WS_LOGS_PATH}/{job_key}
|
|
12
|
+
|
|
13
|
+
# GHCR images
|
|
14
|
+
GHCR_IMAGES = "ghcr.io/hypercliai/images"
|
|
15
|
+
COMFYUI_IMAGE = f"{GHCR_IMAGES}/comfyui"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _load_config_file() -> dict:
|
|
19
|
+
"""Load config from ~/.c3/config"""
|
|
20
|
+
config = {}
|
|
21
|
+
if CONFIG_FILE.exists():
|
|
22
|
+
for line in CONFIG_FILE.read_text().splitlines():
|
|
23
|
+
line = line.strip()
|
|
24
|
+
if line and not line.startswith("#") and "=" in line:
|
|
25
|
+
key, value = line.split("=", 1)
|
|
26
|
+
config[key.strip()] = value.strip()
|
|
27
|
+
return config
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_config_value(key: str, default: str = None) -> Optional[str]:
|
|
31
|
+
"""Get config value: env var > config file > default"""
|
|
32
|
+
env_val = os.getenv(key)
|
|
33
|
+
if env_val:
|
|
34
|
+
return env_val
|
|
35
|
+
config = _load_config_file()
|
|
36
|
+
return config.get(key, default)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_api_key() -> Optional[str]:
|
|
40
|
+
"""Get API key from env or config file"""
|
|
41
|
+
return get_config_value("C3_API_KEY")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_api_url() -> str:
|
|
45
|
+
"""Get API URL"""
|
|
46
|
+
return get_config_value("C3_API_URL", DEFAULT_API_URL)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_ws_url() -> str:
|
|
50
|
+
"""Get WebSocket URL"""
|
|
51
|
+
ws = get_config_value("C3_WS_URL")
|
|
52
|
+
if ws:
|
|
53
|
+
return ws
|
|
54
|
+
# Derive from API URL
|
|
55
|
+
api = get_api_url()
|
|
56
|
+
return api.replace("https://", "wss://").replace("http://", "ws://")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def configure(api_key: str, api_url: str = None):
|
|
60
|
+
"""Save configuration to ~/.c3/config"""
|
|
61
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
|
|
63
|
+
config = _load_config_file()
|
|
64
|
+
config["C3_API_KEY"] = api_key
|
|
65
|
+
if api_url:
|
|
66
|
+
config["C3_API_URL"] = api_url
|
|
67
|
+
|
|
68
|
+
lines = [f"{k}={v}" for k, v in config.items()]
|
|
69
|
+
CONFIG_FILE.write_text("\n".join(lines) + "\n")
|
|
70
|
+
CONFIG_FILE.chmod(0o600)
|