lumilake-cli 0.1.0.dev1__tar.gz → 0.1.2__tar.gz
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.
- {lumilake_cli-0.1.0.dev1/src/lumilake_cli.egg-info → lumilake_cli-0.1.2}/PKG-INFO +3 -3
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.2}/pyproject.toml +3 -3
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.2}/src/lumilake_cli/commands/__init__.py +1 -0
- lumilake_cli-0.1.2/src/lumilake_cli/commands/base.py +139 -0
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.2}/src/lumilake_cli/commands/deploy.py +166 -16
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.2}/src/lumilake_cli/commands/job.py +20 -3
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.2}/src/lumilake_cli/commands/trace.py +7 -1
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.2}/src/lumilake_cli/commands/worker.py +6 -2
- lumilake_cli-0.1.2/src/lumilake_cli/core/config.py +5 -0
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.2}/src/lumilake_cli/core/http.py +32 -27
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.2/src/lumilake_cli.egg-info}/PKG-INFO +3 -3
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.2}/src/lumilake_cli.egg-info/SOURCES.txt +0 -1
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.2}/src/lumilake_cli.egg-info/requires.txt +2 -2
- lumilake_cli-0.1.0.dev1/src/lumilake_cli/commands/base.py +0 -85
- lumilake_cli-0.1.0.dev1/src/lumilake_cli/core/config.py +0 -38
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.2}/LICENSE +0 -0
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.2}/README.md +0 -0
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.2}/setup.cfg +0 -0
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.2}/src/lumilake_cli/__init__.py +0 -0
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.2}/src/lumilake_cli/cli.py +0 -0
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.2}/src/lumilake_cli/core/__init__.py +0 -0
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.2}/src/lumilake_cli/core/logging.py +0 -0
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.2}/src/lumilake_cli/core/query.py +0 -0
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.2}/src/lumilake_cli/core/typer.py +0 -0
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.2}/src/lumilake_cli.egg-info/dependency_links.txt +0 -0
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.2}/src/lumilake_cli.egg-info/entry_points.txt +0 -0
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.2}/src/lumilake_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lumilake-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Lumilake command-line interface
|
|
5
5
|
Author: Lumilake contributors
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -14,12 +14,12 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
14
14
|
Requires-Python: <3.13,>=3.12
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
License-File: LICENSE
|
|
17
|
-
Requires-Dist: lumilake-sdk==0.1.
|
|
17
|
+
Requires-Dist: lumilake-sdk==0.1.2
|
|
18
18
|
Requires-Dist: flowmesh-sdk==0.1.1
|
|
19
19
|
Requires-Dist: requests>=2.32.3
|
|
20
20
|
Requires-Dist: typer>=0.12.5
|
|
21
21
|
Provides-Extra: deploy
|
|
22
|
-
Requires-Dist: lumilake-deploy==0.1.
|
|
22
|
+
Requires-Dist: lumilake-deploy==0.1.2; extra == "deploy"
|
|
23
23
|
Dynamic: license-file
|
|
24
24
|
|
|
25
25
|
# `lumilake-cli`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "lumilake-cli"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.2"
|
|
4
4
|
description = "Lumilake command-line interface"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = "<3.13,>=3.12"
|
|
@@ -10,7 +10,7 @@ authors = [
|
|
|
10
10
|
{name = "Lumilake contributors"},
|
|
11
11
|
]
|
|
12
12
|
dependencies = [
|
|
13
|
-
"lumilake-sdk==0.1.
|
|
13
|
+
"lumilake-sdk==0.1.2",
|
|
14
14
|
# Renders ``lumilake trace`` ProfileSummary payloads.
|
|
15
15
|
"flowmesh-sdk==0.1.1",
|
|
16
16
|
"requests>=2.32.3",
|
|
@@ -26,7 +26,7 @@ classifiers = [
|
|
|
26
26
|
|
|
27
27
|
[project.optional-dependencies]
|
|
28
28
|
# Thin ``pip install lumilake-cli`` (no extra) skips Docker/FlowMesh.
|
|
29
|
-
deploy = ["lumilake-deploy==0.1.
|
|
29
|
+
deploy = ["lumilake-deploy==0.1.2"]
|
|
30
30
|
|
|
31
31
|
[project.urls]
|
|
32
32
|
Homepage = "https://github.com/mlsys-io/lumilake_OSS"
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from lumilake._base_client import resolve_config
|
|
6
|
+
from lumilake.config import LumilakeConfig
|
|
7
|
+
from lumilake.errors import ConfigInvalidError, ConfigNotFoundError
|
|
8
|
+
|
|
9
|
+
from ..core import logging
|
|
10
|
+
from ..core.config import DEFAULT_CONFIG_PATH
|
|
11
|
+
from ..core.http import HttpError, client_from_config
|
|
12
|
+
from ..core.typer import get_typer
|
|
13
|
+
|
|
14
|
+
app = get_typer()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _redact_api_key(
|
|
18
|
+
api_key: str | None, prefix: int | None = None, suffix: int | None = None
|
|
19
|
+
) -> str | None:
|
|
20
|
+
if api_key is None:
|
|
21
|
+
return None
|
|
22
|
+
length = len(api_key)
|
|
23
|
+
if prefix is None and suffix is None:
|
|
24
|
+
prefix = suffix = 4
|
|
25
|
+
if length <= prefix + suffix:
|
|
26
|
+
return "*" * length
|
|
27
|
+
elif prefix is None or suffix is None:
|
|
28
|
+
raise ValueError("Both prefix and suffix must be provided together")
|
|
29
|
+
elif length <= prefix + suffix:
|
|
30
|
+
raise ValueError(
|
|
31
|
+
"API key is too short to redact with the given prefix/suffix lengths"
|
|
32
|
+
)
|
|
33
|
+
masked = "*" * (length - prefix - suffix)
|
|
34
|
+
return f"{api_key[:prefix]}{masked}{api_key[-suffix:]}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@app.command()
|
|
38
|
+
def init(
|
|
39
|
+
url: str = typer.Argument(
|
|
40
|
+
"http://127.0.0.1:9000",
|
|
41
|
+
help="Lumilake server URL (e.g. http://127.0.0.1:9000).",
|
|
42
|
+
),
|
|
43
|
+
api_key: str | None = typer.Option(
|
|
44
|
+
None, "--api-key", help="Bearer presented as Authorization: Bearer …"
|
|
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 the Lumilake CLI."""
|
|
58
|
+
if config_path.exists():
|
|
59
|
+
if force:
|
|
60
|
+
logging.warning(f"Overwriting existing config at {config_path}")
|
|
61
|
+
elif not typer.confirm(f"Config file {config_path} already exists. Overwrite?"):
|
|
62
|
+
raise typer.Exit()
|
|
63
|
+
LumilakeConfig(base_url=url, api_key=api_key).save(config_path)
|
|
64
|
+
logging.success(f"Config saved to {config_path}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@app.command()
|
|
68
|
+
def deinit(
|
|
69
|
+
config_path: Path = typer.Option(
|
|
70
|
+
DEFAULT_CONFIG_PATH, "--config", help="Path to configuration file."
|
|
71
|
+
),
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Delete the saved configuration file."""
|
|
74
|
+
if config_path.exists():
|
|
75
|
+
config_path.unlink()
|
|
76
|
+
logging.success(f"Deleted config file {config_path}")
|
|
77
|
+
else:
|
|
78
|
+
logging.warning(f"No config file found at {config_path}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@app.command()
|
|
82
|
+
def config(
|
|
83
|
+
config_path: Path = typer.Option(
|
|
84
|
+
DEFAULT_CONFIG_PATH, "--config", help="Path to configuration file."
|
|
85
|
+
),
|
|
86
|
+
source: str = typer.Option(
|
|
87
|
+
"auto",
|
|
88
|
+
"--source",
|
|
89
|
+
help="Configuration source: auto, file, or env.",
|
|
90
|
+
),
|
|
91
|
+
show_api_key: bool = typer.Option(
|
|
92
|
+
False, "--show-api-key", help="Show api_key in plain text."
|
|
93
|
+
),
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Display the resolved Lumilake CLI configuration."""
|
|
96
|
+
normalized = source.strip().lower()
|
|
97
|
+
match normalized:
|
|
98
|
+
case "file":
|
|
99
|
+
try:
|
|
100
|
+
cfg = LumilakeConfig.from_file(config_path)
|
|
101
|
+
except (ConfigNotFoundError, ConfigInvalidError) as exc:
|
|
102
|
+
logging.error(f"Error loading config from file: {exc}")
|
|
103
|
+
raise typer.Exit(code=1)
|
|
104
|
+
case "env":
|
|
105
|
+
try:
|
|
106
|
+
cfg = LumilakeConfig.from_env()
|
|
107
|
+
except ConfigInvalidError as exc:
|
|
108
|
+
logging.error(f"Error loading config from environment: {exc}")
|
|
109
|
+
raise typer.Exit(code=1)
|
|
110
|
+
case "auto":
|
|
111
|
+
try:
|
|
112
|
+
cfg = resolve_config(config_path=config_path)
|
|
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."""
|
|
127
|
+
client = client_from_config()
|
|
128
|
+
try:
|
|
129
|
+
response = client.get("/healthz")
|
|
130
|
+
except HttpError as exc:
|
|
131
|
+
logging.error(str(exc))
|
|
132
|
+
raise typer.Exit(code=1)
|
|
133
|
+
logging.log(json.dumps(response.json(), indent=2))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@app.command()
|
|
137
|
+
def health() -> None:
|
|
138
|
+
"""Check if the Lumilake server is reachable and healthy."""
|
|
139
|
+
info()
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
"""Deploy commands: init, doctor, up, down, clean, restart, reset, logs."""
|
|
1
|
+
"""Deploy commands: init, doctor, up, down, clean, purge, restart, reset, logs."""
|
|
2
2
|
|
|
3
3
|
import datetime as dt
|
|
4
|
+
import difflib
|
|
4
5
|
import re
|
|
5
6
|
import sys
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
import typer
|
|
9
10
|
from flowmesh_cli_stack.stack import stack_env_example
|
|
11
|
+
from lumilake import envs
|
|
12
|
+
from lumilake.errors import ConfigInvalidError, ConfigNotFoundError
|
|
10
13
|
from lumilake_deploy import docker_client
|
|
14
|
+
from lumilake_deploy import purge as purge_mod
|
|
11
15
|
from lumilake_deploy import setup as setup_mod
|
|
12
16
|
from lumilake_deploy import stop as stop_mod
|
|
13
17
|
from lumilake_deploy import update_flowmesh as update_fm_mod
|
|
@@ -27,6 +31,7 @@ from lumilake_deploy.setup import (
|
|
|
27
31
|
)
|
|
28
32
|
|
|
29
33
|
from ..core import logging
|
|
34
|
+
from ..core.config import DEFAULT_CONFIG_PATH, LumilakeConfig
|
|
30
35
|
from ..core.typer import get_typer
|
|
31
36
|
|
|
32
37
|
app = get_typer(
|
|
@@ -83,9 +88,44 @@ def _run_setup(
|
|
|
83
88
|
raise typer.Exit(code=1) from exc
|
|
84
89
|
|
|
85
90
|
|
|
86
|
-
def
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
def _preview_write(target: Path, new_content: str) -> None:
|
|
92
|
+
if target.exists():
|
|
93
|
+
old = target.read_text().splitlines(keepends=True)
|
|
94
|
+
new = new_content.splitlines(keepends=True)
|
|
95
|
+
diff = "".join(
|
|
96
|
+
difflib.unified_diff(
|
|
97
|
+
old,
|
|
98
|
+
new,
|
|
99
|
+
fromfile=f"a/{target.name}",
|
|
100
|
+
tofile=f"b/{target.name}",
|
|
101
|
+
n=3,
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
if diff:
|
|
105
|
+
logging.info(f"Diff vs existing {target}:")
|
|
106
|
+
for line in diff.splitlines():
|
|
107
|
+
logging.info(line)
|
|
108
|
+
else:
|
|
109
|
+
logging.info(f"{target} already matches the bundled template.")
|
|
110
|
+
return
|
|
111
|
+
logging.info(f"Preview of {target} (first 20 lines):")
|
|
112
|
+
for line in new_content.splitlines()[:20]:
|
|
113
|
+
logging.info(line)
|
|
114
|
+
remaining = max(0, len(new_content.splitlines()) - 20)
|
|
115
|
+
if remaining:
|
|
116
|
+
logging.info(f"... and {remaining} more line(s).")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _confirm_overwrite(target: Path, *, force: bool, new_content: str) -> bool:
|
|
120
|
+
"""Return True when the target may be written, False to skip.
|
|
121
|
+
|
|
122
|
+
Shows a preview (or diff vs an existing file) before prompting on
|
|
123
|
+
overwrite. Brand-new writes pass through without prompting.
|
|
124
|
+
"""
|
|
125
|
+
if force:
|
|
126
|
+
return True
|
|
127
|
+
_preview_write(target, new_content)
|
|
128
|
+
if not target.exists():
|
|
89
129
|
return True
|
|
90
130
|
if typer.confirm(f"{target} already exists. Overwrite?", default=False):
|
|
91
131
|
return True
|
|
@@ -156,21 +196,23 @@ def init(
|
|
|
156
196
|
root = _project_dir(ctx)
|
|
157
197
|
template = env_example_path()
|
|
158
198
|
target = root / ENV_FILE_NAME
|
|
199
|
+
template_text = template.read_text()
|
|
159
200
|
wrote_env = False
|
|
160
|
-
if _confirm_overwrite(target, force=force):
|
|
161
|
-
target.write_text(
|
|
201
|
+
if _confirm_overwrite(target, force=force, new_content=template_text):
|
|
202
|
+
target.write_text(template_text)
|
|
162
203
|
logging.success(f"Wrote {target} from packaged {template.name}.")
|
|
163
204
|
wrote_env = True
|
|
164
205
|
|
|
165
206
|
if flowmesh:
|
|
166
207
|
fm_target = root / FLOWMESH_ENV_FILE_NAME
|
|
208
|
+
example = stack_env_example()
|
|
209
|
+
if not example.exists():
|
|
210
|
+
logging.error(f"FlowMesh env example not found: {example}")
|
|
211
|
+
raise typer.Exit(code=1)
|
|
212
|
+
fm_template_text = example.read_text()
|
|
167
213
|
wrote_flowmesh_env = False
|
|
168
|
-
if _confirm_overwrite(fm_target, force=force):
|
|
169
|
-
|
|
170
|
-
if not example.exists():
|
|
171
|
-
logging.error(f"FlowMesh env example not found: {example}")
|
|
172
|
-
raise typer.Exit(code=1)
|
|
173
|
-
fm_target.write_text(example.read_text())
|
|
214
|
+
if _confirm_overwrite(fm_target, force=force, new_content=fm_template_text):
|
|
215
|
+
fm_target.write_text(fm_template_text)
|
|
174
216
|
logging.success(f"Wrote {fm_target} from {example.name}.")
|
|
175
217
|
_patch_flowmesh_env(fm_target)
|
|
176
218
|
wrote_flowmesh_env = True
|
|
@@ -240,6 +282,34 @@ def pull(ctx: typer.Context) -> None:
|
|
|
240
282
|
raise typer.Exit(code=1) from exc
|
|
241
283
|
|
|
242
284
|
|
|
285
|
+
def _write_cli_config(root: Path, config_path: Path | None = None) -> None:
|
|
286
|
+
"""Persist the local server URL so subsequent CLI / SDK calls find it.
|
|
287
|
+
|
|
288
|
+
Preserves any existing ``api_key`` (written by ``lumilake init``).
|
|
289
|
+
"""
|
|
290
|
+
if config_path is None:
|
|
291
|
+
config_path = DEFAULT_CONFIG_PATH
|
|
292
|
+
setup_mod.load_project_env(root)
|
|
293
|
+
port = envs.LUMILAKE_SERVER_PORT or 9000
|
|
294
|
+
base_url = f"http://127.0.0.1:{port}"
|
|
295
|
+
try:
|
|
296
|
+
existing = LumilakeConfig.from_file(config_path)
|
|
297
|
+
except (ConfigNotFoundError, ConfigInvalidError):
|
|
298
|
+
existing = None
|
|
299
|
+
if existing is not None and existing.base_url == base_url:
|
|
300
|
+
logging.info(f"CLI config already points at {base_url}.")
|
|
301
|
+
return
|
|
302
|
+
if existing is not None and existing.base_url != base_url:
|
|
303
|
+
logging.info(f"Updating {config_path}: {existing.base_url} -> {base_url}")
|
|
304
|
+
api_key = existing.api_key if existing is not None else None
|
|
305
|
+
try:
|
|
306
|
+
LumilakeConfig(base_url=base_url, api_key=api_key).save(config_path)
|
|
307
|
+
except OSError as exc:
|
|
308
|
+
logging.warning(f"Could not save CLI config to {config_path}: {exc}")
|
|
309
|
+
return
|
|
310
|
+
logging.info(f"Saved CLI config to {config_path} (base_url={base_url}).")
|
|
311
|
+
|
|
312
|
+
|
|
243
313
|
@app.command()
|
|
244
314
|
def up(ctx: typer.Context) -> None:
|
|
245
315
|
"""Start the full Lumilake stack (Docker-backed).
|
|
@@ -247,7 +317,9 @@ def up(ctx: typer.Context) -> None:
|
|
|
247
317
|
The server image must already be present locally — run
|
|
248
318
|
``lumilake deploy pull`` or ``lumilake deploy build`` first.
|
|
249
319
|
"""
|
|
250
|
-
|
|
320
|
+
root = _project_dir(ctx)
|
|
321
|
+
_run_setup(root, background=True)
|
|
322
|
+
_write_cli_config(root)
|
|
251
323
|
|
|
252
324
|
|
|
253
325
|
@app.command()
|
|
@@ -266,7 +338,15 @@ def down(
|
|
|
266
338
|
),
|
|
267
339
|
),
|
|
268
340
|
) -> None:
|
|
269
|
-
"""Stop
|
|
341
|
+
"""Stop the stack but keep data volumes.
|
|
342
|
+
|
|
343
|
+
Safe to run between sessions: the archive bucket (job records, run
|
|
344
|
+
artifacts) and the compute postgres/minio volumes survive, so
|
|
345
|
+
``deploy up`` resumes against the same state. ``--wipe-archive`` also
|
|
346
|
+
removes the compute Postgres and FlowMesh runtime-state volumes, while
|
|
347
|
+
preserving MinIO corpus data. Use ``deploy reset`` (destructive) to
|
|
348
|
+
wipe every volume instead.
|
|
349
|
+
"""
|
|
270
350
|
try:
|
|
271
351
|
stop_mod.run_stop(
|
|
272
352
|
_project_dir(ctx),
|
|
@@ -288,6 +368,56 @@ def clean(ctx: typer.Context) -> None:
|
|
|
288
368
|
raise typer.Exit(code=1) from exc
|
|
289
369
|
|
|
290
370
|
|
|
371
|
+
@app.command()
|
|
372
|
+
def purge(
|
|
373
|
+
ctx: typer.Context,
|
|
374
|
+
image_tag: str = typer.Argument(
|
|
375
|
+
...,
|
|
376
|
+
help="Lumilake server image tag to purge from local Docker.",
|
|
377
|
+
),
|
|
378
|
+
dry_run: bool = typer.Option(
|
|
379
|
+
False,
|
|
380
|
+
"--dry-run",
|
|
381
|
+
help="List the image to be purged without deleting it.",
|
|
382
|
+
),
|
|
383
|
+
force: bool = typer.Option(
|
|
384
|
+
False,
|
|
385
|
+
"--force",
|
|
386
|
+
help="Force image removal even if Docker reports dependent containers.",
|
|
387
|
+
),
|
|
388
|
+
) -> None:
|
|
389
|
+
"""Purge one Lumilake server image tag from local Docker."""
|
|
390
|
+
try:
|
|
391
|
+
plan = purge_mod.build_server_image_purge_plan(
|
|
392
|
+
_project_dir(ctx),
|
|
393
|
+
image_tag=image_tag,
|
|
394
|
+
)
|
|
395
|
+
except DeployError as exc:
|
|
396
|
+
logging.error(str(exc))
|
|
397
|
+
raise typer.Exit(code=1) from exc
|
|
398
|
+
|
|
399
|
+
logging.info(f"Purging Lumilake server image with tag '{image_tag}'...")
|
|
400
|
+
if not plan.exists:
|
|
401
|
+
logging.warning(f"Image not found: {plan.image_ref}")
|
|
402
|
+
logging.info("No images to purge.")
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
if dry_run:
|
|
406
|
+
logging.info("Images to be purged:")
|
|
407
|
+
logging.info(f" {plan.image_ref}")
|
|
408
|
+
return
|
|
409
|
+
|
|
410
|
+
try:
|
|
411
|
+
result = purge_mod.run_server_image_purge(plan, force=force)
|
|
412
|
+
except DeployError as exc:
|
|
413
|
+
logging.error(str(exc))
|
|
414
|
+
raise typer.Exit(code=1) from exc
|
|
415
|
+
if result.removed:
|
|
416
|
+
logging.success(f"Removed image: {result.image_ref}")
|
|
417
|
+
return
|
|
418
|
+
logging.warning(f"Image already absent: {result.image_ref}")
|
|
419
|
+
|
|
420
|
+
|
|
291
421
|
@app.command()
|
|
292
422
|
def status(ctx: typer.Context) -> None:
|
|
293
423
|
"""Show the running state of every Lumilake stack container."""
|
|
@@ -354,8 +484,28 @@ def restart(
|
|
|
354
484
|
|
|
355
485
|
|
|
356
486
|
@app.command()
|
|
357
|
-
def reset(
|
|
358
|
-
|
|
487
|
+
def reset(
|
|
488
|
+
ctx: typer.Context,
|
|
489
|
+
yes: bool = typer.Option(
|
|
490
|
+
False,
|
|
491
|
+
"--yes",
|
|
492
|
+
"-y",
|
|
493
|
+
help="Skip the destructive-action confirmation prompt.",
|
|
494
|
+
),
|
|
495
|
+
) -> None:
|
|
496
|
+
"""Wipe the archive and every volume, then start the stack fresh.
|
|
497
|
+
|
|
498
|
+
Destructive: removes the compute postgres / minio volumes (job
|
|
499
|
+
records, run artifacts, demo data) and the FlowMesh runtime state.
|
|
500
|
+
Use ``deploy down`` if you want to stop the stack but keep its data.
|
|
501
|
+
"""
|
|
502
|
+
if not yes and not typer.confirm(
|
|
503
|
+
"deploy reset deletes every Lumilake volume (archive + compute "
|
|
504
|
+
"data). Continue?",
|
|
505
|
+
default=False,
|
|
506
|
+
):
|
|
507
|
+
logging.info("Aborted.")
|
|
508
|
+
raise typer.Exit(code=0)
|
|
359
509
|
root = _project_dir(ctx)
|
|
360
510
|
try:
|
|
361
511
|
stop_mod.run_stop(root, purge=True)
|
|
@@ -135,13 +135,30 @@ def submit(
|
|
|
135
135
|
None, "--batch-size", help="Input batch size"
|
|
136
136
|
),
|
|
137
137
|
input_values: list[str] | None = typer.Option(
|
|
138
|
-
None,
|
|
138
|
+
None,
|
|
139
|
+
"--input",
|
|
140
|
+
help=(
|
|
141
|
+
"Input as Name=val1,val2,val3 (comma-separated; whitespace around "
|
|
142
|
+
"values is trimmed). Repeat for different names: "
|
|
143
|
+
"`--input query=NVDA,TSLA --input year=2024`. A single value "
|
|
144
|
+
"still uses the same syntax: `--input query=NVDA`."
|
|
145
|
+
),
|
|
139
146
|
),
|
|
140
147
|
input_files: list[str] | None = typer.Option(
|
|
141
|
-
None,
|
|
148
|
+
None,
|
|
149
|
+
"--input-file",
|
|
150
|
+
help=(
|
|
151
|
+
"Input from file as Name=path.txt; one value per non-empty line. "
|
|
152
|
+
"Repeatable: `--input-file tickers=tickers.txt`."
|
|
153
|
+
),
|
|
142
154
|
),
|
|
143
155
|
input_json: Path | None = typer.Option(
|
|
144
|
-
None,
|
|
156
|
+
None,
|
|
157
|
+
"--input-json",
|
|
158
|
+
help=(
|
|
159
|
+
"JSON file with the full inputs object, e.g. "
|
|
160
|
+
'`{"query": ["NVDA", "TSLA"], "year": ["2024"]}`.'
|
|
161
|
+
),
|
|
145
162
|
),
|
|
146
163
|
) -> None:
|
|
147
164
|
"""Submit a workflow for optimization and execution."""
|
|
@@ -25,6 +25,8 @@ OutputFormat = typer.Option(
|
|
|
25
25
|
help="Output format: summary | json | mermaid",
|
|
26
26
|
)
|
|
27
27
|
|
|
28
|
+
_JSON_OPTION_HELP = "Shortcut for ``--format json``."
|
|
29
|
+
|
|
28
30
|
|
|
29
31
|
@app.command("list")
|
|
30
32
|
def list_traces(
|
|
@@ -46,14 +48,18 @@ def list_traces(
|
|
|
46
48
|
def get_trace(
|
|
47
49
|
exec_id: str = typer.Argument(..., help="Execution trace identifier"),
|
|
48
50
|
output_format: str = OutputFormat,
|
|
51
|
+
json_output: bool = typer.Option(False, "--json", help=_JSON_OPTION_HELP),
|
|
49
52
|
) -> None:
|
|
50
53
|
"""Retrieve and render a specific execution trace.
|
|
51
54
|
|
|
52
55
|
Default ``summary`` shows the FlowMesh ``ProfileSummary`` as
|
|
53
56
|
headline metrics + hardware / network / critical-path tables.
|
|
54
57
|
``json`` returns the raw payload; ``mermaid`` prints the lineage
|
|
55
|
-
graph in Mermaid syntax.
|
|
58
|
+
graph in Mermaid syntax. ``--json`` is a shortcut for
|
|
59
|
+
``--format json``.
|
|
56
60
|
"""
|
|
61
|
+
if json_output:
|
|
62
|
+
output_format = "json"
|
|
57
63
|
client = client_from_config()
|
|
58
64
|
try:
|
|
59
65
|
response = client.get(f"/trace/{exec_id}", version_prefix=True)
|
|
@@ -9,6 +9,10 @@ from ..core.typer import get_typer
|
|
|
9
9
|
app = get_typer(help="Query runtime workers.")
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
def _emit_json(payload: object) -> None:
|
|
13
|
+
logging.log(json.dumps(payload, indent=2))
|
|
14
|
+
|
|
15
|
+
|
|
12
16
|
@app.command("list")
|
|
13
17
|
def list_workers() -> None:
|
|
14
18
|
"""List all available workers."""
|
|
@@ -18,7 +22,7 @@ def list_workers() -> None:
|
|
|
18
22
|
except HttpError as exc:
|
|
19
23
|
logging.error(str(exc))
|
|
20
24
|
raise typer.Exit(code=1)
|
|
21
|
-
|
|
25
|
+
_emit_json(response.json())
|
|
22
26
|
|
|
23
27
|
|
|
24
28
|
@app.command("get")
|
|
@@ -32,4 +36,4 @@ def get_worker(
|
|
|
32
36
|
except HttpError as exc:
|
|
33
37
|
logging.error(str(exc))
|
|
34
38
|
raise typer.Exit(code=1)
|
|
35
|
-
|
|
39
|
+
_emit_json(response.json())
|
|
@@ -3,20 +3,22 @@
|
|
|
3
3
|
from collections.abc import Mapping
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Any
|
|
6
|
+
from typing import Any
|
|
7
7
|
|
|
8
8
|
import requests
|
|
9
|
-
import typer
|
|
10
9
|
from lumilake import envs
|
|
10
|
+
from lumilake._base_client import DEFAULT_BASE_URL, resolve_config
|
|
11
|
+
from lumilake.errors import ConfigInvalidError, ConfigNotFoundError
|
|
11
12
|
|
|
12
|
-
from . import
|
|
13
|
-
from .config import DEFAULT_CONFIG_PATH, LumilakeConfig, load_config
|
|
13
|
+
from .config import DEFAULT_CONFIG_PATH, LumilakeConfig
|
|
14
14
|
|
|
15
15
|
API_VERSION_PREFIX = "/api/v1"
|
|
16
16
|
|
|
17
17
|
DEFAULT_TIMEOUT: float = 300.0
|
|
18
18
|
"""Default request timeout in seconds."""
|
|
19
19
|
|
|
20
|
+
DEFAULT_LOCAL_BASE_URL = DEFAULT_BASE_URL
|
|
21
|
+
|
|
20
22
|
|
|
21
23
|
def _resolve_timeout() -> float:
|
|
22
24
|
"""Read timeout from ``LUMILAKE_TIMEOUT`` env var, falling back to default."""
|
|
@@ -30,10 +32,14 @@ class HttpError(RuntimeError):
|
|
|
30
32
|
@dataclass
|
|
31
33
|
class HttpClient:
|
|
32
34
|
base_url: str
|
|
35
|
+
api_key: str | None = None
|
|
33
36
|
timeout: float = field(default_factory=_resolve_timeout)
|
|
34
37
|
|
|
35
38
|
def _headers(self) -> Mapping[str, str]:
|
|
36
|
-
|
|
39
|
+
headers: dict[str, str] = {"Accept": "application/json"}
|
|
40
|
+
if self.api_key:
|
|
41
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
42
|
+
return headers
|
|
37
43
|
|
|
38
44
|
def get(
|
|
39
45
|
self, path: str, version_prefix: bool = False, **kwargs: Any
|
|
@@ -100,27 +106,26 @@ class HttpClient:
|
|
|
100
106
|
return response
|
|
101
107
|
|
|
102
108
|
|
|
103
|
-
def
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
logging.error(msg)
|
|
108
|
-
raise typer.Exit(code=1)
|
|
109
|
+
def resolve_base_url(
|
|
110
|
+
config_path: Path = DEFAULT_CONFIG_PATH,
|
|
111
|
+
) -> tuple[str, str]:
|
|
112
|
+
"""Return ``(base_url, source)`` with source in {"env", "config", "default"}.
|
|
109
113
|
|
|
114
|
+
Env > config file > local default. Mirrors the SDK's precedence but
|
|
115
|
+
reports the source so ``lumilake config`` can show where the value
|
|
116
|
+
came from.
|
|
117
|
+
"""
|
|
118
|
+
env_url = envs.get_lumilake_base_url()
|
|
119
|
+
if env_url:
|
|
120
|
+
return env_url, "env"
|
|
110
121
|
try:
|
|
111
|
-
|
|
112
|
-
except
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def client_from_config(config: LumilakeConfig | None = None) -> HttpClient:
|
|
123
|
-
"""Build an HttpClient from saved config. Exits if not logged in."""
|
|
124
|
-
if config is None:
|
|
125
|
-
config = _require_config()
|
|
126
|
-
return HttpClient(base_url=config.base_url)
|
|
122
|
+
cfg = LumilakeConfig.from_file(config_path)
|
|
123
|
+
except (ConfigNotFoundError, ConfigInvalidError):
|
|
124
|
+
return DEFAULT_LOCAL_BASE_URL, "default"
|
|
125
|
+
return cfg.base_url, "config"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def client_from_config(config_path: Path = DEFAULT_CONFIG_PATH) -> HttpClient:
|
|
129
|
+
"""Build an HttpClient by resolving base_url + api_key (env > file > default)."""
|
|
130
|
+
cfg = resolve_config(config_path=config_path)
|
|
131
|
+
return HttpClient(base_url=cfg.base_url, api_key=cfg.api_key)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lumilake-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Lumilake command-line interface
|
|
5
5
|
Author: Lumilake contributors
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -14,12 +14,12 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
14
14
|
Requires-Python: <3.13,>=3.12
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
License-File: LICENSE
|
|
17
|
-
Requires-Dist: lumilake-sdk==0.1.
|
|
17
|
+
Requires-Dist: lumilake-sdk==0.1.2
|
|
18
18
|
Requires-Dist: flowmesh-sdk==0.1.1
|
|
19
19
|
Requires-Dist: requests>=2.32.3
|
|
20
20
|
Requires-Dist: typer>=0.12.5
|
|
21
21
|
Provides-Extra: deploy
|
|
22
|
-
Requires-Dist: lumilake-deploy==0.1.
|
|
22
|
+
Requires-Dist: lumilake-deploy==0.1.2; extra == "deploy"
|
|
23
23
|
Dynamic: license-file
|
|
24
24
|
|
|
25
25
|
# `lumilake-cli`
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
import typer
|
|
5
|
-
|
|
6
|
-
from ..core import logging
|
|
7
|
-
from ..core.config import DEFAULT_CONFIG_PATH, LumilakeConfig, save_config
|
|
8
|
-
from ..core.http import HttpClient, HttpError, client_from_config
|
|
9
|
-
from ..core.typer import get_typer
|
|
10
|
-
|
|
11
|
-
app = get_typer()
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@app.command()
|
|
15
|
-
def login(
|
|
16
|
-
url: str = typer.Argument(
|
|
17
|
-
..., help="Lumilake server URL (e.g., http://localhost:9000)"
|
|
18
|
-
),
|
|
19
|
-
config_path: Path = typer.Option(
|
|
20
|
-
DEFAULT_CONFIG_PATH, "--config", help="Path to save configuration file"
|
|
21
|
-
),
|
|
22
|
-
) -> None:
|
|
23
|
-
"""Configure connection to a Lumilake server and save it locally."""
|
|
24
|
-
client = HttpClient(base_url=url)
|
|
25
|
-
try:
|
|
26
|
-
client.get("/healthz")
|
|
27
|
-
except HttpError as exc:
|
|
28
|
-
logging.error(str(exc))
|
|
29
|
-
raise typer.Exit(code=1)
|
|
30
|
-
save_config(LumilakeConfig(base_url=url), path=config_path)
|
|
31
|
-
logging.success(f"Login successful. Saved config to {config_path}")
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@app.command()
|
|
35
|
-
def logout(
|
|
36
|
-
config_path: Path = typer.Option(
|
|
37
|
-
DEFAULT_CONFIG_PATH, "--config", help="Path to configuration file to remove"
|
|
38
|
-
),
|
|
39
|
-
) -> None:
|
|
40
|
-
"""Delete saved configuration."""
|
|
41
|
-
config_path.unlink(missing_ok=True)
|
|
42
|
-
logging.success(f"Logout successful. Removed config {config_path}")
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
@app.command()
|
|
46
|
-
def config(
|
|
47
|
-
refresh: bool = typer.Option(
|
|
48
|
-
False, "--refresh", "-r", help="Reload and show the latest configuration"
|
|
49
|
-
),
|
|
50
|
-
config_path: Path = typer.Option(
|
|
51
|
-
DEFAULT_CONFIG_PATH, "--config", help="Path to configuration file"
|
|
52
|
-
),
|
|
53
|
-
) -> None:
|
|
54
|
-
"""Show the current Lumilake CLI configuration."""
|
|
55
|
-
from ..core.http import _require_config
|
|
56
|
-
|
|
57
|
-
cfg = _require_config(config_path)
|
|
58
|
-
if refresh:
|
|
59
|
-
client = HttpClient(base_url=cfg.base_url)
|
|
60
|
-
try:
|
|
61
|
-
client.get("/healthz")
|
|
62
|
-
except HttpError as exc:
|
|
63
|
-
logging.error(str(exc))
|
|
64
|
-
raise typer.Exit(code=1)
|
|
65
|
-
save_config(cfg, path=config_path)
|
|
66
|
-
logging.success(f"Configuration refreshed. Saved to {config_path}")
|
|
67
|
-
logging.log(json.dumps(cfg.to_mapping(), indent=2))
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
@app.command()
|
|
71
|
-
def info() -> None:
|
|
72
|
-
"""Query server health status."""
|
|
73
|
-
client = client_from_config()
|
|
74
|
-
try:
|
|
75
|
-
response = client.get("/healthz")
|
|
76
|
-
except HttpError as exc:
|
|
77
|
-
logging.error(str(exc))
|
|
78
|
-
raise typer.Exit(code=1)
|
|
79
|
-
logging.log(json.dumps(response.json(), indent=2))
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
@app.command()
|
|
83
|
-
def health() -> None:
|
|
84
|
-
"""Check if the Lumilake server is reachable and healthy."""
|
|
85
|
-
info()
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
"""Configuration handling for the Lumilake CLI."""
|
|
2
|
-
|
|
3
|
-
import tomllib
|
|
4
|
-
from dataclasses import asdict, dataclass
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Any
|
|
7
|
-
|
|
8
|
-
DEFAULT_CONFIG_DIR = Path.home() / ".lumilake"
|
|
9
|
-
DEFAULT_CONFIG_PATH = DEFAULT_CONFIG_DIR / "config.toml"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@dataclass
|
|
13
|
-
class LumilakeConfig:
|
|
14
|
-
base_url: str
|
|
15
|
-
|
|
16
|
-
@classmethod
|
|
17
|
-
def from_mapping(cls, data: dict[str, Any]) -> "LumilakeConfig":
|
|
18
|
-
if "base_url" not in data:
|
|
19
|
-
raise ValueError("Missing config key: base_url")
|
|
20
|
-
return cls(base_url=data["base_url"])
|
|
21
|
-
|
|
22
|
-
def to_mapping(self) -> dict[str, str]:
|
|
23
|
-
return asdict(self)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def load_config(path: Path = DEFAULT_CONFIG_PATH) -> LumilakeConfig:
|
|
27
|
-
if not path.exists():
|
|
28
|
-
raise FileNotFoundError(f"Config file not found at {path}")
|
|
29
|
-
content = tomllib.loads(path.read_text())
|
|
30
|
-
return LumilakeConfig.from_mapping(content)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def save_config(config: LumilakeConfig, path: Path = DEFAULT_CONFIG_PATH) -> None:
|
|
34
|
-
"""Persist config to disk, creating the directory if needed."""
|
|
35
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
-
data = config.to_mapping()
|
|
37
|
-
lines = [f'{key} = "{value}"' for key, value in data.items()]
|
|
38
|
-
path.write_text("\n".join(lines) + ("\n" if lines else ""))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.2}/src/lumilake_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|