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.
Files changed (26) hide show
  1. {lumilake_cli-0.1.0.dev1/src/lumilake_cli.egg-info → lumilake_cli-0.1.1}/PKG-INFO +3 -3
  2. {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/pyproject.toml +3 -3
  3. {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli/commands/__init__.py +1 -0
  4. lumilake_cli-0.1.1/src/lumilake_cli/commands/base.py +41 -0
  5. {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli/commands/deploy.py +167 -16
  6. {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli/commands/job.py +20 -3
  7. {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli/commands/trace.py +7 -1
  8. {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli/commands/worker.py +6 -2
  9. {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli/core/config.py +2 -1
  10. {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli/core/http.py +30 -24
  11. {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1/src/lumilake_cli.egg-info}/PKG-INFO +3 -3
  12. {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli.egg-info/SOURCES.txt +0 -1
  13. {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli.egg-info/requires.txt +2 -2
  14. lumilake_cli-0.1.0.dev1/src/lumilake_cli/commands/base.py +0 -85
  15. {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/LICENSE +0 -0
  16. {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/README.md +0 -0
  17. {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/setup.cfg +0 -0
  18. {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli/__init__.py +0 -0
  19. {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli/cli.py +0 -0
  20. {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli/core/__init__.py +0 -0
  21. {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli/core/logging.py +0 -0
  22. {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli/core/query.py +0 -0
  23. {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli/core/typer.py +0 -0
  24. {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli.egg-info/dependency_links.txt +0 -0
  25. {lumilake_cli-0.1.0.dev1 → lumilake_cli-0.1.1}/src/lumilake_cli.egg-info/entry_points.txt +0 -0
  26. {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.0.dev1
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.0.dev1
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.0.dev1; extra == "deploy"
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.0.dev1"
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.0.dev1",
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.0.dev1"]
29
+ deploy = ["lumilake-deploy==0.1.1"]
30
30
 
31
31
  [project.urls]
32
32
  Homepage = "https://github.com/mlsys-io/lumilake_OSS"
@@ -51,6 +51,7 @@ else:
51
51
  "up",
52
52
  "down",
53
53
  "clean",
54
+ "purge",
54
55
  "reset",
55
56
  "status",
56
57
  "restart",
@@ -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 _confirm_overwrite(target: Path, *, force: bool) -> bool:
87
- """Return True when the target may be written, False to skip."""
88
- if not target.exists() or force:
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(template.read_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
- example = stack_env_example()
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
- _run_setup(_project_dir(ctx), background=True)
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 all services (keep data)."""
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(ctx: typer.Context) -> None:
358
- """Clean reset (stop + purge + up; deletes all data)."""
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, "--input", help="Input as Name=v1,v2,v3 (repeatable for different names)"
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, "--input-file", help="Input from file as Name=path.txt (repeatable)"
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, "--input-json", help="JSON file with full inputs object"
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
- logging.log(json.dumps(response.json(), indent=2))
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
- logging.log(json.dumps(response.json(), indent=2))
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'{key} = "{value}"' for key, value in data.items()]
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, NoReturn
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 logging
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 _require_config(path: Path = DEFAULT_CONFIG_PATH) -> LumilakeConfig:
104
- """Load config or exit with a clear message directing the user to login."""
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
- def _error(msg: str) -> NoReturn:
107
- logging.error(msg)
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
- config = load_config(path)
119
+ cfg = load_config(config_path)
112
120
  except FileNotFoundError:
113
- _error("Not logged in. Run `lumilake login <url>` first.")
114
- except ValueError as exc:
115
- _error(f"Invalid config file: {exc}. Please re-login.")
116
-
117
- if not config.base_url:
118
- _error("Missing base_url in config. Please re-login.")
119
- return config
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)
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.0.dev1
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.0.dev1
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.0.dev1; extra == "deploy"
22
+ Requires-Dist: lumilake-deploy==0.1.1; extra == "deploy"
23
23
  Dynamic: license-file
24
24
 
25
25
  # `lumilake-cli`
@@ -1,7 +1,6 @@
1
1
  LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
- ../../LICENSE
5
4
  src/lumilake_cli/__init__.py
6
5
  src/lumilake_cli/cli.py
7
6
  src/lumilake_cli.egg-info/PKG-INFO
@@ -1,7 +1,7 @@
1
- lumilake-sdk==0.1.0.dev1
1
+ lumilake-sdk==0.1.1
2
2
  flowmesh-sdk==0.1.1
3
3
  requests>=2.32.3
4
4
  typer>=0.12.5
5
5
 
6
6
  [deploy]
7
- lumilake-deploy==0.1.0.dev1
7
+ lumilake-deploy==0.1.1
@@ -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