fluxhive-cli 0.1.0__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.
- fluxhive_cli-0.1.0/LICENSE +23 -0
- fluxhive_cli-0.1.0/PKG-INFO +94 -0
- fluxhive_cli-0.1.0/README.md +84 -0
- fluxhive_cli-0.1.0/fluxhive_cli.egg-info/PKG-INFO +94 -0
- fluxhive_cli-0.1.0/fluxhive_cli.egg-info/SOURCES.txt +23 -0
- fluxhive_cli-0.1.0/fluxhive_cli.egg-info/dependency_links.txt +1 -0
- fluxhive_cli-0.1.0/fluxhive_cli.egg-info/entry_points.txt +3 -0
- fluxhive_cli-0.1.0/fluxhive_cli.egg-info/top_level.txt +1 -0
- fluxhive_cli-0.1.0/fluxhivectl/__init__.py +5 -0
- fluxhive_cli-0.1.0/fluxhivectl/client.py +175 -0
- fluxhive_cli-0.1.0/fluxhivectl/commands/__init__.py +16 -0
- fluxhive_cli-0.1.0/fluxhivectl/commands/agents.py +191 -0
- fluxhive_cli-0.1.0/fluxhivectl/commands/auth.py +134 -0
- fluxhive_cli-0.1.0/fluxhivectl/commands/config.py +65 -0
- fluxhive_cli-0.1.0/fluxhivectl/commands/jobs.py +235 -0
- fluxhive_cli-0.1.0/fluxhivectl/commands/runs.py +122 -0
- fluxhive_cli-0.1.0/fluxhivectl/config.py +158 -0
- fluxhive_cli-0.1.0/fluxhivectl/context.py +16 -0
- fluxhive_cli-0.1.0/fluxhivectl/formatting.py +83 -0
- fluxhive_cli-0.1.0/fluxhivectl/main.py +52 -0
- fluxhive_cli-0.1.0/pyproject.toml +25 -0
- fluxhive_cli-0.1.0/setup.cfg +4 -0
- fluxhive_cli-0.1.0/tests/test_client.py +28 -0
- fluxhive_cli-0.1.0/tests/test_config.py +42 -0
- fluxhive_cli-0.1.0/tests/test_main.py +20 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
FluxHive License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 FluxHive
|
|
4
|
+
|
|
5
|
+
This repository contains multiple components with different licenses:
|
|
6
|
+
|
|
7
|
+
1. Agent Component (app-agent/)
|
|
8
|
+
Licensed under the FluxHive Agent Non-Commercial Copyleft License v1.0.
|
|
9
|
+
- Open source: You may view, modify, and distribute the source code
|
|
10
|
+
- Non-commercial: Commercial use is prohibited (separate commercial license required)
|
|
11
|
+
- Copyleft: Modifications and derivative works must remain open source
|
|
12
|
+
See app-agent/LICENSE for full details.
|
|
13
|
+
|
|
14
|
+
2. Control Server (app-control/)
|
|
15
|
+
Proprietary and confidential.
|
|
16
|
+
Unauthorized copying or distribution is strictly prohibited.
|
|
17
|
+
|
|
18
|
+
3. Web Client (app-web/)
|
|
19
|
+
Proprietary and confidential.
|
|
20
|
+
Unauthorized copying or distribution is strictly prohibited.
|
|
21
|
+
|
|
22
|
+
For commercial licensing or enterprise support, please contact:
|
|
23
|
+
[dramwig@gmail.com]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fluxhive-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: FluxHive control-plane command line client
|
|
5
|
+
Author: FluxHive Team
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Dynamic: license-file
|
|
10
|
+
|
|
11
|
+
# @fluxhive/cli
|
|
12
|
+
|
|
13
|
+
Node-installable launcher and Python implementation for the FluxHive control-plane CLI.
|
|
14
|
+
|
|
15
|
+
The CLI talks to Control Server HTTP APIs and does not implement scheduling or agent runtime logic locally.
|
|
16
|
+
|
|
17
|
+
The package exposes two equivalent commands:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
fluxhive
|
|
21
|
+
fluxhivectl
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## npm Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install -g @fluxhive/cli
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## PyPI Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pipx install fluxhive-cli
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The compatibility package `fluxhivectl` also installs the same CLI:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pipx install fluxhivectl
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Or with pip:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
python -m pip install fluxhive-cli
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Python 3.10 or newer must be available on `PATH`. You can also point the launcher at a specific Python executable:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
FLUXHIVE_PYTHON=/path/to/python fluxhive --version
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
On Windows PowerShell:
|
|
55
|
+
|
|
56
|
+
```powershell
|
|
57
|
+
$env:FLUXHIVE_PYTHON = "C:\Python312\python.exe"
|
|
58
|
+
fluxhive --version
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Development Install
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python -m pip install -e .
|
|
65
|
+
npm test
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Quick Start
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
fluxhive config base_url http://127.0.0.1:8001
|
|
72
|
+
fluxhive auth login --username <username>
|
|
73
|
+
|
|
74
|
+
fluxhive jobs list
|
|
75
|
+
fluxhive agents list
|
|
76
|
+
fluxhive runs list --limit 20
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
You can also use environment variables:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
FLUXHIVE_CONTROL_URL=http://127.0.0.1:8001
|
|
83
|
+
FLUXHIVE_ACCESS_TOKEN=<access-token>
|
|
84
|
+
FLUXHIVE_REFRESH_TOKEN=<refresh-token>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Command Groups
|
|
88
|
+
|
|
89
|
+
- `auth`: login, refresh tokens, inspect current user, clear local tokens
|
|
90
|
+
- `config`: read and write local CLI configuration
|
|
91
|
+
- `jobs`: create, list, inspect, publish, run, cancel, clone, and delete jobs
|
|
92
|
+
- `runs`: list run attempts, inspect run attempts, print stored run logs
|
|
93
|
+
- `agents`: list agents, inspect GPUs, inspect queues, rename agents
|
|
94
|
+
- `nodes`: alias for `agents`
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# @fluxhive/cli
|
|
2
|
+
|
|
3
|
+
Node-installable launcher and Python implementation for the FluxHive control-plane CLI.
|
|
4
|
+
|
|
5
|
+
The CLI talks to Control Server HTTP APIs and does not implement scheduling or agent runtime logic locally.
|
|
6
|
+
|
|
7
|
+
The package exposes two equivalent commands:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
fluxhive
|
|
11
|
+
fluxhivectl
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## npm Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g @fluxhive/cli
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## PyPI Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pipx install fluxhive-cli
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The compatibility package `fluxhivectl` also installs the same CLI:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pipx install fluxhivectl
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or with pip:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
python -m pip install fluxhive-cli
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Python 3.10 or newer must be available on `PATH`. You can also point the launcher at a specific Python executable:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
FLUXHIVE_PYTHON=/path/to/python fluxhive --version
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
On Windows PowerShell:
|
|
45
|
+
|
|
46
|
+
```powershell
|
|
47
|
+
$env:FLUXHIVE_PYTHON = "C:\Python312\python.exe"
|
|
48
|
+
fluxhive --version
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Development Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
python -m pip install -e .
|
|
55
|
+
npm test
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
fluxhive config base_url http://127.0.0.1:8001
|
|
62
|
+
fluxhive auth login --username <username>
|
|
63
|
+
|
|
64
|
+
fluxhive jobs list
|
|
65
|
+
fluxhive agents list
|
|
66
|
+
fluxhive runs list --limit 20
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
You can also use environment variables:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
FLUXHIVE_CONTROL_URL=http://127.0.0.1:8001
|
|
73
|
+
FLUXHIVE_ACCESS_TOKEN=<access-token>
|
|
74
|
+
FLUXHIVE_REFRESH_TOKEN=<refresh-token>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Command Groups
|
|
78
|
+
|
|
79
|
+
- `auth`: login, refresh tokens, inspect current user, clear local tokens
|
|
80
|
+
- `config`: read and write local CLI configuration
|
|
81
|
+
- `jobs`: create, list, inspect, publish, run, cancel, clone, and delete jobs
|
|
82
|
+
- `runs`: list run attempts, inspect run attempts, print stored run logs
|
|
83
|
+
- `agents`: list agents, inspect GPUs, inspect queues, rename agents
|
|
84
|
+
- `nodes`: alias for `agents`
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fluxhive-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: FluxHive control-plane command line client
|
|
5
|
+
Author: FluxHive Team
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Dynamic: license-file
|
|
10
|
+
|
|
11
|
+
# @fluxhive/cli
|
|
12
|
+
|
|
13
|
+
Node-installable launcher and Python implementation for the FluxHive control-plane CLI.
|
|
14
|
+
|
|
15
|
+
The CLI talks to Control Server HTTP APIs and does not implement scheduling or agent runtime logic locally.
|
|
16
|
+
|
|
17
|
+
The package exposes two equivalent commands:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
fluxhive
|
|
21
|
+
fluxhivectl
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## npm Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install -g @fluxhive/cli
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## PyPI Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pipx install fluxhive-cli
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The compatibility package `fluxhivectl` also installs the same CLI:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pipx install fluxhivectl
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Or with pip:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
python -m pip install fluxhive-cli
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Python 3.10 or newer must be available on `PATH`. You can also point the launcher at a specific Python executable:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
FLUXHIVE_PYTHON=/path/to/python fluxhive --version
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
On Windows PowerShell:
|
|
55
|
+
|
|
56
|
+
```powershell
|
|
57
|
+
$env:FLUXHIVE_PYTHON = "C:\Python312\python.exe"
|
|
58
|
+
fluxhive --version
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Development Install
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python -m pip install -e .
|
|
65
|
+
npm test
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Quick Start
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
fluxhive config base_url http://127.0.0.1:8001
|
|
72
|
+
fluxhive auth login --username <username>
|
|
73
|
+
|
|
74
|
+
fluxhive jobs list
|
|
75
|
+
fluxhive agents list
|
|
76
|
+
fluxhive runs list --limit 20
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
You can also use environment variables:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
FLUXHIVE_CONTROL_URL=http://127.0.0.1:8001
|
|
83
|
+
FLUXHIVE_ACCESS_TOKEN=<access-token>
|
|
84
|
+
FLUXHIVE_REFRESH_TOKEN=<refresh-token>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Command Groups
|
|
88
|
+
|
|
89
|
+
- `auth`: login, refresh tokens, inspect current user, clear local tokens
|
|
90
|
+
- `config`: read and write local CLI configuration
|
|
91
|
+
- `jobs`: create, list, inspect, publish, run, cancel, clone, and delete jobs
|
|
92
|
+
- `runs`: list run attempts, inspect run attempts, print stored run logs
|
|
93
|
+
- `agents`: list agents, inspect GPUs, inspect queues, rename agents
|
|
94
|
+
- `nodes`: alias for `agents`
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
fluxhive_cli.egg-info/PKG-INFO
|
|
5
|
+
fluxhive_cli.egg-info/SOURCES.txt
|
|
6
|
+
fluxhive_cli.egg-info/dependency_links.txt
|
|
7
|
+
fluxhive_cli.egg-info/entry_points.txt
|
|
8
|
+
fluxhive_cli.egg-info/top_level.txt
|
|
9
|
+
fluxhivectl/__init__.py
|
|
10
|
+
fluxhivectl/client.py
|
|
11
|
+
fluxhivectl/config.py
|
|
12
|
+
fluxhivectl/context.py
|
|
13
|
+
fluxhivectl/formatting.py
|
|
14
|
+
fluxhivectl/main.py
|
|
15
|
+
fluxhivectl/commands/__init__.py
|
|
16
|
+
fluxhivectl/commands/agents.py
|
|
17
|
+
fluxhivectl/commands/auth.py
|
|
18
|
+
fluxhivectl/commands/config.py
|
|
19
|
+
fluxhivectl/commands/jobs.py
|
|
20
|
+
fluxhivectl/commands/runs.py
|
|
21
|
+
tests/test_client.py
|
|
22
|
+
tests/test_config.py
|
|
23
|
+
tests/test_main.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fluxhivectl
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""HTTP client for FluxHive Control Server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import urllib.error
|
|
7
|
+
import urllib.parse
|
|
8
|
+
import urllib.request
|
|
9
|
+
from dataclasses import replace
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from . import __version__
|
|
13
|
+
from .config import CLIConfig, save_values
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ApiError(RuntimeError):
|
|
17
|
+
def __init__(self, message: str, *, status: int | None = None, code: str | None = None):
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
self.status = status
|
|
20
|
+
self.code = code
|
|
21
|
+
|
|
22
|
+
def __str__(self) -> str:
|
|
23
|
+
parts = []
|
|
24
|
+
if self.status is not None:
|
|
25
|
+
parts.append(str(self.status))
|
|
26
|
+
if self.code:
|
|
27
|
+
parts.append(self.code)
|
|
28
|
+
if parts:
|
|
29
|
+
return f"[{', '.join(parts)}] {super().__str__()}"
|
|
30
|
+
return super().__str__()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ApiClient:
|
|
34
|
+
def __init__(self, config: CLIConfig, *, timeout: float = 30.0):
|
|
35
|
+
self.config = config
|
|
36
|
+
self.timeout = timeout
|
|
37
|
+
|
|
38
|
+
def get(self, path: str, *, query: dict[str, Any] | None = None, auth: bool = True) -> Any:
|
|
39
|
+
return self.request("GET", path, query=query, auth=auth)
|
|
40
|
+
|
|
41
|
+
def post(
|
|
42
|
+
self,
|
|
43
|
+
path: str,
|
|
44
|
+
*,
|
|
45
|
+
body: dict[str, Any] | None = None,
|
|
46
|
+
query: dict[str, Any] | None = None,
|
|
47
|
+
auth: bool = True,
|
|
48
|
+
) -> Any:
|
|
49
|
+
return self.request("POST", path, body=body, query=query, auth=auth)
|
|
50
|
+
|
|
51
|
+
def put(self, path: str, *, body: dict[str, Any] | None = None, auth: bool = True) -> Any:
|
|
52
|
+
return self.request("PUT", path, body=body, auth=auth)
|
|
53
|
+
|
|
54
|
+
def patch(self, path: str, *, body: dict[str, Any] | None = None, auth: bool = True) -> Any:
|
|
55
|
+
return self.request("PATCH", path, body=body, auth=auth)
|
|
56
|
+
|
|
57
|
+
def delete(self, path: str, *, auth: bool = True) -> Any:
|
|
58
|
+
return self.request("DELETE", path, auth=auth)
|
|
59
|
+
|
|
60
|
+
def request(
|
|
61
|
+
self,
|
|
62
|
+
method: str,
|
|
63
|
+
path: str,
|
|
64
|
+
*,
|
|
65
|
+
body: dict[str, Any] | None = None,
|
|
66
|
+
query: dict[str, Any] | None = None,
|
|
67
|
+
auth: bool = True,
|
|
68
|
+
_retried_after_refresh: bool = False,
|
|
69
|
+
) -> Any:
|
|
70
|
+
url = self._build_url(path, query=query)
|
|
71
|
+
payload = json.dumps(body).encode("utf-8") if body is not None else None
|
|
72
|
+
headers = {
|
|
73
|
+
"Accept": "application/json",
|
|
74
|
+
"User-Agent": f"fluxhivectl/{__version__}",
|
|
75
|
+
}
|
|
76
|
+
if body is not None:
|
|
77
|
+
headers["Content-Type"] = "application/json"
|
|
78
|
+
if auth and self.config.access_token:
|
|
79
|
+
headers["Authorization"] = f"Bearer {self.config.access_token}"
|
|
80
|
+
|
|
81
|
+
request = urllib.request.Request(url, data=payload, headers=headers, method=method.upper())
|
|
82
|
+
try:
|
|
83
|
+
with urllib.request.urlopen(request, timeout=self.timeout) as response:
|
|
84
|
+
response_body = response.read()
|
|
85
|
+
return self._parse_response(response.status, response_body)
|
|
86
|
+
except urllib.error.HTTPError as exc:
|
|
87
|
+
response_body = exc.read()
|
|
88
|
+
if (
|
|
89
|
+
exc.code == 401
|
|
90
|
+
and auth
|
|
91
|
+
and self.config.refresh_token
|
|
92
|
+
and not _retried_after_refresh
|
|
93
|
+
):
|
|
94
|
+
self._refresh_token()
|
|
95
|
+
return self.request(
|
|
96
|
+
method,
|
|
97
|
+
path,
|
|
98
|
+
body=body,
|
|
99
|
+
query=query,
|
|
100
|
+
auth=auth,
|
|
101
|
+
_retried_after_refresh=True,
|
|
102
|
+
)
|
|
103
|
+
self._raise_api_error(exc.code, response_body)
|
|
104
|
+
except urllib.error.URLError as exc:
|
|
105
|
+
raise ApiError(f"Failed to connect to Control Server: {exc.reason}") from exc
|
|
106
|
+
|
|
107
|
+
def _build_url(self, path: str, *, query: dict[str, Any] | None = None) -> str:
|
|
108
|
+
if path.startswith("http://") or path.startswith("https://"):
|
|
109
|
+
url = path
|
|
110
|
+
else:
|
|
111
|
+
url = f"{self.config.api_base_url}/{path.lstrip('/')}"
|
|
112
|
+
if query:
|
|
113
|
+
clean_query = {key: value for key, value in query.items() if value is not None}
|
|
114
|
+
if clean_query:
|
|
115
|
+
url = f"{url}?{urllib.parse.urlencode(clean_query, doseq=True)}"
|
|
116
|
+
return url
|
|
117
|
+
|
|
118
|
+
def _refresh_token(self) -> None:
|
|
119
|
+
data = self.request(
|
|
120
|
+
"POST",
|
|
121
|
+
"/auth/refresh",
|
|
122
|
+
body={"refresh_token": self.config.refresh_token},
|
|
123
|
+
auth=False,
|
|
124
|
+
)
|
|
125
|
+
access_token = data.get("access_token")
|
|
126
|
+
refresh_token = data.get("refresh_token")
|
|
127
|
+
if not access_token or not refresh_token:
|
|
128
|
+
raise ApiError("Refresh response did not include tokens", status=401)
|
|
129
|
+
save_values({"access_token": access_token, "refresh_token": refresh_token})
|
|
130
|
+
self.config = replace(
|
|
131
|
+
self.config,
|
|
132
|
+
access_token=access_token,
|
|
133
|
+
refresh_token=refresh_token,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def _parse_response(self, status: int, response_body: bytes) -> Any:
|
|
137
|
+
if not response_body:
|
|
138
|
+
return None
|
|
139
|
+
try:
|
|
140
|
+
payload = json.loads(response_body.decode("utf-8"))
|
|
141
|
+
except json.JSONDecodeError as exc:
|
|
142
|
+
raise ApiError("Control Server returned invalid JSON", status=status) from exc
|
|
143
|
+
|
|
144
|
+
if isinstance(payload, dict) and "success" in payload and "data" in payload:
|
|
145
|
+
if not payload.get("success"):
|
|
146
|
+
error = payload.get("error") or {}
|
|
147
|
+
meta = payload.get("meta") or {}
|
|
148
|
+
message = error.get("message") or meta.get("message") or "Request failed"
|
|
149
|
+
code = error.get("code") or meta.get("code")
|
|
150
|
+
raise ApiError(message, status=status, code=code)
|
|
151
|
+
return payload.get("data")
|
|
152
|
+
return payload
|
|
153
|
+
|
|
154
|
+
def _raise_api_error(self, status: int, response_body: bytes) -> None:
|
|
155
|
+
if not response_body:
|
|
156
|
+
raise ApiError("Request failed", status=status)
|
|
157
|
+
try:
|
|
158
|
+
payload = json.loads(response_body.decode("utf-8"))
|
|
159
|
+
except json.JSONDecodeError as exc:
|
|
160
|
+
raise ApiError(response_body.decode("utf-8", errors="replace"), status=status) from exc
|
|
161
|
+
|
|
162
|
+
if isinstance(payload, dict):
|
|
163
|
+
error = payload.get("error") or {}
|
|
164
|
+
meta = payload.get("meta") or {}
|
|
165
|
+
detail = payload.get("detail")
|
|
166
|
+
message = (
|
|
167
|
+
error.get("message")
|
|
168
|
+
or meta.get("message")
|
|
169
|
+
or (detail if isinstance(detail, str) else None)
|
|
170
|
+
or "Request failed"
|
|
171
|
+
)
|
|
172
|
+
code = error.get("code") or meta.get("code")
|
|
173
|
+
raise ApiError(message, status=status, code=code)
|
|
174
|
+
|
|
175
|
+
raise ApiError("Request failed", status=status)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Command group registration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from . import agents, auth, config, jobs, runs
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def register_commands(subparsers: argparse._SubParsersAction) -> None:
|
|
11
|
+
config.register(subparsers)
|
|
12
|
+
auth.register(subparsers)
|
|
13
|
+
jobs.register(subparsers)
|
|
14
|
+
runs.register(subparsers)
|
|
15
|
+
agents.register(subparsers, name="agents")
|
|
16
|
+
agents.register(subparsers, name="nodes")
|