lumilake-cli 0.1.0.dev1__tar.gz → 0.1.1__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.1}/PKG-INFO +3 -3
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/pyproject.toml +3 -3
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli/commands/__init__.py +1 -0
- lumilake_cli-0.1.1/src/lumilake_cli/commands/base.py +41 -0
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli/commands/deploy.py +167 -16
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli/commands/job.py +20 -3
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli/commands/trace.py +7 -1
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli/commands/worker.py +6 -2
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli/core/config.py +2 -1
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli/core/http.py +30 -24
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1/src/lumilake_cli.egg-info}/PKG-INFO +3 -3
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli.egg-info/SOURCES.txt +0 -1
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/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 → lumilake_cli-0.1.1}/LICENSE +0 -0
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/README.md +0 -0
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/setup.cfg +0 -0
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli/__init__.py +0 -0
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli/cli.py +0 -0
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli/core/__init__.py +0 -0
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli/core/logging.py +0 -0
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli/core/query.py +0 -0
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli/core/typer.py +0 -0
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli.egg-info/dependency_links.txt +0 -0
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli.egg-info/entry_points.txt +0 -0
- {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/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.1
|
|
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.1
|
|
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.1; 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.1"
|
|
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.1",
|
|
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.1"]
|
|
30
30
|
|
|
31
31
|
[project.urls]
|
|
32
32
|
Homepage = "https://github.com/mlsys-io/lumilake_OSS"
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
|
8
|
+
from ..core.http import HttpError, client_from_config, resolve_base_url
|
|
9
|
+
from ..core.typer import get_typer
|
|
10
|
+
|
|
11
|
+
app = get_typer()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command()
|
|
15
|
+
def config(
|
|
16
|
+
config_path: Path = typer.Option(
|
|
17
|
+
DEFAULT_CONFIG_PATH, "--config", help="Path to configuration file"
|
|
18
|
+
),
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Show the resolved Lumilake CLI configuration."""
|
|
21
|
+
base_url, source = resolve_base_url(config_path)
|
|
22
|
+
payload = {"base_url": base_url, "source": source, "config_path": str(config_path)}
|
|
23
|
+
logging.log(json.dumps(payload, indent=2))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@app.command()
|
|
27
|
+
def info() -> None:
|
|
28
|
+
"""Query server health status."""
|
|
29
|
+
client = client_from_config()
|
|
30
|
+
try:
|
|
31
|
+
response = client.get("/healthz")
|
|
32
|
+
except HttpError as exc:
|
|
33
|
+
logging.error(str(exc))
|
|
34
|
+
raise typer.Exit(code=1)
|
|
35
|
+
logging.log(json.dumps(response.json(), indent=2))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.command()
|
|
39
|
+
def health() -> None:
|
|
40
|
+
"""Check if the Lumilake server is reachable and healthy."""
|
|
41
|
+
info()
|
|
@@ -1,13 +1,16 @@
|
|
|
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
|
|
10
12
|
from lumilake_deploy import docker_client
|
|
13
|
+
from lumilake_deploy import purge as purge_mod
|
|
11
14
|
from lumilake_deploy import setup as setup_mod
|
|
12
15
|
from lumilake_deploy import stop as stop_mod
|
|
13
16
|
from lumilake_deploy import update_flowmesh as update_fm_mod
|
|
@@ -27,6 +30,7 @@ from lumilake_deploy.setup import (
|
|
|
27
30
|
)
|
|
28
31
|
|
|
29
32
|
from ..core import logging
|
|
33
|
+
from ..core.config import DEFAULT_CONFIG_PATH, LumilakeConfig, load_config, save_config
|
|
30
34
|
from ..core.typer import get_typer
|
|
31
35
|
|
|
32
36
|
app = get_typer(
|
|
@@ -83,9 +87,44 @@ def _run_setup(
|
|
|
83
87
|
raise typer.Exit(code=1) from exc
|
|
84
88
|
|
|
85
89
|
|
|
86
|
-
def
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
def _preview_write(target: Path, new_content: str) -> None:
|
|
91
|
+
if target.exists():
|
|
92
|
+
old = target.read_text().splitlines(keepends=True)
|
|
93
|
+
new = new_content.splitlines(keepends=True)
|
|
94
|
+
diff = "".join(
|
|
95
|
+
difflib.unified_diff(
|
|
96
|
+
old,
|
|
97
|
+
new,
|
|
98
|
+
fromfile=f"a/{target.name}",
|
|
99
|
+
tofile=f"b/{target.name}",
|
|
100
|
+
n=3,
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
if diff:
|
|
104
|
+
logging.info(f"Diff vs existing {target}:")
|
|
105
|
+
for line in diff.splitlines():
|
|
106
|
+
logging.info(line)
|
|
107
|
+
else:
|
|
108
|
+
logging.info(f"{target} already matches the bundled template.")
|
|
109
|
+
return
|
|
110
|
+
logging.info(f"Preview of {target} (first 20 lines):")
|
|
111
|
+
for line in new_content.splitlines()[:20]:
|
|
112
|
+
logging.info(line)
|
|
113
|
+
remaining = max(0, len(new_content.splitlines()) - 20)
|
|
114
|
+
if remaining:
|
|
115
|
+
logging.info(f"... and {remaining} more line(s).")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _confirm_overwrite(target: Path, *, force: bool, new_content: str) -> bool:
|
|
119
|
+
"""Return True when the target may be written, False to skip.
|
|
120
|
+
|
|
121
|
+
Shows a preview (or diff vs an existing file) before prompting on
|
|
122
|
+
overwrite. Brand-new writes pass through without prompting.
|
|
123
|
+
"""
|
|
124
|
+
if force:
|
|
125
|
+
return True
|
|
126
|
+
_preview_write(target, new_content)
|
|
127
|
+
if not target.exists():
|
|
89
128
|
return True
|
|
90
129
|
if typer.confirm(f"{target} already exists. Overwrite?", default=False):
|
|
91
130
|
return True
|
|
@@ -156,21 +195,23 @@ def init(
|
|
|
156
195
|
root = _project_dir(ctx)
|
|
157
196
|
template = env_example_path()
|
|
158
197
|
target = root / ENV_FILE_NAME
|
|
198
|
+
template_text = template.read_text()
|
|
159
199
|
wrote_env = False
|
|
160
|
-
if _confirm_overwrite(target, force=force):
|
|
161
|
-
target.write_text(
|
|
200
|
+
if _confirm_overwrite(target, force=force, new_content=template_text):
|
|
201
|
+
target.write_text(template_text)
|
|
162
202
|
logging.success(f"Wrote {target} from packaged {template.name}.")
|
|
163
203
|
wrote_env = True
|
|
164
204
|
|
|
165
205
|
if flowmesh:
|
|
166
206
|
fm_target = root / FLOWMESH_ENV_FILE_NAME
|
|
207
|
+
example = stack_env_example()
|
|
208
|
+
if not example.exists():
|
|
209
|
+
logging.error(f"FlowMesh env example not found: {example}")
|
|
210
|
+
raise typer.Exit(code=1)
|
|
211
|
+
fm_template_text = example.read_text()
|
|
167
212
|
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())
|
|
213
|
+
if _confirm_overwrite(fm_target, force=force, new_content=fm_template_text):
|
|
214
|
+
fm_target.write_text(fm_template_text)
|
|
174
215
|
logging.success(f"Wrote {fm_target} from {example.name}.")
|
|
175
216
|
_patch_flowmesh_env(fm_target)
|
|
176
217
|
wrote_flowmesh_env = True
|
|
@@ -240,6 +281,36 @@ def pull(ctx: typer.Context) -> None:
|
|
|
240
281
|
raise typer.Exit(code=1) from exc
|
|
241
282
|
|
|
242
283
|
|
|
284
|
+
def _write_cli_config(root: Path, config_path: Path | None = None) -> None:
|
|
285
|
+
"""Persist the local server URL so subsequent CLI / SDK calls find it.
|
|
286
|
+
|
|
287
|
+
Reads the deployment's ``.env`` to compute the host port the server
|
|
288
|
+
is listening on, then writes ``base_url`` to ``config_path``. If a
|
|
289
|
+
config already exists with a different ``base_url``, we log a hint
|
|
290
|
+
and overwrite. Errors are non-fatal — the stack is already running.
|
|
291
|
+
"""
|
|
292
|
+
if config_path is None:
|
|
293
|
+
config_path = DEFAULT_CONFIG_PATH
|
|
294
|
+
setup_mod.load_project_env(root)
|
|
295
|
+
port = envs.LUMILAKE_SERVER_PORT or 9000
|
|
296
|
+
base_url = f"http://127.0.0.1:{port}"
|
|
297
|
+
try:
|
|
298
|
+
existing = load_config(config_path)
|
|
299
|
+
except (FileNotFoundError, ValueError):
|
|
300
|
+
existing = None
|
|
301
|
+
if existing is not None and existing.base_url == base_url:
|
|
302
|
+
logging.info(f"CLI config already points at {base_url}.")
|
|
303
|
+
return
|
|
304
|
+
if existing is not None and existing.base_url != base_url:
|
|
305
|
+
logging.info(f"Updating {config_path}: {existing.base_url} -> {base_url}")
|
|
306
|
+
try:
|
|
307
|
+
save_config(LumilakeConfig(base_url=base_url), path=config_path)
|
|
308
|
+
except OSError as exc:
|
|
309
|
+
logging.warning(f"Could not save CLI config to {config_path}: {exc}")
|
|
310
|
+
return
|
|
311
|
+
logging.info(f"Saved CLI config to {config_path} (base_url={base_url}).")
|
|
312
|
+
|
|
313
|
+
|
|
243
314
|
@app.command()
|
|
244
315
|
def up(ctx: typer.Context) -> None:
|
|
245
316
|
"""Start the full Lumilake stack (Docker-backed).
|
|
@@ -247,7 +318,9 @@ def up(ctx: typer.Context) -> None:
|
|
|
247
318
|
The server image must already be present locally — run
|
|
248
319
|
``lumilake deploy pull`` or ``lumilake deploy build`` first.
|
|
249
320
|
"""
|
|
250
|
-
|
|
321
|
+
root = _project_dir(ctx)
|
|
322
|
+
_run_setup(root, background=True)
|
|
323
|
+
_write_cli_config(root)
|
|
251
324
|
|
|
252
325
|
|
|
253
326
|
@app.command()
|
|
@@ -266,7 +339,15 @@ def down(
|
|
|
266
339
|
),
|
|
267
340
|
),
|
|
268
341
|
) -> None:
|
|
269
|
-
"""Stop
|
|
342
|
+
"""Stop the stack but keep data volumes.
|
|
343
|
+
|
|
344
|
+
Safe to run between sessions: the archive bucket (job records, run
|
|
345
|
+
artifacts) and the compute postgres/minio volumes survive, so
|
|
346
|
+
``deploy up`` resumes against the same state. ``--wipe-archive`` also
|
|
347
|
+
removes the compute Postgres and FlowMesh runtime-state volumes, while
|
|
348
|
+
preserving MinIO corpus data. Use ``deploy reset`` (destructive) to
|
|
349
|
+
wipe every volume instead.
|
|
350
|
+
"""
|
|
270
351
|
try:
|
|
271
352
|
stop_mod.run_stop(
|
|
272
353
|
_project_dir(ctx),
|
|
@@ -288,6 +369,56 @@ def clean(ctx: typer.Context) -> None:
|
|
|
288
369
|
raise typer.Exit(code=1) from exc
|
|
289
370
|
|
|
290
371
|
|
|
372
|
+
@app.command()
|
|
373
|
+
def purge(
|
|
374
|
+
ctx: typer.Context,
|
|
375
|
+
image_tag: str = typer.Argument(
|
|
376
|
+
...,
|
|
377
|
+
help="Lumilake server image tag to purge from local Docker.",
|
|
378
|
+
),
|
|
379
|
+
dry_run: bool = typer.Option(
|
|
380
|
+
False,
|
|
381
|
+
"--dry-run",
|
|
382
|
+
help="List the image to be purged without deleting it.",
|
|
383
|
+
),
|
|
384
|
+
force: bool = typer.Option(
|
|
385
|
+
False,
|
|
386
|
+
"--force",
|
|
387
|
+
help="Force image removal even if Docker reports dependent containers.",
|
|
388
|
+
),
|
|
389
|
+
) -> None:
|
|
390
|
+
"""Purge one Lumilake server image tag from local Docker."""
|
|
391
|
+
try:
|
|
392
|
+
plan = purge_mod.build_server_image_purge_plan(
|
|
393
|
+
_project_dir(ctx),
|
|
394
|
+
image_tag=image_tag,
|
|
395
|
+
)
|
|
396
|
+
except DeployError as exc:
|
|
397
|
+
logging.error(str(exc))
|
|
398
|
+
raise typer.Exit(code=1) from exc
|
|
399
|
+
|
|
400
|
+
logging.info(f"Purging Lumilake server image with tag '{image_tag}'...")
|
|
401
|
+
if not plan.exists:
|
|
402
|
+
logging.warning(f"Image not found: {plan.image_ref}")
|
|
403
|
+
logging.info("No images to purge.")
|
|
404
|
+
return
|
|
405
|
+
|
|
406
|
+
if dry_run:
|
|
407
|
+
logging.info("Images to be purged:")
|
|
408
|
+
logging.info(f" {plan.image_ref}")
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
result = purge_mod.run_server_image_purge(plan, force=force)
|
|
413
|
+
except DeployError as exc:
|
|
414
|
+
logging.error(str(exc))
|
|
415
|
+
raise typer.Exit(code=1) from exc
|
|
416
|
+
if result.removed:
|
|
417
|
+
logging.success(f"Removed image: {result.image_ref}")
|
|
418
|
+
return
|
|
419
|
+
logging.warning(f"Image already absent: {result.image_ref}")
|
|
420
|
+
|
|
421
|
+
|
|
291
422
|
@app.command()
|
|
292
423
|
def status(ctx: typer.Context) -> None:
|
|
293
424
|
"""Show the running state of every Lumilake stack container."""
|
|
@@ -354,8 +485,28 @@ def restart(
|
|
|
354
485
|
|
|
355
486
|
|
|
356
487
|
@app.command()
|
|
357
|
-
def reset(
|
|
358
|
-
|
|
488
|
+
def reset(
|
|
489
|
+
ctx: typer.Context,
|
|
490
|
+
yes: bool = typer.Option(
|
|
491
|
+
False,
|
|
492
|
+
"--yes",
|
|
493
|
+
"-y",
|
|
494
|
+
help="Skip the destructive-action confirmation prompt.",
|
|
495
|
+
),
|
|
496
|
+
) -> None:
|
|
497
|
+
"""Wipe the archive and every volume, then start the stack fresh.
|
|
498
|
+
|
|
499
|
+
Destructive: removes the compute postgres / minio volumes (job
|
|
500
|
+
records, run artifacts, demo data) and the FlowMesh runtime state.
|
|
501
|
+
Use ``deploy down`` if you want to stop the stack but keep its data.
|
|
502
|
+
"""
|
|
503
|
+
if not yes and not typer.confirm(
|
|
504
|
+
"deploy reset deletes every Lumilake volume (archive + compute "
|
|
505
|
+
"data). Continue?",
|
|
506
|
+
default=False,
|
|
507
|
+
):
|
|
508
|
+
logging.info("Aborted.")
|
|
509
|
+
raise typer.Exit(code=0)
|
|
359
510
|
root = _project_dir(ctx)
|
|
360
511
|
try:
|
|
361
512
|
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())
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Configuration handling for the Lumilake CLI."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import tomllib
|
|
4
5
|
from dataclasses import asdict, dataclass
|
|
5
6
|
from pathlib import Path
|
|
@@ -34,5 +35,5 @@ def save_config(config: LumilakeConfig, path: Path = DEFAULT_CONFIG_PATH) -> Non
|
|
|
34
35
|
"""Persist config to disk, creating the directory if needed."""
|
|
35
36
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
36
37
|
data = config.to_mapping()
|
|
37
|
-
lines = [f
|
|
38
|
+
lines = [f"{key} = {json.dumps(value)}" for key, value in data.items()]
|
|
38
39
|
path.write_text("\n".join(lines) + ("\n" if lines else ""))
|
|
@@ -3,20 +3,20 @@
|
|
|
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
|
|
11
10
|
|
|
12
|
-
from . import
|
|
13
|
-
from .config import DEFAULT_CONFIG_PATH, LumilakeConfig, load_config
|
|
11
|
+
from .config import DEFAULT_CONFIG_PATH, load_config
|
|
14
12
|
|
|
15
13
|
API_VERSION_PREFIX = "/api/v1"
|
|
16
14
|
|
|
17
15
|
DEFAULT_TIMEOUT: float = 300.0
|
|
18
16
|
"""Default request timeout in seconds."""
|
|
19
17
|
|
|
18
|
+
DEFAULT_LOCAL_BASE_URL = "http://127.0.0.1:9000"
|
|
19
|
+
|
|
20
20
|
|
|
21
21
|
def _resolve_timeout() -> float:
|
|
22
22
|
"""Read timeout from ``LUMILAKE_TIMEOUT`` env var, falling back to default."""
|
|
@@ -100,27 +100,33 @@ class HttpClient:
|
|
|
100
100
|
return response
|
|
101
101
|
|
|
102
102
|
|
|
103
|
-
def
|
|
104
|
-
|
|
103
|
+
def resolve_base_url(
|
|
104
|
+
config_path: Path = DEFAULT_CONFIG_PATH,
|
|
105
|
+
) -> tuple[str, str]:
|
|
106
|
+
"""Pick a server URL with a clear precedence order.
|
|
105
107
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
raise typer.Exit(code=1)
|
|
108
|
+
Returns ``(base_url, source)`` where ``source`` is one of ``"env"``,
|
|
109
|
+
``"config"``, or ``"default"``. Resolution order:
|
|
109
110
|
|
|
111
|
+
1. ``LUMILAKE_BASE_URL`` environment variable.
|
|
112
|
+
2. ``~/.lumilake/config.toml`` written by ``lumilake deploy up``.
|
|
113
|
+
3. ``http://127.0.0.1:9000`` (the local deploy default).
|
|
114
|
+
"""
|
|
115
|
+
env_url = envs.get_lumilake_base_url()
|
|
116
|
+
if env_url:
|
|
117
|
+
return env_url, "env"
|
|
110
118
|
try:
|
|
111
|
-
|
|
119
|
+
cfg = load_config(config_path)
|
|
112
120
|
except FileNotFoundError:
|
|
113
|
-
|
|
114
|
-
except ValueError
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
config = _require_config()
|
|
126
|
-
return HttpClient(base_url=config.base_url)
|
|
121
|
+
return DEFAULT_LOCAL_BASE_URL, "default"
|
|
122
|
+
except ValueError:
|
|
123
|
+
return DEFAULT_LOCAL_BASE_URL, "default"
|
|
124
|
+
if cfg.base_url:
|
|
125
|
+
return cfg.base_url, "config"
|
|
126
|
+
return DEFAULT_LOCAL_BASE_URL, "default"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def client_from_config() -> HttpClient:
|
|
130
|
+
"""Build an HttpClient from the resolved base URL."""
|
|
131
|
+
base_url, _ = resolve_base_url()
|
|
132
|
+
return HttpClient(base_url=base_url)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lumilake-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
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.1
|
|
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.1; 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()
|
|
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.1}/src/lumilake_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|