flowmesh-sdk 0.1.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.
- flowmesh/__init__.py +55 -0
- flowmesh/_base_client.py +422 -0
- flowmesh/_constants.py +7 -0
- flowmesh/async_client.py +66 -0
- flowmesh/client.py +76 -0
- flowmesh/config.py +83 -0
- flowmesh/exceptions.py +55 -0
- flowmesh/models/__init__.py +98 -0
- flowmesh/models/common.py +103 -0
- flowmesh/models/nodes.py +54 -0
- flowmesh/models/results.py +20 -0
- flowmesh/models/ssh.py +17 -0
- flowmesh/models/tasks.py +65 -0
- flowmesh/models/traces.py +82 -0
- flowmesh/models/workers.py +106 -0
- flowmesh/models/workflows.py +48 -0
- flowmesh/params.py +34 -0
- flowmesh/profile_views.py +96 -0
- flowmesh/resources/__init__.py +23 -0
- flowmesh/resources/_base.py +24 -0
- flowmesh/resources/nodes.py +215 -0
- flowmesh/resources/results.py +211 -0
- flowmesh/resources/ssh.py +155 -0
- flowmesh/resources/system.py +50 -0
- flowmesh/resources/tasks.py +241 -0
- flowmesh/resources/traces.py +84 -0
- flowmesh/resources/workers.py +75 -0
- flowmesh/resources/workflows.py +271 -0
- flowmesh/ssh.py +245 -0
- flowmesh_sdk-0.1.0.dist-info/METADATA +21 -0
- flowmesh_sdk-0.1.0.dist-info/RECORD +34 -0
- flowmesh_sdk-0.1.0.dist-info/WHEEL +5 -0
- flowmesh_sdk-0.1.0.dist-info/licenses/LICENSE +202 -0
- flowmesh_sdk-0.1.0.dist-info/top_level.txt +1 -0
flowmesh/__init__.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""FlowMesh Python SDK.
|
|
2
|
+
|
|
3
|
+
Provides typed sync and async clients for the FlowMesh Server REST API.
|
|
4
|
+
|
|
5
|
+
Quick start::
|
|
6
|
+
|
|
7
|
+
from flowmesh import FlowMesh
|
|
8
|
+
|
|
9
|
+
client = FlowMesh(base_url="https://kv.run:8000/flowmesh", api_key="flm-...")
|
|
10
|
+
|
|
11
|
+
# Submit a workflow
|
|
12
|
+
resp = client.workflows.submit(open("workflow.yaml").read())
|
|
13
|
+
print(resp.workflow_id)
|
|
14
|
+
|
|
15
|
+
# List workers
|
|
16
|
+
for w in client.workers.list():
|
|
17
|
+
print(w.id, w.status)
|
|
18
|
+
|
|
19
|
+
Async usage::
|
|
20
|
+
|
|
21
|
+
from flowmesh import AsyncFlowMesh
|
|
22
|
+
|
|
23
|
+
async with AsyncFlowMesh(base_url="...", api_key="...") as client:
|
|
24
|
+
workflows = await client.workflows.list()
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from .async_client import AsyncFlowMesh
|
|
28
|
+
from .client import FlowMesh
|
|
29
|
+
from .config import FlowMeshConfig
|
|
30
|
+
from .exceptions import (
|
|
31
|
+
APIError,
|
|
32
|
+
AuthenticationError,
|
|
33
|
+
ConfigInvalidError,
|
|
34
|
+
ConfigNotFoundError,
|
|
35
|
+
FlowMeshConnectionError,
|
|
36
|
+
FlowMeshError,
|
|
37
|
+
NotFoundError,
|
|
38
|
+
ValidationError,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"FlowMesh",
|
|
43
|
+
"AsyncFlowMesh",
|
|
44
|
+
"FlowMeshConfig",
|
|
45
|
+
"FlowMeshError",
|
|
46
|
+
"APIError",
|
|
47
|
+
"AuthenticationError",
|
|
48
|
+
"NotFoundError",
|
|
49
|
+
"ValidationError",
|
|
50
|
+
"FlowMeshConnectionError",
|
|
51
|
+
"ConfigNotFoundError",
|
|
52
|
+
"ConfigInvalidError",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
__version__ = "0.1.0"
|
flowmesh/_base_client.py
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
"""Base client implementation with shared HTTP transport logic."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from collections.abc import AsyncIterator, Iterator
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Self
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from ._constants import API_VERSION_PREFIX, DEFAULT_BASE_URL, STREAM_TIMEOUT, USER_AGENT
|
|
13
|
+
from .config import DEFAULT_CONFIG_PATH, FlowMeshConfig
|
|
14
|
+
from .exceptions import (
|
|
15
|
+
APIError,
|
|
16
|
+
AuthenticationError,
|
|
17
|
+
ConfigNotFoundError,
|
|
18
|
+
FlowMeshConnectionError,
|
|
19
|
+
NotFoundError,
|
|
20
|
+
ValidationError,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _build_headers(api_key: str | None) -> dict[str, str]:
|
|
25
|
+
headers: dict[str, str] = {
|
|
26
|
+
"Accept": "application/json",
|
|
27
|
+
"User-Agent": USER_AGENT,
|
|
28
|
+
}
|
|
29
|
+
if api_key:
|
|
30
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
31
|
+
return headers
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _make_url(base_url: str, path: str) -> str:
|
|
35
|
+
return base_url.rstrip("/") + API_VERSION_PREFIX + "/" + path.lstrip("/")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _raise_for_status(response: httpx.Response, method: str) -> None:
|
|
39
|
+
if response.status_code < 400:
|
|
40
|
+
return
|
|
41
|
+
try:
|
|
42
|
+
body = response.json()
|
|
43
|
+
except Exception:
|
|
44
|
+
body = response.text
|
|
45
|
+
|
|
46
|
+
message = ""
|
|
47
|
+
if isinstance(body, dict):
|
|
48
|
+
message = body.get("detail", "") or body.get("message", "")
|
|
49
|
+
if not message:
|
|
50
|
+
message = str(body)
|
|
51
|
+
|
|
52
|
+
status = response.status_code
|
|
53
|
+
kwargs: dict[str, Any] = dict(
|
|
54
|
+
status_code=status,
|
|
55
|
+
method=method,
|
|
56
|
+
url=str(response.url),
|
|
57
|
+
body=body,
|
|
58
|
+
)
|
|
59
|
+
if status in (401, 403):
|
|
60
|
+
raise AuthenticationError(message, **kwargs)
|
|
61
|
+
if status == 404:
|
|
62
|
+
raise NotFoundError(message, **kwargs)
|
|
63
|
+
if status in (400, 422):
|
|
64
|
+
raise ValidationError(message, **kwargs)
|
|
65
|
+
raise APIError(message, **kwargs)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _raise_for_stream_status(response: httpx.Response, method: str) -> None:
|
|
69
|
+
if response.status_code < 400:
|
|
70
|
+
return
|
|
71
|
+
response.read()
|
|
72
|
+
_raise_for_status(response, method)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def _raise_for_stream_status_async(response: httpx.Response, method: str) -> None:
|
|
76
|
+
if response.status_code < 400:
|
|
77
|
+
return
|
|
78
|
+
await response.aread()
|
|
79
|
+
_raise_for_status(response, method)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class BaseClient:
|
|
83
|
+
"""Synchronous HTTP transport for the FlowMesh API."""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
base_url: str,
|
|
88
|
+
api_key: str | None,
|
|
89
|
+
timeout: float,
|
|
90
|
+
http_client: httpx.Client | None,
|
|
91
|
+
) -> None:
|
|
92
|
+
self.base_url = base_url.rstrip("/")
|
|
93
|
+
self.api_key = api_key
|
|
94
|
+
self._timeout = timeout
|
|
95
|
+
self._headers = _build_headers(api_key)
|
|
96
|
+
self._http = http_client or httpx.Client(
|
|
97
|
+
headers=self._headers,
|
|
98
|
+
timeout=httpx.Timeout(timeout),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def _request(
|
|
102
|
+
self,
|
|
103
|
+
method: str,
|
|
104
|
+
path: str,
|
|
105
|
+
params: dict[str, Any] | list[tuple[str, str]] | None = None,
|
|
106
|
+
json_body: Any = None,
|
|
107
|
+
data: str | bytes | None = None,
|
|
108
|
+
headers: dict[str, str] | None = None,
|
|
109
|
+
) -> Any:
|
|
110
|
+
url = _make_url(self.base_url, path)
|
|
111
|
+
kwargs: dict[str, Any] = {}
|
|
112
|
+
if params:
|
|
113
|
+
kwargs["params"] = params
|
|
114
|
+
if json_body is not None:
|
|
115
|
+
kwargs["json"] = json_body
|
|
116
|
+
if data is not None:
|
|
117
|
+
kwargs["content"] = data
|
|
118
|
+
if headers:
|
|
119
|
+
kwargs["headers"] = headers
|
|
120
|
+
try:
|
|
121
|
+
response = self._http.request(method, url, **kwargs)
|
|
122
|
+
except httpx.ConnectError as exc:
|
|
123
|
+
raise FlowMeshConnectionError(f"Failed to connect to {url}: {exc}")
|
|
124
|
+
_raise_for_status(response, method)
|
|
125
|
+
if not response.content:
|
|
126
|
+
return None
|
|
127
|
+
return response.json()
|
|
128
|
+
|
|
129
|
+
def _request_raw(
|
|
130
|
+
self,
|
|
131
|
+
method: str,
|
|
132
|
+
path: str,
|
|
133
|
+
params: dict[str, Any] | list[tuple[str, str]] | None = None,
|
|
134
|
+
headers: dict[str, str] | None = None,
|
|
135
|
+
) -> httpx.Response:
|
|
136
|
+
url = _make_url(self.base_url, path)
|
|
137
|
+
kwargs: dict[str, Any] = {}
|
|
138
|
+
if params:
|
|
139
|
+
kwargs["params"] = params
|
|
140
|
+
if headers:
|
|
141
|
+
kwargs["headers"] = headers
|
|
142
|
+
try:
|
|
143
|
+
response = self._http.request(method, url, **kwargs)
|
|
144
|
+
except httpx.ConnectError as exc:
|
|
145
|
+
raise FlowMeshConnectionError(f"Failed to connect to {url}: {exc}")
|
|
146
|
+
_raise_for_status(response, method)
|
|
147
|
+
return response
|
|
148
|
+
|
|
149
|
+
def _stream_sse(
|
|
150
|
+
self,
|
|
151
|
+
path: str,
|
|
152
|
+
cursor: str | None = None,
|
|
153
|
+
params: dict[str, str] | None = None,
|
|
154
|
+
) -> Iterator[tuple[str | None, dict[str, Any]]]:
|
|
155
|
+
"""Stream SSE log events, yielding cursor/data pairs."""
|
|
156
|
+
url = _make_url(self.base_url, path)
|
|
157
|
+
headers = self._headers.copy()
|
|
158
|
+
headers["Accept"] = "text/event-stream"
|
|
159
|
+
params = params.copy() if params else {}
|
|
160
|
+
if cursor:
|
|
161
|
+
params["cursor"] = cursor
|
|
162
|
+
try:
|
|
163
|
+
with self._http.stream(
|
|
164
|
+
"GET",
|
|
165
|
+
url,
|
|
166
|
+
params=params,
|
|
167
|
+
headers=headers,
|
|
168
|
+
timeout=httpx.Timeout(STREAM_TIMEOUT, connect=self._timeout),
|
|
169
|
+
) as response:
|
|
170
|
+
_raise_for_stream_status(response, "GET")
|
|
171
|
+
current_data: list[str] = []
|
|
172
|
+
current_event: str | None = None
|
|
173
|
+
current_id: str | None = None
|
|
174
|
+
for raw_line in response.iter_lines():
|
|
175
|
+
line = raw_line.strip("\r")
|
|
176
|
+
if not line:
|
|
177
|
+
if current_data and (
|
|
178
|
+
current_event is None or current_event == "log"
|
|
179
|
+
):
|
|
180
|
+
blob = "".join(current_data).strip()
|
|
181
|
+
if blob:
|
|
182
|
+
try:
|
|
183
|
+
yield current_id, json.loads(blob)
|
|
184
|
+
except json.JSONDecodeError:
|
|
185
|
+
yield current_id, {"message": blob}
|
|
186
|
+
current_data = []
|
|
187
|
+
current_event = None
|
|
188
|
+
current_id = None
|
|
189
|
+
continue
|
|
190
|
+
if line.startswith(":"):
|
|
191
|
+
continue
|
|
192
|
+
if line.startswith("id:"):
|
|
193
|
+
current_id = line.split(":", 1)[1].strip() or None
|
|
194
|
+
continue
|
|
195
|
+
if line.startswith("event:"):
|
|
196
|
+
current_event = line.split(":", 1)[1].strip() or None
|
|
197
|
+
continue
|
|
198
|
+
if line.startswith("data:"):
|
|
199
|
+
current_data.append(line.split(":", 1)[1].lstrip() + "\n")
|
|
200
|
+
continue
|
|
201
|
+
# flush remainder
|
|
202
|
+
if current_data and (current_event is None or current_event == "log"):
|
|
203
|
+
blob = "".join(current_data).strip()
|
|
204
|
+
if blob:
|
|
205
|
+
try:
|
|
206
|
+
yield current_id, json.loads(blob)
|
|
207
|
+
except json.JSONDecodeError:
|
|
208
|
+
yield current_id, {"message": blob}
|
|
209
|
+
except httpx.ConnectError as exc:
|
|
210
|
+
raise FlowMeshConnectionError(f"Failed to connect to {url}: {exc}")
|
|
211
|
+
|
|
212
|
+
def _download(
|
|
213
|
+
self,
|
|
214
|
+
path: str,
|
|
215
|
+
output_path: Path,
|
|
216
|
+
chunk_size: int = 256 * 1024,
|
|
217
|
+
) -> None:
|
|
218
|
+
url = _make_url(self.base_url, path)
|
|
219
|
+
try:
|
|
220
|
+
with self._http.stream("GET", url) as response:
|
|
221
|
+
_raise_for_stream_status(response, "GET")
|
|
222
|
+
with output_path.open("wb") as fh:
|
|
223
|
+
for chunk in response.iter_bytes(chunk_size=chunk_size):
|
|
224
|
+
fh.write(chunk)
|
|
225
|
+
except httpx.ConnectError as exc:
|
|
226
|
+
raise FlowMeshConnectionError(f"Failed to connect to {url}: {exc}")
|
|
227
|
+
|
|
228
|
+
def close(self) -> None:
|
|
229
|
+
self._http.close()
|
|
230
|
+
|
|
231
|
+
def __enter__(self) -> Self:
|
|
232
|
+
return self
|
|
233
|
+
|
|
234
|
+
def __exit__(self, *args: Any) -> None:
|
|
235
|
+
self.close()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class BaseAsyncClient:
|
|
239
|
+
"""Asynchronous HTTP transport for the FlowMesh API."""
|
|
240
|
+
|
|
241
|
+
def __init__(
|
|
242
|
+
self,
|
|
243
|
+
base_url: str,
|
|
244
|
+
api_key: str | None,
|
|
245
|
+
timeout: float,
|
|
246
|
+
http_client: httpx.AsyncClient | None,
|
|
247
|
+
) -> None:
|
|
248
|
+
self.base_url = base_url.rstrip("/")
|
|
249
|
+
self.api_key = api_key
|
|
250
|
+
self._timeout = timeout
|
|
251
|
+
self._headers = _build_headers(api_key)
|
|
252
|
+
self._http = http_client or httpx.AsyncClient(
|
|
253
|
+
headers=self._headers,
|
|
254
|
+
timeout=httpx.Timeout(timeout),
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
async def _request(
|
|
258
|
+
self,
|
|
259
|
+
method: str,
|
|
260
|
+
path: str,
|
|
261
|
+
params: dict[str, Any] | list[tuple[str, str]] | None = None,
|
|
262
|
+
json_body: Any = None,
|
|
263
|
+
data: str | bytes | None = None,
|
|
264
|
+
headers: dict[str, str] | None = None,
|
|
265
|
+
) -> Any:
|
|
266
|
+
url = _make_url(self.base_url, path)
|
|
267
|
+
kwargs: dict[str, Any] = {}
|
|
268
|
+
if params:
|
|
269
|
+
kwargs["params"] = params
|
|
270
|
+
if json_body is not None:
|
|
271
|
+
kwargs["json"] = json_body
|
|
272
|
+
if data is not None:
|
|
273
|
+
kwargs["content"] = data
|
|
274
|
+
if headers:
|
|
275
|
+
kwargs["headers"] = headers
|
|
276
|
+
try:
|
|
277
|
+
response = await self._http.request(method, url, **kwargs)
|
|
278
|
+
except httpx.ConnectError as exc:
|
|
279
|
+
raise FlowMeshConnectionError(f"Failed to connect to {url}: {exc}")
|
|
280
|
+
_raise_for_status(response, method)
|
|
281
|
+
if not response.content:
|
|
282
|
+
return None
|
|
283
|
+
return response.json()
|
|
284
|
+
|
|
285
|
+
async def _request_raw(
|
|
286
|
+
self,
|
|
287
|
+
method: str,
|
|
288
|
+
path: str,
|
|
289
|
+
params: dict[str, Any] | list[tuple[str, str]] | None = None,
|
|
290
|
+
headers: dict[str, str] | None = None,
|
|
291
|
+
) -> httpx.Response:
|
|
292
|
+
url = _make_url(self.base_url, path)
|
|
293
|
+
kwargs: dict[str, Any] = {}
|
|
294
|
+
if params:
|
|
295
|
+
kwargs["params"] = params
|
|
296
|
+
if headers:
|
|
297
|
+
kwargs["headers"] = headers
|
|
298
|
+
try:
|
|
299
|
+
response = await self._http.request(method, url, **kwargs)
|
|
300
|
+
except httpx.ConnectError as exc:
|
|
301
|
+
raise FlowMeshConnectionError(f"Failed to connect to {url}: {exc}")
|
|
302
|
+
_raise_for_status(response, method)
|
|
303
|
+
return response
|
|
304
|
+
|
|
305
|
+
async def _stream_sse(
|
|
306
|
+
self,
|
|
307
|
+
path: str,
|
|
308
|
+
cursor: str | None = None,
|
|
309
|
+
params: dict[str, str] | None = None,
|
|
310
|
+
) -> AsyncIterator[tuple[str | None, dict[str, Any]]]:
|
|
311
|
+
"""Stream SSE log events, yielding cursor/data pairs."""
|
|
312
|
+
url = _make_url(self.base_url, path)
|
|
313
|
+
headers = dict(self._headers)
|
|
314
|
+
headers["Accept"] = "text/event-stream"
|
|
315
|
+
params = dict(params) if params else {}
|
|
316
|
+
if cursor:
|
|
317
|
+
params["cursor"] = cursor
|
|
318
|
+
try:
|
|
319
|
+
async with self._http.stream(
|
|
320
|
+
"GET",
|
|
321
|
+
url,
|
|
322
|
+
params=params,
|
|
323
|
+
headers=headers,
|
|
324
|
+
timeout=httpx.Timeout(STREAM_TIMEOUT, connect=self._timeout),
|
|
325
|
+
) as response:
|
|
326
|
+
await _raise_for_stream_status_async(response, "GET")
|
|
327
|
+
current_data: list[str] = []
|
|
328
|
+
current_event: str | None = None
|
|
329
|
+
current_id: str | None = None
|
|
330
|
+
async for raw_line in response.aiter_lines():
|
|
331
|
+
line = raw_line.strip("\r")
|
|
332
|
+
if not line:
|
|
333
|
+
if current_data and (
|
|
334
|
+
current_event is None or current_event == "log"
|
|
335
|
+
):
|
|
336
|
+
blob = "".join(current_data).strip()
|
|
337
|
+
if blob:
|
|
338
|
+
try:
|
|
339
|
+
yield current_id, json.loads(blob)
|
|
340
|
+
except json.JSONDecodeError:
|
|
341
|
+
yield current_id, {"message": blob}
|
|
342
|
+
current_data = []
|
|
343
|
+
current_event = None
|
|
344
|
+
current_id = None
|
|
345
|
+
continue
|
|
346
|
+
if line.startswith(":"):
|
|
347
|
+
continue
|
|
348
|
+
if line.startswith("id:"):
|
|
349
|
+
current_id = line.split(":", 1)[1].strip() or None
|
|
350
|
+
continue
|
|
351
|
+
if line.startswith("event:"):
|
|
352
|
+
current_event = line.split(":", 1)[1].strip() or None
|
|
353
|
+
continue
|
|
354
|
+
if line.startswith("data:"):
|
|
355
|
+
current_data.append(line.split(":", 1)[1].lstrip() + "\n")
|
|
356
|
+
continue
|
|
357
|
+
# flush remainder
|
|
358
|
+
if current_data and (current_event is None or current_event == "log"):
|
|
359
|
+
blob = "".join(current_data).strip()
|
|
360
|
+
if blob:
|
|
361
|
+
try:
|
|
362
|
+
yield current_id, json.loads(blob)
|
|
363
|
+
except json.JSONDecodeError:
|
|
364
|
+
yield current_id, {"message": blob}
|
|
365
|
+
except httpx.ConnectError as exc:
|
|
366
|
+
raise FlowMeshConnectionError(f"Failed to connect to {url}: {exc}")
|
|
367
|
+
|
|
368
|
+
async def _download(
|
|
369
|
+
self,
|
|
370
|
+
path: str,
|
|
371
|
+
output_path: Path,
|
|
372
|
+
chunk_size: int = 256 * 1024,
|
|
373
|
+
) -> None:
|
|
374
|
+
url = _make_url(self.base_url, path)
|
|
375
|
+
try:
|
|
376
|
+
async with self._http.stream("GET", url) as response:
|
|
377
|
+
await _raise_for_stream_status_async(response, "GET")
|
|
378
|
+
fh = await asyncio.to_thread(output_path.open, "wb")
|
|
379
|
+
try:
|
|
380
|
+
async for chunk in response.aiter_bytes(chunk_size=chunk_size):
|
|
381
|
+
await asyncio.to_thread(fh.write, chunk)
|
|
382
|
+
finally:
|
|
383
|
+
await asyncio.to_thread(fh.close)
|
|
384
|
+
except httpx.ConnectError as exc:
|
|
385
|
+
raise FlowMeshConnectionError(f"Failed to connect to {url}: {exc}")
|
|
386
|
+
|
|
387
|
+
async def close(self) -> None:
|
|
388
|
+
await self._http.aclose()
|
|
389
|
+
|
|
390
|
+
async def __aenter__(self) -> Self:
|
|
391
|
+
return self
|
|
392
|
+
|
|
393
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
394
|
+
await self.close()
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def resolve_config(
|
|
398
|
+
base_url: str | None = None,
|
|
399
|
+
api_key: str | None = None,
|
|
400
|
+
config_path: Path | None = DEFAULT_CONFIG_PATH,
|
|
401
|
+
) -> FlowMeshConfig:
|
|
402
|
+
"""Resolve configuration from params → env → config file → defaults."""
|
|
403
|
+
if base_url is None:
|
|
404
|
+
base_url = os.getenv("FLOWMESH_BASE_URL", "").strip() or None
|
|
405
|
+
if api_key is None:
|
|
406
|
+
api_key = os.getenv("FLOWMESH_API_KEY", "").strip() or None
|
|
407
|
+
|
|
408
|
+
if not (base_url is None or api_key is None):
|
|
409
|
+
return FlowMeshConfig(base_url=base_url, api_key=api_key)
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
cfg = FlowMeshConfig.from_file(config_path or DEFAULT_CONFIG_PATH)
|
|
413
|
+
except ConfigNotFoundError:
|
|
414
|
+
return FlowMeshConfig(
|
|
415
|
+
base_url=DEFAULT_BASE_URL if base_url is None else base_url, api_key=api_key
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
if base_url is not None:
|
|
419
|
+
cfg.base_url = base_url
|
|
420
|
+
if api_key is not None:
|
|
421
|
+
cfg.api_key = api_key
|
|
422
|
+
return cfg
|
flowmesh/_constants.py
ADDED
flowmesh/async_client.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Asynchronous FlowMesh client."""
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from ._base_client import BaseAsyncClient, resolve_config
|
|
6
|
+
from ._constants import DEFAULT_TIMEOUT
|
|
7
|
+
from .resources.nodes import AsyncNodes
|
|
8
|
+
from .resources.results import AsyncResults
|
|
9
|
+
from .resources.ssh import AsyncSSH
|
|
10
|
+
from .resources.system import AsyncSystem
|
|
11
|
+
from .resources.tasks import AsyncTasks
|
|
12
|
+
from .resources.traces import AsyncTraces
|
|
13
|
+
from .resources.workers import AsyncWorkers
|
|
14
|
+
from .resources.workflows import AsyncWorkflows
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AsyncFlowMesh(BaseAsyncClient):
|
|
18
|
+
"""Asynchronous FlowMesh API client.
|
|
19
|
+
|
|
20
|
+
Usage::
|
|
21
|
+
|
|
22
|
+
from flowmesh import AsyncFlowMesh
|
|
23
|
+
|
|
24
|
+
async with AsyncFlowMesh(
|
|
25
|
+
base_url="https://kv.run:8000/flowmesh",
|
|
26
|
+
api_key="flm-xxxx-...",
|
|
27
|
+
) as client:
|
|
28
|
+
result = await client.workflows.submit(
|
|
29
|
+
open("workflow.yaml").read()
|
|
30
|
+
)
|
|
31
|
+
workers = await client.workers.list()
|
|
32
|
+
|
|
33
|
+
Configuration resolution is the same as :class:`FlowMesh`.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
workflows: AsyncWorkflows
|
|
37
|
+
tasks: AsyncTasks
|
|
38
|
+
results: AsyncResults
|
|
39
|
+
workers: AsyncWorkers
|
|
40
|
+
nodes: AsyncNodes
|
|
41
|
+
ssh: AsyncSSH
|
|
42
|
+
system: AsyncSystem
|
|
43
|
+
traces: AsyncTraces
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
base_url: str | None = None,
|
|
48
|
+
api_key: str | None = None,
|
|
49
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
50
|
+
http_client: httpx.AsyncClient | None = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
cfg = resolve_config(base_url, api_key)
|
|
53
|
+
super().__init__(
|
|
54
|
+
base_url=cfg.base_url,
|
|
55
|
+
api_key=cfg.api_key,
|
|
56
|
+
timeout=timeout,
|
|
57
|
+
http_client=http_client,
|
|
58
|
+
)
|
|
59
|
+
self.workflows = AsyncWorkflows(self)
|
|
60
|
+
self.tasks = AsyncTasks(self)
|
|
61
|
+
self.results = AsyncResults(self)
|
|
62
|
+
self.workers = AsyncWorkers(self)
|
|
63
|
+
self.nodes = AsyncNodes(self)
|
|
64
|
+
self.ssh = AsyncSSH(self)
|
|
65
|
+
self.system = AsyncSystem(self)
|
|
66
|
+
self.traces = AsyncTraces(self)
|
flowmesh/client.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Synchronous FlowMesh client."""
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from ._base_client import BaseClient, resolve_config
|
|
6
|
+
from ._constants import DEFAULT_TIMEOUT
|
|
7
|
+
from .resources.nodes import Nodes
|
|
8
|
+
from .resources.results import Results
|
|
9
|
+
from .resources.ssh import SSH
|
|
10
|
+
from .resources.system import System
|
|
11
|
+
from .resources.tasks import Tasks
|
|
12
|
+
from .resources.traces import Traces
|
|
13
|
+
from .resources.workers import Workers
|
|
14
|
+
from .resources.workflows import Workflows
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FlowMesh(BaseClient):
|
|
18
|
+
"""Synchronous FlowMesh API client.
|
|
19
|
+
|
|
20
|
+
Usage::
|
|
21
|
+
|
|
22
|
+
from flowmesh import FlowMesh
|
|
23
|
+
|
|
24
|
+
client = FlowMesh(
|
|
25
|
+
base_url="https://kv.run:8000/flowmesh",
|
|
26
|
+
api_key="flm-xxxx-...",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Submit a workflow
|
|
30
|
+
result = client.workflows.submit(open("workflow.yaml").read())
|
|
31
|
+
|
|
32
|
+
# List workers
|
|
33
|
+
workers = client.workers.list()
|
|
34
|
+
|
|
35
|
+
# Use as context manager
|
|
36
|
+
with FlowMesh(base_url="...", api_key="...") as client:
|
|
37
|
+
wf = client.workflows.retrieve("wf-123")
|
|
38
|
+
|
|
39
|
+
Configuration resolution (in order of precedence):
|
|
40
|
+
1. Explicit ``base_url`` / ``api_key`` parameters
|
|
41
|
+
2. Environment variables: ``FLOWMESH_BASE_URL``, ``FLOWMESH_API_KEY``
|
|
42
|
+
3. CLI config file: ``~/.flowmesh/config.toml``
|
|
43
|
+
4. Default: ``http://localhost:8000``
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
workflows: Workflows
|
|
47
|
+
tasks: Tasks
|
|
48
|
+
results: Results
|
|
49
|
+
workers: Workers
|
|
50
|
+
nodes: Nodes
|
|
51
|
+
ssh: SSH
|
|
52
|
+
system: System
|
|
53
|
+
traces: Traces
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
base_url: str | None = None,
|
|
58
|
+
api_key: str | None = None,
|
|
59
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
60
|
+
http_client: httpx.Client | None = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
cfg = resolve_config(base_url, api_key)
|
|
63
|
+
super().__init__(
|
|
64
|
+
base_url=cfg.base_url,
|
|
65
|
+
api_key=cfg.api_key,
|
|
66
|
+
timeout=timeout,
|
|
67
|
+
http_client=http_client,
|
|
68
|
+
)
|
|
69
|
+
self.workflows = Workflows(self)
|
|
70
|
+
self.tasks = Tasks(self)
|
|
71
|
+
self.results = Results(self)
|
|
72
|
+
self.workers = Workers(self)
|
|
73
|
+
self.nodes = Nodes(self)
|
|
74
|
+
self.ssh = SSH(self)
|
|
75
|
+
self.system = System(self)
|
|
76
|
+
self.traces = Traces(self)
|