buildify-cli 1.0.0__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.
- buildify_cli/__init__.py +1 -0
- buildify_cli/__main__.py +4 -0
- buildify_cli/cli.py +1206 -0
- buildify_cli/client.py +288 -0
- buildify_cli/config.py +593 -0
- buildify_cli/credential_form.py +156 -0
- buildify_cli/exit_codes.py +8 -0
- buildify_cli/output.py +46 -0
- buildify_cli/publish_view.py +122 -0
- buildify_cli/usage.md +245 -0
- buildify_cli-1.0.0.dist-info/METADATA +95 -0
- buildify_cli-1.0.0.dist-info/RECORD +15 -0
- buildify_cli-1.0.0.dist-info/WHEEL +5 -0
- buildify_cli-1.0.0.dist-info/entry_points.txt +2 -0
- buildify_cli-1.0.0.dist-info/top_level.txt +1 -0
buildify_cli/client.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from buildify_cli.output import body_success
|
|
8
|
+
|
|
9
|
+
API_PREFIX = "/openapi/v1"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OpenApiClient:
|
|
13
|
+
"""Thin HTTP wrapper: one method per endpoint, no client-side business logic.
|
|
14
|
+
|
|
15
|
+
Every method returns ``(http_status, parsed_body)``; the caller decides how to
|
|
16
|
+
render and which exit code to use.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, base_url: str, api_key: Optional[str] = None, timeout: float = 60.0) -> None:
|
|
20
|
+
self.base_url = base_url.rstrip("/")
|
|
21
|
+
self.api_key = api_key
|
|
22
|
+
self._client = httpx.Client(timeout=timeout, follow_redirects=True)
|
|
23
|
+
|
|
24
|
+
def close(self) -> None:
|
|
25
|
+
self._client.close()
|
|
26
|
+
|
|
27
|
+
def __enter__(self) -> "OpenApiClient":
|
|
28
|
+
return self
|
|
29
|
+
|
|
30
|
+
def __exit__(self, *_exc: object) -> None:
|
|
31
|
+
self.close()
|
|
32
|
+
|
|
33
|
+
def _headers(self, need_auth: bool) -> dict[str, str]:
|
|
34
|
+
h = {"Accept": "application/json"}
|
|
35
|
+
if need_auth:
|
|
36
|
+
if not self.api_key:
|
|
37
|
+
raise ValueError("api_key required")
|
|
38
|
+
h["Authorization"] = f"Bearer {self.api_key}"
|
|
39
|
+
return h
|
|
40
|
+
|
|
41
|
+
def _get(self, path: str, params: Optional[dict[str, Any]] = None, *, auth: bool = True) -> tuple[int, Any]:
|
|
42
|
+
clean = {k: v for k, v in (params or {}).items() if v is not None}
|
|
43
|
+
r = self._client.get(f"{self.base_url}{path}", headers=self._headers(auth), params=clean)
|
|
44
|
+
return r.status_code, _parse_body(r)
|
|
45
|
+
|
|
46
|
+
def _post(self, path: str, payload: Optional[dict[str, Any]] = None, *, timeout: Optional[float] = None) -> tuple[int, Any]:
|
|
47
|
+
r = self._client.post(
|
|
48
|
+
f"{self.base_url}{path}",
|
|
49
|
+
headers=self._headers(True),
|
|
50
|
+
json=payload if payload is not None else {},
|
|
51
|
+
timeout=timeout if timeout is not None else self._client.timeout,
|
|
52
|
+
)
|
|
53
|
+
return r.status_code, _parse_body(r)
|
|
54
|
+
|
|
55
|
+
def _delete(self, path: str) -> tuple[int, Any]:
|
|
56
|
+
r = self._client.delete(f"{self.base_url}{path}", headers=self._headers(True))
|
|
57
|
+
return r.status_code, _parse_body(r)
|
|
58
|
+
|
|
59
|
+
# --- health / key ---
|
|
60
|
+
|
|
61
|
+
def health(self) -> tuple[int, Any]:
|
|
62
|
+
return self._get(f"{API_PREFIX}/health", auth=False)
|
|
63
|
+
|
|
64
|
+
def key_test(self) -> tuple[int, Any]:
|
|
65
|
+
return self._get(f"{API_PREFIX}/key/test")
|
|
66
|
+
|
|
67
|
+
# --- bundle discovery ---
|
|
68
|
+
|
|
69
|
+
def bundle_list(self, keyword: Optional[str] = None) -> tuple[int, Any]:
|
|
70
|
+
return self._get(f"{API_PREFIX}/bundles", {"keyword": keyword})
|
|
71
|
+
|
|
72
|
+
def bundle_nodes(self, bundle_name: str, bundle_version: Optional[str]) -> tuple[int, Any]:
|
|
73
|
+
return self._get(
|
|
74
|
+
f"{API_PREFIX}/bundles/nodes",
|
|
75
|
+
{"bundleName": bundle_name, "bundleVersion": bundle_version},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def node_properties(self, bundle_name: str, bundle_version: Optional[str], name: str) -> tuple[int, Any]:
|
|
79
|
+
return self._get(
|
|
80
|
+
f"{API_PREFIX}/bundles/nodes/properties",
|
|
81
|
+
{"bundleName": bundle_name, "bundleVersion": bundle_version, "name": name},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def node_relations(self, bundle_name: str, bundle_version: Optional[str], name: str) -> tuple[int, Any]:
|
|
85
|
+
return self._get(
|
|
86
|
+
f"{API_PREFIX}/bundles/nodes/relations",
|
|
87
|
+
{"bundleName": bundle_name, "bundleVersion": bundle_version, "name": name},
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def bundle_doc(self, bundle_name: str, bundle_version: Optional[str]) -> tuple[int, Any]:
|
|
91
|
+
return self._get(
|
|
92
|
+
f"{API_PREFIX}/bundles/doc",
|
|
93
|
+
{"bundleName": bundle_name, "bundleVersion": bundle_version},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def credentials_types(self, bundle_name: str, bundle_version: Optional[str]) -> tuple[int, Any]:
|
|
97
|
+
return self._get(
|
|
98
|
+
f"{API_PREFIX}/bundles/credentials/types",
|
|
99
|
+
{"bundleName": bundle_name, "bundleVersion": bundle_version},
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def credentials_properties(self, bundle_name: str, bundle_version: Optional[str], type_: str) -> tuple[int, Any]:
|
|
103
|
+
return self._get(
|
|
104
|
+
f"{API_PREFIX}/bundles/credentials/properties",
|
|
105
|
+
{"bundleName": bundle_name, "bundleVersion": bundle_version, "type": type_},
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# --- projects ---
|
|
109
|
+
|
|
110
|
+
def project_list(self) -> tuple[int, Any]:
|
|
111
|
+
return self._get(f"{API_PREFIX}/projects")
|
|
112
|
+
|
|
113
|
+
def project_create(self, name: str, remark: Optional[str]) -> tuple[int, Any]:
|
|
114
|
+
return self._post(f"{API_PREFIX}/projects", {"projectName": name, "remark": remark})
|
|
115
|
+
|
|
116
|
+
def project_get(self, project_id: str) -> tuple[int, Any]:
|
|
117
|
+
return self._get(f"{API_PREFIX}/projects/{project_id}")
|
|
118
|
+
|
|
119
|
+
def project_credentials(self, project_id: str, bundle_name: Optional[str] = None) -> tuple[int, Any]:
|
|
120
|
+
return self._get(
|
|
121
|
+
f"{API_PREFIX}/projects/{project_id}/credentials",
|
|
122
|
+
{"bundleName": bundle_name},
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def project_create_credential(
|
|
126
|
+
self,
|
|
127
|
+
project_id: str,
|
|
128
|
+
*,
|
|
129
|
+
bundle_name: str,
|
|
130
|
+
type_: str,
|
|
131
|
+
name: str,
|
|
132
|
+
label: str,
|
|
133
|
+
data: dict[str, Any],
|
|
134
|
+
bundle_version: Optional[str] = None,
|
|
135
|
+
description: Optional[str] = None,
|
|
136
|
+
) -> tuple[int, Any]:
|
|
137
|
+
payload: dict[str, Any] = {
|
|
138
|
+
"bundleName": bundle_name,
|
|
139
|
+
"type": type_,
|
|
140
|
+
"name": name,
|
|
141
|
+
"label": label,
|
|
142
|
+
"data": data,
|
|
143
|
+
}
|
|
144
|
+
if bundle_version:
|
|
145
|
+
payload["bundleVersion"] = bundle_version
|
|
146
|
+
if description is not None:
|
|
147
|
+
payload["description"] = description
|
|
148
|
+
return self._post(f"{API_PREFIX}/projects/{project_id}/credentials", payload)
|
|
149
|
+
|
|
150
|
+
def project_workers(self, project_id: str, online_only: bool = False) -> tuple[int, Any]:
|
|
151
|
+
return self._get(
|
|
152
|
+
f"{API_PREFIX}/projects/{project_id}/workers",
|
|
153
|
+
{"onlineOnly": "true" if online_only else None},
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# --- workflows ---
|
|
157
|
+
|
|
158
|
+
def flow_list(self, project_id: str, keyword: Optional[str] = None) -> tuple[int, Any]:
|
|
159
|
+
return self._get(f"{API_PREFIX}/projects/{project_id}/workflows", {"keyword": keyword})
|
|
160
|
+
|
|
161
|
+
def flow_create(
|
|
162
|
+
self,
|
|
163
|
+
project_id: str,
|
|
164
|
+
*,
|
|
165
|
+
workflow_name: str,
|
|
166
|
+
worker_ids: list[str],
|
|
167
|
+
remark: Optional[str],
|
|
168
|
+
) -> tuple[int, Any]:
|
|
169
|
+
return self._post(
|
|
170
|
+
f"{API_PREFIX}/projects/{project_id}/workflows",
|
|
171
|
+
{"workflowName": workflow_name, "workerIds": worker_ids, "remark": remark},
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def flow_get_draft(self, project_id: str, workflow_id: str) -> tuple[int, Any]:
|
|
175
|
+
return self._get(f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}/draft")
|
|
176
|
+
|
|
177
|
+
def flow_save_draft(
|
|
178
|
+
self,
|
|
179
|
+
project_id: str,
|
|
180
|
+
workflow_id: str,
|
|
181
|
+
*,
|
|
182
|
+
workflow_json: Any,
|
|
183
|
+
version: int,
|
|
184
|
+
) -> tuple[int, Any]:
|
|
185
|
+
return self._post(
|
|
186
|
+
f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}/draft",
|
|
187
|
+
{"workflowJson": workflow_json, "version": version},
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def flow_validate(self, project_id: str, workflow_json: Any) -> tuple[int, Any]:
|
|
191
|
+
return self._post(
|
|
192
|
+
f"{API_PREFIX}/projects/{project_id}/workflows/validate",
|
|
193
|
+
{"workflowJson": workflow_json},
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def flow_test_run(
|
|
197
|
+
self,
|
|
198
|
+
project_id: str,
|
|
199
|
+
*,
|
|
200
|
+
workflow_json: Any,
|
|
201
|
+
worker_id: str,
|
|
202
|
+
workflow_id: Optional[str],
|
|
203
|
+
start_node_id: Optional[str],
|
|
204
|
+
inputs: Any,
|
|
205
|
+
timeout_seconds: Optional[int],
|
|
206
|
+
) -> tuple[int, Any]:
|
|
207
|
+
payload = {
|
|
208
|
+
"workflowJson": workflow_json,
|
|
209
|
+
"workerId": worker_id,
|
|
210
|
+
"workflowId": workflow_id,
|
|
211
|
+
"startNodeId": start_node_id,
|
|
212
|
+
"inputs": inputs,
|
|
213
|
+
"timeoutSeconds": timeout_seconds,
|
|
214
|
+
}
|
|
215
|
+
# 服务端等待 workflow.completed,HTTP 读超时必须留出余量,否则本地先断开
|
|
216
|
+
wait = (timeout_seconds or 60) + 30
|
|
217
|
+
return self._post(f"{API_PREFIX}/projects/{project_id}/workflows/test_runs", payload, timeout=float(wait))
|
|
218
|
+
|
|
219
|
+
def flow_cancel_test(self, project_id: str, execution_id: str, worker_id: str) -> tuple[int, Any]:
|
|
220
|
+
return self._post(
|
|
221
|
+
f"{API_PREFIX}/projects/{project_id}/workflows/test_runs/{execution_id}/cancel",
|
|
222
|
+
{"workerId": worker_id},
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def flow_deploy(
|
|
226
|
+
self,
|
|
227
|
+
project_id: str,
|
|
228
|
+
workflow_id: str,
|
|
229
|
+
*,
|
|
230
|
+
worker_ids: list[str],
|
|
231
|
+
workflow_json: Any = None,
|
|
232
|
+
version: Optional[int] = None,
|
|
233
|
+
remark: Optional[str] = None,
|
|
234
|
+
version_name: Optional[str] = None,
|
|
235
|
+
) -> tuple[int, Any]:
|
|
236
|
+
payload: dict[str, Any] = {"workerIds": worker_ids}
|
|
237
|
+
if workflow_json is not None:
|
|
238
|
+
payload["workflowJson"] = workflow_json
|
|
239
|
+
if version is not None:
|
|
240
|
+
payload["version"] = version
|
|
241
|
+
if remark is not None:
|
|
242
|
+
payload["remark"] = remark
|
|
243
|
+
if version_name is not None:
|
|
244
|
+
payload["versionName"] = version_name
|
|
245
|
+
return self._post(
|
|
246
|
+
f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}/deploy",
|
|
247
|
+
payload,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def flow_published(self, project_id: str, workflow_id: str) -> tuple[int, Any]:
|
|
251
|
+
return self._get(f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}/published")
|
|
252
|
+
|
|
253
|
+
def flow_deployment(
|
|
254
|
+
self,
|
|
255
|
+
project_id: str,
|
|
256
|
+
workflow_id: str,
|
|
257
|
+
deployment_id: str,
|
|
258
|
+
*,
|
|
259
|
+
status_only: bool = False,
|
|
260
|
+
) -> tuple[int, Any]:
|
|
261
|
+
return self._get(
|
|
262
|
+
f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}/deployments/{deployment_id}",
|
|
263
|
+
{"statusOnly": "true"} if status_only else None,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _parse_body(r: httpx.Response) -> Any:
|
|
268
|
+
try:
|
|
269
|
+
return r.json()
|
|
270
|
+
except Exception:
|
|
271
|
+
return r.text
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def interpret_r_body(body: Any) -> tuple[Optional[int], Optional[str], Any]:
|
|
275
|
+
"""Return (business code, msg, data) from server R<T> JSON or None if not shaped."""
|
|
276
|
+
if not isinstance(body, dict):
|
|
277
|
+
return None, None, body
|
|
278
|
+
if "code" in body and ("msg" in body or "message" in body):
|
|
279
|
+
msg = body.get("msg") or body.get("message")
|
|
280
|
+
return int(body["code"]), str(msg) if msg is not None else "", body.get("data")
|
|
281
|
+
return None, None, body
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def operation_ok(http_status: int, body: Any) -> bool:
|
|
285
|
+
if http_status < 200 or http_status >= 300:
|
|
286
|
+
return False
|
|
287
|
+
code, _, _ = interpret_r_body(body)
|
|
288
|
+
return body_success(code)
|