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/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)