flowmesh-cli 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_cli/__init__.py +1 -0
- flowmesh_cli/cli.py +46 -0
- flowmesh_cli/commands/__init__.py +26 -0
- flowmesh_cli/commands/base.py +139 -0
- flowmesh_cli/commands/node.py +142 -0
- flowmesh_cli/commands/result.py +57 -0
- flowmesh_cli/commands/ssh.py +422 -0
- flowmesh_cli/commands/system.py +22 -0
- flowmesh_cli/commands/task.py +251 -0
- flowmesh_cli/commands/trace.py +382 -0
- flowmesh_cli/commands/worker.py +75 -0
- flowmesh_cli/commands/workflow.py +243 -0
- flowmesh_cli/core/__init__.py +1 -0
- flowmesh_cli/core/assets.py +34 -0
- flowmesh_cli/core/logging.py +43 -0
- flowmesh_cli/core/paths.py +32 -0
- flowmesh_cli/core/query.py +36 -0
- flowmesh_cli/core/task.py +30 -0
- flowmesh_cli/core/typer.py +12 -0
- flowmesh_cli-0.1.0.dist-info/METADATA +36 -0
- flowmesh_cli-0.1.0.dist-info/RECORD +25 -0
- flowmesh_cli-0.1.0.dist-info/WHEEL +5 -0
- flowmesh_cli-0.1.0.dist-info/entry_points.txt +2 -0
- flowmesh_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
- flowmesh_cli-0.1.0.dist-info/top_level.txt +1 -0
flowmesh_cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""FlowMesh CLI package."""
|
flowmesh_cli/cli.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""FlowMesh CLI entrypoint."""
|
|
2
|
+
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
from importlib.util import find_spec
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from .core.typer import get_typer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _register_optional(
|
|
12
|
+
module_name: str, app: typer.Typer, register_func: str = "register"
|
|
13
|
+
) -> bool:
|
|
14
|
+
"""Import and register a command module if it is installed.
|
|
15
|
+
|
|
16
|
+
Extras gate availability by deciding which modules are packaged. If a module
|
|
17
|
+
is missing, we silently skip it; unexpected import errors still bubble up.
|
|
18
|
+
"""
|
|
19
|
+
package = __package__ if module_name.startswith(".") else None
|
|
20
|
+
spec = find_spec(module_name, package=package)
|
|
21
|
+
if spec is None:
|
|
22
|
+
return False
|
|
23
|
+
module = import_module(module_name, package=package)
|
|
24
|
+
register = getattr(module, register_func, None)
|
|
25
|
+
if register is None:
|
|
26
|
+
return False
|
|
27
|
+
register(app)
|
|
28
|
+
return True
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def build_cli_app() -> typer.Typer:
|
|
32
|
+
"""Construct the CLI app by attaching available command groups."""
|
|
33
|
+
app = get_typer(help="FlowMesh command line interface.")
|
|
34
|
+
|
|
35
|
+
_register_optional(".commands", app)
|
|
36
|
+
_register_optional("flowmesh_cli_stack", app)
|
|
37
|
+
|
|
38
|
+
return app
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main() -> None:
|
|
42
|
+
build_cli_app()()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
main()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Command modules for the FlowMesh CLI."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from .base import app as base_app
|
|
6
|
+
from .node import app as node_app
|
|
7
|
+
from .result import app as results_app
|
|
8
|
+
from .ssh import app as ssh_app
|
|
9
|
+
from .system import app as system_app
|
|
10
|
+
from .task import app as task_app
|
|
11
|
+
from .trace import app as trace_app
|
|
12
|
+
from .worker import app as worker_app
|
|
13
|
+
from .workflow import app as workflow_app
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def register(app: typer.Typer) -> None:
|
|
17
|
+
"""Register command groups on the root app."""
|
|
18
|
+
app.add_typer(base_app)
|
|
19
|
+
app.add_typer(task_app, name="task")
|
|
20
|
+
app.add_typer(results_app, name="result")
|
|
21
|
+
app.add_typer(workflow_app, name="workflow")
|
|
22
|
+
app.add_typer(system_app, name="system")
|
|
23
|
+
app.add_typer(node_app, name="node")
|
|
24
|
+
app.add_typer(worker_app, name="worker")
|
|
25
|
+
app.add_typer(ssh_app, name="ssh")
|
|
26
|
+
app.add_typer(trace_app, name="trace")
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from flowmesh.client import FlowMesh, resolve_config
|
|
6
|
+
from flowmesh.config import DEFAULT_CONFIG_PATH, FlowMeshConfig
|
|
7
|
+
from flowmesh.exceptions import FlowMeshError
|
|
8
|
+
|
|
9
|
+
from ..core import logging
|
|
10
|
+
from ..core.typer import get_typer
|
|
11
|
+
|
|
12
|
+
app = get_typer()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _redact_api_key(
|
|
16
|
+
api_key: str | None, prefix: int | None = None, suffix: int | None = None
|
|
17
|
+
) -> str | None:
|
|
18
|
+
if api_key is None:
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
length = len(api_key)
|
|
22
|
+
if prefix is None and suffix is None:
|
|
23
|
+
prefix = suffix = 4
|
|
24
|
+
if length <= prefix + suffix:
|
|
25
|
+
return "*" * length # If the key is too short, redact the entire key
|
|
26
|
+
elif prefix is None or suffix is None:
|
|
27
|
+
raise ValueError("Both prefix and suffix must be provided together")
|
|
28
|
+
elif length <= prefix + suffix:
|
|
29
|
+
raise ValueError(
|
|
30
|
+
"API key is too short to redact with the given prefix/suffix lengths"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
masked_middle = "*" * (length - prefix - suffix)
|
|
34
|
+
return f"{api_key[:prefix]}{masked_middle}{api_key[-suffix:]}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@app.command()
|
|
38
|
+
def init(
|
|
39
|
+
url: str = typer.Argument(
|
|
40
|
+
"http://localhost:8000",
|
|
41
|
+
help="FlowMesh server API URL (e.g., http://localhost:8000)",
|
|
42
|
+
),
|
|
43
|
+
api_key: str | None = typer.Option(
|
|
44
|
+
None, "--api-key", help="Optional API key for authentication"
|
|
45
|
+
),
|
|
46
|
+
config_path: Path = typer.Option(
|
|
47
|
+
DEFAULT_CONFIG_PATH, "--config", help="Path to save configuration file"
|
|
48
|
+
),
|
|
49
|
+
force: bool = typer.Option(
|
|
50
|
+
False,
|
|
51
|
+
"--force",
|
|
52
|
+
"-f",
|
|
53
|
+
help="Overwrite existing config file without confirmation",
|
|
54
|
+
show_default=False,
|
|
55
|
+
),
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Initialize configuration for FlowMesh CLI."""
|
|
58
|
+
if config_path.exists():
|
|
59
|
+
if force:
|
|
60
|
+
logging.warning(f"Overwriting existing config at {config_path}")
|
|
61
|
+
else:
|
|
62
|
+
if not typer.confirm(
|
|
63
|
+
f"Config file {config_path} already exists. "
|
|
64
|
+
"Do you want to overwrite it?"
|
|
65
|
+
):
|
|
66
|
+
raise typer.Exit()
|
|
67
|
+
config = FlowMeshConfig(url, api_key)
|
|
68
|
+
config.save(config_path)
|
|
69
|
+
logging.success(f"Config saved to {config_path}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@app.command()
|
|
73
|
+
def deinit(config_path: Path = typer.Option(DEFAULT_CONFIG_PATH, "--config")) -> None:
|
|
74
|
+
"""Delete saved configuration file."""
|
|
75
|
+
if config_path.exists():
|
|
76
|
+
config_path.unlink()
|
|
77
|
+
logging.success(f"Deleted config file {config_path}")
|
|
78
|
+
else:
|
|
79
|
+
logging.warning(f"No config file found at {config_path}")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@app.command()
|
|
83
|
+
def config(
|
|
84
|
+
config_path: Path = typer.Option(DEFAULT_CONFIG_PATH, "--config"),
|
|
85
|
+
source: str = typer.Option(
|
|
86
|
+
"auto", "--source", help="Configuration source: auto, file, or env"
|
|
87
|
+
),
|
|
88
|
+
show_api_key: bool = typer.Option(
|
|
89
|
+
False, "--show-api-key", help="Show API key in plain text"
|
|
90
|
+
),
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Display current configuration."""
|
|
93
|
+
source = source.strip().lower()
|
|
94
|
+
|
|
95
|
+
match source:
|
|
96
|
+
case "file":
|
|
97
|
+
try:
|
|
98
|
+
cfg = FlowMeshConfig.from_file(config_path)
|
|
99
|
+
except Exception as exc:
|
|
100
|
+
logging.error(f"Error loading config from file: {exc}")
|
|
101
|
+
raise typer.Exit(code=1)
|
|
102
|
+
case "env":
|
|
103
|
+
try:
|
|
104
|
+
cfg = FlowMeshConfig.from_env()
|
|
105
|
+
except Exception as exc:
|
|
106
|
+
logging.error(f"Error loading config from environment: {exc}")
|
|
107
|
+
raise typer.Exit(code=1)
|
|
108
|
+
case "auto":
|
|
109
|
+
try:
|
|
110
|
+
cfg = resolve_config(
|
|
111
|
+
base_url=None, api_key=None, config_path=config_path
|
|
112
|
+
)
|
|
113
|
+
except Exception as exc:
|
|
114
|
+
logging.error(f"Error resolving config: {exc}")
|
|
115
|
+
raise typer.Exit(code=1)
|
|
116
|
+
case _:
|
|
117
|
+
logging.error("Invalid --source value. Expected one of: auto, file, env")
|
|
118
|
+
raise typer.Exit(code=2)
|
|
119
|
+
|
|
120
|
+
cfg.api_key = cfg.api_key if show_api_key else _redact_api_key(cfg.api_key)
|
|
121
|
+
logging.log(json.dumps(cfg.to_mapping(), indent=2))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@app.command()
|
|
125
|
+
def info() -> None:
|
|
126
|
+
"""Query server health status and basic system information."""
|
|
127
|
+
client = FlowMesh()
|
|
128
|
+
try:
|
|
129
|
+
resp = client.system.health()
|
|
130
|
+
except FlowMeshError as exc:
|
|
131
|
+
logging.error(str(exc))
|
|
132
|
+
raise typer.Exit(code=1)
|
|
133
|
+
logging.log(resp.model_dump_json(indent=2))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@app.command()
|
|
137
|
+
def health() -> None:
|
|
138
|
+
"""Check if the FlowMesh server is reachable and healthy."""
|
|
139
|
+
info()
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from flowmesh import FlowMesh
|
|
5
|
+
from flowmesh.exceptions import FlowMeshError
|
|
6
|
+
from flowmesh.params import append_param, extend_params
|
|
7
|
+
|
|
8
|
+
from ..core import logging
|
|
9
|
+
from ..core.query import parse_query_filters
|
|
10
|
+
from ..core.typer import get_typer
|
|
11
|
+
|
|
12
|
+
app = get_typer(help="Manage nodes registered with FlowMesh.")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command()
|
|
16
|
+
def info(node_id: str = typer.Argument(..., help="Node identifier")) -> None:
|
|
17
|
+
"""Retrieve information for a specific node."""
|
|
18
|
+
client = FlowMesh()
|
|
19
|
+
try:
|
|
20
|
+
node = client.nodes.retrieve(node_id)
|
|
21
|
+
except FlowMeshError as exc:
|
|
22
|
+
logging.error(str(exc))
|
|
23
|
+
raise typer.Exit(code=1)
|
|
24
|
+
logging.log(node.model_dump_json(indent=2))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.command("list")
|
|
28
|
+
def list_nodes(
|
|
29
|
+
node_id: str | None = typer.Option(None, "--id", help="Filter by node id"),
|
|
30
|
+
namespace: str | None = typer.Option(
|
|
31
|
+
None, "--namespace", help="Filter by node namespace"
|
|
32
|
+
),
|
|
33
|
+
cluster: str | None = typer.Option(
|
|
34
|
+
None, "--cluster", help="Filter by node cluster"
|
|
35
|
+
),
|
|
36
|
+
alias: str | None = typer.Option(None, "--alias", help="Filter by node alias"),
|
|
37
|
+
tag: list[str] | None = typer.Option(
|
|
38
|
+
None, "--tag", help="Filter by tag (repeatable)"
|
|
39
|
+
),
|
|
40
|
+
query: list[str] | None = typer.Option(
|
|
41
|
+
None, "--query", "-q", help="Filter nodes by key=value pairs"
|
|
42
|
+
),
|
|
43
|
+
) -> None:
|
|
44
|
+
"""List all nodes registered with FlowMesh."""
|
|
45
|
+
client = FlowMesh()
|
|
46
|
+
query_params = parse_query_filters(query)
|
|
47
|
+
try:
|
|
48
|
+
nodes = client.nodes.list(
|
|
49
|
+
node_id=node_id,
|
|
50
|
+
namespace=namespace,
|
|
51
|
+
cluster=cluster,
|
|
52
|
+
alias=alias,
|
|
53
|
+
tags=tag or None,
|
|
54
|
+
query_params=query_params,
|
|
55
|
+
)
|
|
56
|
+
except FlowMeshError as exc:
|
|
57
|
+
logging.error(str(exc))
|
|
58
|
+
raise typer.Exit(code=1)
|
|
59
|
+
logging.log(json.dumps([s.model_dump(mode="json") for s in nodes], indent=2))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
worker_app = get_typer(help="Manage workers on nodes.")
|
|
63
|
+
app.add_typer(worker_app, name="worker")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@worker_app.command("list")
|
|
67
|
+
def list_workers(
|
|
68
|
+
node_id: str | None = typer.Argument(None, help="Node identifier"),
|
|
69
|
+
worker_id: str | None = typer.Option(None, "--id", help="Filter by worker id"),
|
|
70
|
+
name: str | None = typer.Option(None, "--name", help="Filter by worker name"),
|
|
71
|
+
namespace: str | None = typer.Option(
|
|
72
|
+
None, "--namespace", help="Filter by worker namespace"
|
|
73
|
+
),
|
|
74
|
+
cluster: str | None = typer.Option(
|
|
75
|
+
None, "--cluster", help="Filter by worker cluster"
|
|
76
|
+
),
|
|
77
|
+
provider: str | None = typer.Option(
|
|
78
|
+
None, "--provider", help="Filter by worker provider"
|
|
79
|
+
),
|
|
80
|
+
status: list[str] | None = typer.Option(
|
|
81
|
+
None, "--status", "-s", help="Filter by status (repeatable)"
|
|
82
|
+
),
|
|
83
|
+
node_id_filter: str | None = typer.Option(
|
|
84
|
+
None, "--node-id", help="Filter by associated node id"
|
|
85
|
+
),
|
|
86
|
+
node_alias: str | None = typer.Option(
|
|
87
|
+
None, "--node-alias", help="Filter by associated node alias"
|
|
88
|
+
),
|
|
89
|
+
query: list[str] | None = typer.Option(
|
|
90
|
+
None, "--query", "-q", help="Filter workers by key=value pairs"
|
|
91
|
+
),
|
|
92
|
+
) -> None:
|
|
93
|
+
"""List workers on a specific node or all nodes."""
|
|
94
|
+
client = FlowMesh()
|
|
95
|
+
query_params = parse_query_filters(query)
|
|
96
|
+
append_param(query_params, "id", worker_id)
|
|
97
|
+
append_param(query_params, "name", name)
|
|
98
|
+
append_param(query_params, "namespace", namespace)
|
|
99
|
+
append_param(query_params, "cluster", cluster)
|
|
100
|
+
append_param(query_params, "provider", provider)
|
|
101
|
+
extend_params(query_params, "status", status)
|
|
102
|
+
append_param(query_params, "node_id", node_id_filter)
|
|
103
|
+
append_param(query_params, "node_alias", node_alias)
|
|
104
|
+
try:
|
|
105
|
+
if node_id is None:
|
|
106
|
+
workers = client.nodes.list_all_workers(query_params=query_params)
|
|
107
|
+
else:
|
|
108
|
+
workers = client.nodes.list_workers(node_id, query_params=query_params)
|
|
109
|
+
except FlowMeshError as exc:
|
|
110
|
+
logging.error(str(exc))
|
|
111
|
+
raise typer.Exit(code=1)
|
|
112
|
+
logging.log(json.dumps([w.model_dump(mode="json") for w in workers], indent=2))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@worker_app.command("start")
|
|
116
|
+
def start_worker(
|
|
117
|
+
node_id: str = typer.Argument(..., help="Node identifier"),
|
|
118
|
+
worker_name: str = typer.Argument(..., help="Worker name"),
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Start a worker on a specific node."""
|
|
121
|
+
client = FlowMesh()
|
|
122
|
+
try:
|
|
123
|
+
client.nodes.start_worker(node_id, worker_name)
|
|
124
|
+
except FlowMeshError as exc:
|
|
125
|
+
logging.error(str(exc))
|
|
126
|
+
raise typer.Exit(code=1)
|
|
127
|
+
logging.success(f"Worker '{worker_name}' started on node '{node_id}'")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@worker_app.command("stop")
|
|
131
|
+
def stop_worker(
|
|
132
|
+
node_id: str = typer.Argument(..., help="Node identifier"),
|
|
133
|
+
worker_name: str = typer.Argument(..., help="Worker name"),
|
|
134
|
+
) -> None:
|
|
135
|
+
"""Stop a worker on a specific node."""
|
|
136
|
+
client = FlowMesh()
|
|
137
|
+
try:
|
|
138
|
+
client.nodes.stop_worker(node_id, worker_name)
|
|
139
|
+
except FlowMeshError as exc:
|
|
140
|
+
logging.error(str(exc))
|
|
141
|
+
raise typer.Exit(code=1)
|
|
142
|
+
logging.success(f"Worker '{worker_name}' stopped on node '{node_id}'")
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from flowmesh import FlowMesh
|
|
6
|
+
from flowmesh.exceptions import FlowMeshError
|
|
7
|
+
|
|
8
|
+
from ..core import logging
|
|
9
|
+
from ..core.typer import get_typer
|
|
10
|
+
|
|
11
|
+
app = get_typer(help="Retrieve task execution results and output artifacts.")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command("fetch")
|
|
15
|
+
def fetch(
|
|
16
|
+
task_id: str = typer.Argument(..., help="Task identifier"),
|
|
17
|
+
output: Path | None = typer.Option(
|
|
18
|
+
None, "--output", "-o", help="Directory to write result JSON"
|
|
19
|
+
),
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Download task result JSON and optionally save it to a local file."""
|
|
22
|
+
client = FlowMesh()
|
|
23
|
+
if output:
|
|
24
|
+
try:
|
|
25
|
+
payload, target, downloaded = client.results.materialize(task_id, output)
|
|
26
|
+
except FlowMeshError as exc:
|
|
27
|
+
logging.error(str(exc))
|
|
28
|
+
raise typer.Exit(code=1)
|
|
29
|
+
if downloaded:
|
|
30
|
+
logging.log(f"Downloaded images to {output / f'{task_id}-artifacts'}")
|
|
31
|
+
logging.log(f"Wrote result to {target}")
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
payload = client.results.retrieve(task_id)
|
|
36
|
+
except FlowMeshError as exc:
|
|
37
|
+
logging.error(str(exc))
|
|
38
|
+
raise typer.Exit(code=1)
|
|
39
|
+
logging.log(json.dumps(payload, indent=2))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command("download")
|
|
43
|
+
def download_result_files(
|
|
44
|
+
task_id: str = typer.Argument(..., help="Task identifier"),
|
|
45
|
+
file_paths: list[str] = typer.Argument(..., help="List of file paths to download"),
|
|
46
|
+
output_dir: Path = typer.Option(
|
|
47
|
+
..., "--output", "-o", help="Directory to save downloaded files"
|
|
48
|
+
),
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Download specified result files for a task."""
|
|
51
|
+
client = FlowMesh()
|
|
52
|
+
try:
|
|
53
|
+
for path in client.results.download_files(task_id, file_paths, output_dir):
|
|
54
|
+
logging.log(f"Wrote file to {path}")
|
|
55
|
+
except FlowMeshError as exc:
|
|
56
|
+
logging.error(str(exc))
|
|
57
|
+
raise typer.Exit(code=1)
|