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.
@@ -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)