magic-lab-observer-cli 0.1.1__tar.gz → 0.2.0__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.
- {magic_lab_observer_cli-0.1.1 → magic_lab_observer_cli-0.2.0}/.gitignore +7 -0
- {magic_lab_observer_cli-0.1.1 → magic_lab_observer_cli-0.2.0}/PKG-INFO +2 -2
- {magic_lab_observer_cli-0.1.1 → magic_lab_observer_cli-0.2.0}/cli/main.py +26 -17
- {magic_lab_observer_cli-0.1.1 → magic_lab_observer_cli-0.2.0}/pyproject.toml +1 -1
- {magic_lab_observer_cli-0.1.1 → magic_lab_observer_cli-0.2.0}/tests/test_cli.py +5 -5
- {magic_lab_observer_cli-0.1.1 → magic_lab_observer_cli-0.2.0}/README.md +0 -0
- {magic_lab_observer_cli-0.1.1 → magic_lab_observer_cli-0.2.0}/cli/__init__.py +0 -0
- {magic_lab_observer_cli-0.1.1 → magic_lab_observer_cli-0.2.0}/cli/db.py +0 -0
- {magic_lab_observer_cli-0.1.1 → magic_lab_observer_cli-0.2.0}/tests/__init__.py +0 -0
|
@@ -22,6 +22,9 @@ wheels/
|
|
|
22
22
|
|
|
23
23
|
# Virtual environments
|
|
24
24
|
.venv/
|
|
25
|
+
.venv.backup-*/
|
|
26
|
+
.venv.failed-*/
|
|
27
|
+
.runtime/
|
|
25
28
|
venv/
|
|
26
29
|
ENV/
|
|
27
30
|
|
|
@@ -65,4 +68,8 @@ podman-compose.override.yml
|
|
|
65
68
|
# OS
|
|
66
69
|
.DS_Store
|
|
67
70
|
Thumbs.db
|
|
71
|
+
backend/providers_snapshot.json
|
|
68
72
|
|
|
73
|
+
# SQLite WAL side files (the backend enables WAL per connection since 2026-10-17)
|
|
74
|
+
*.db-wal
|
|
75
|
+
*.db-shm
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: magic-lab-observer-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: CLI for LLM Observatory
|
|
5
5
|
Project-URL: Homepage, https://github.com/Magic-Lab-Studio/observer
|
|
6
6
|
Project-URL: Repository, https://github.com/Magic-Lab-Studio/observer.git
|
|
@@ -13,7 +13,7 @@ console = Console()
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
@click.group()
|
|
16
|
-
@click.version_option(version="0.
|
|
16
|
+
@click.version_option(version="0.2.0", prog_name="llm-observatory")
|
|
17
17
|
@click.option("--api-key", envvar="OBSERVATORY_API_KEY", default=None, help="API key for backend authentication")
|
|
18
18
|
@click.pass_context
|
|
19
19
|
def cli(ctx: click.Context, api_key: str | None) -> None:
|
|
@@ -26,7 +26,7 @@ def cli(ctx: click.Context, api_key: str | None) -> None:
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
@cli.command()
|
|
29
|
-
@click.option("--db", type=click.Path(exists=True), default="observatory.db", help="SQLite database path")
|
|
29
|
+
@click.option("--db", type=click.Path(exists=True), envvar="OBSERVATORY_DB", default="observatory.db", show_default=True, help="SQLite database path (env OBSERVATORY_DB)")
|
|
30
30
|
@click.option("--limit", default=20, help="Number of traces to show")
|
|
31
31
|
@click.option("--status", type=click.Choice(["ok", "error", "unset"]), help="Filter by status")
|
|
32
32
|
@click.option("--search", help="Search trace names")
|
|
@@ -61,7 +61,7 @@ def traces(db: str, limit: int, status: str | None, search: str | None) -> None:
|
|
|
61
61
|
|
|
62
62
|
@cli.command()
|
|
63
63
|
@click.argument("trace_id")
|
|
64
|
-
@click.option("--db", type=click.Path(exists=True), default="observatory.db", help="SQLite database path")
|
|
64
|
+
@click.option("--db", type=click.Path(exists=True), envvar="OBSERVATORY_DB", default="observatory.db", show_default=True, help="SQLite database path (env OBSERVATORY_DB)")
|
|
65
65
|
def inspect(trace_id: str, db: str) -> None:
|
|
66
66
|
"""Inspect a trace and its spans."""
|
|
67
67
|
from cli.db import get_trace, get_trace_spans
|
|
@@ -108,7 +108,7 @@ def inspect(trace_id: str, db: str) -> None:
|
|
|
108
108
|
|
|
109
109
|
@cli.command(name="import")
|
|
110
110
|
@click.argument("file", type=click.Path(exists=True))
|
|
111
|
-
@click.option("--db", type=click.Path(), default="observatory.db", help="SQLite database path")
|
|
111
|
+
@click.option("--db", type=click.Path(), envvar="OBSERVATORY_DB", default="observatory.db", show_default=True, help="SQLite database path (env OBSERVATORY_DB)")
|
|
112
112
|
@click.option("--dry-run", is_flag=True, help="Preview without writing")
|
|
113
113
|
def import_data(file: str, db: str, dry_run: bool) -> None:
|
|
114
114
|
"""Import traces from a JSON export file."""
|
|
@@ -178,7 +178,7 @@ def import_data(file: str, db: str, dry_run: bool) -> None:
|
|
|
178
178
|
|
|
179
179
|
|
|
180
180
|
@cli.command()
|
|
181
|
-
@click.option("--db", type=click.Path(exists=True), default="observatory.db", help="SQLite database path")
|
|
181
|
+
@click.option("--db", type=click.Path(exists=True), envvar="OBSERVATORY_DB", default="observatory.db", show_default=True, help="SQLite database path (env OBSERVATORY_DB)")
|
|
182
182
|
def stats(db: str) -> None:
|
|
183
183
|
"""Show summary statistics."""
|
|
184
184
|
from cli.db import get_stats
|
|
@@ -199,7 +199,7 @@ def stats(db: str) -> None:
|
|
|
199
199
|
|
|
200
200
|
@cli.command()
|
|
201
201
|
@click.argument("trace_id")
|
|
202
|
-
@click.option("--db", type=click.Path(exists=True), default="observatory.db", help="SQLite database path")
|
|
202
|
+
@click.option("--db", type=click.Path(exists=True), envvar="OBSERVATORY_DB", default="observatory.db", show_default=True, help="SQLite database path (env OBSERVATORY_DB)")
|
|
203
203
|
@click.option("--format", "output_format", type=click.Choice(["json", "table"]), default="table")
|
|
204
204
|
def spans(trace_id: str, db: str, output_format: str) -> None:
|
|
205
205
|
"""List spans for a trace."""
|
|
@@ -256,7 +256,7 @@ def spans(trace_id: str, db: str, output_format: str) -> None:
|
|
|
256
256
|
|
|
257
257
|
|
|
258
258
|
@cli.command()
|
|
259
|
-
@click.option("--host", default="
|
|
259
|
+
@click.option("--host", default="127.0.0.1", help="Host to bind to (loopback by default; see docs/security.md)")
|
|
260
260
|
@click.option("--port", default=8000, help="Port to bind to")
|
|
261
261
|
@click.option("--reload", is_flag=True, help="Enable auto-reload")
|
|
262
262
|
@click.option("--db", default="sqlite+aiosqlite:///./observatory.db", help="Database URL")
|
|
@@ -293,7 +293,7 @@ def serve(host: str, port: int, reload: bool, db: str) -> None:
|
|
|
293
293
|
help="Evaluator type",
|
|
294
294
|
)
|
|
295
295
|
@click.option("--criteria", help="Comma-separated criteria for evaluation")
|
|
296
|
-
@click.option("--endpoint", default="http://localhost:8000", help="API endpoint")
|
|
296
|
+
@click.option("--endpoint", envvar="OBSERVER_API_URL", default="http://localhost:8000", show_default=True, help="API endpoint (env OBSERVER_API_URL)")
|
|
297
297
|
@click.pass_context
|
|
298
298
|
def evaluate(
|
|
299
299
|
ctx: click.Context,
|
|
@@ -339,7 +339,7 @@ def evaluate(
|
|
|
339
339
|
|
|
340
340
|
|
|
341
341
|
@cli.command()
|
|
342
|
-
@click.option("--db", type=click.Path(exists=True), default="observatory.db", help="SQLite database path")
|
|
342
|
+
@click.option("--db", type=click.Path(exists=True), envvar="OBSERVATORY_DB", default="observatory.db", show_default=True, help="SQLite database path (env OBSERVATORY_DB)")
|
|
343
343
|
@click.option("--format", "output_format", type=click.Choice(["json"]), default="json", help="Export format")
|
|
344
344
|
@click.option("--output", "-o", type=click.Path(), help="Output file (default: stdout)")
|
|
345
345
|
@click.option("--status", type=click.Choice(["ok", "error", "unset"]), help="Filter by status")
|
|
@@ -374,7 +374,7 @@ def export(db: str, output_format: str, output: str | None, status: str | None,
|
|
|
374
374
|
|
|
375
375
|
export_data = {
|
|
376
376
|
"format": "llm-observatory-export",
|
|
377
|
-
"version": "0.
|
|
377
|
+
"version": "0.2.0",
|
|
378
378
|
"trace_count": len(traces_data),
|
|
379
379
|
"traces": traces_data,
|
|
380
380
|
}
|
|
@@ -389,7 +389,7 @@ def export(db: str, output_format: str, output: str | None, status: str | None,
|
|
|
389
389
|
|
|
390
390
|
|
|
391
391
|
@cli.command()
|
|
392
|
-
@click.option("--endpoint", default="http://localhost:8000", help="API endpoint")
|
|
392
|
+
@click.option("--endpoint", envvar="OBSERVER_API_URL", default="http://localhost:8000", show_default=True, help="API endpoint (env OBSERVER_API_URL)")
|
|
393
393
|
@click.pass_context
|
|
394
394
|
def status(ctx: click.Context, endpoint: str) -> None:
|
|
395
395
|
"""Check the status of the observatory server."""
|
|
@@ -401,13 +401,22 @@ def status(ctx: click.Context, endpoint: str) -> None:
|
|
|
401
401
|
headers["Authorization"] = f"Bearer {api_key}"
|
|
402
402
|
|
|
403
403
|
try:
|
|
404
|
-
response = httpx.get(f"{endpoint}/health", headers=headers)
|
|
405
|
-
|
|
406
|
-
|
|
404
|
+
response = httpx.get(f"{endpoint}/health", headers=headers, timeout=5.0)
|
|
405
|
+
except httpx.HTTPError as exc:
|
|
406
|
+
console.print(f"[red]Server is not responding[/red] at {endpoint}: {type(exc).__name__}")
|
|
407
|
+
console.print("Set --endpoint or OBSERVER_API_URL if the server listens elsewhere.")
|
|
408
|
+
raise SystemExit(1) from exc
|
|
409
|
+
try:
|
|
410
|
+
body = response.json()
|
|
411
|
+
except ValueError:
|
|
412
|
+
body = {}
|
|
413
|
+
if response.is_success:
|
|
407
414
|
console.print("[green]Server is healthy![/green]")
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
415
|
+
else:
|
|
416
|
+
console.print(f"[red]Server answered {response.status_code}[/red] {body}")
|
|
417
|
+
console.print(f"Endpoint: {endpoint} db: {body.get('db', '?')} version: {body.get('version', '?')}")
|
|
418
|
+
if not response.is_success:
|
|
419
|
+
raise SystemExit(1)
|
|
411
420
|
|
|
412
421
|
|
|
413
422
|
if __name__ == "__main__":
|
|
@@ -177,7 +177,7 @@ class TestCLIHelp:
|
|
|
177
177
|
def test_version(self, runner: CliRunner) -> None:
|
|
178
178
|
r = runner.invoke(cli, ["--version"])
|
|
179
179
|
assert r.exit_code == 0
|
|
180
|
-
assert "0.
|
|
180
|
+
assert "0.2.0" in r.output
|
|
181
181
|
|
|
182
182
|
def test_help(self, runner: CliRunner) -> None:
|
|
183
183
|
r = runner.invoke(cli, ["--help"])
|
|
@@ -339,7 +339,7 @@ class TestExportCommand:
|
|
|
339
339
|
assert r.exit_code == 0
|
|
340
340
|
data = json.loads(r.output)
|
|
341
341
|
assert data["format"] == "llm-observatory-export"
|
|
342
|
-
assert data["version"] == "0.
|
|
342
|
+
assert data["version"] == "0.2.0"
|
|
343
343
|
assert data["trace_count"] == 4
|
|
344
344
|
|
|
345
345
|
def test_export_to_file(self, runner: CliRunner, db_file: Path, tmp_path: Path) -> None:
|
|
@@ -408,19 +408,19 @@ class TestEvaluateCommand:
|
|
|
408
408
|
class TestStatusCommand:
|
|
409
409
|
def test_status_server_down(self, runner: CliRunner) -> None:
|
|
410
410
|
r = runner.invoke(cli, ["status", "--endpoint", "http://127.0.0.1:19999"])
|
|
411
|
-
assert r.exit_code ==
|
|
411
|
+
assert r.exit_code == 1 # a status check that cannot reach the server fails (audit 2026-10-17)
|
|
412
412
|
assert "not responding" in r.output
|
|
413
413
|
|
|
414
414
|
|
|
415
415
|
class TestAPIKeyOption:
|
|
416
416
|
def test_api_key_from_option(self, runner: CliRunner) -> None:
|
|
417
417
|
r = runner.invoke(cli, ["--api-key", "test-key", "status", "--endpoint", "http://127.0.0.1:19999"])
|
|
418
|
-
assert r.exit_code ==
|
|
418
|
+
assert r.exit_code == 1
|
|
419
419
|
assert "not responding" in r.output
|
|
420
420
|
|
|
421
421
|
def test_api_key_none_by_default(self, runner: CliRunner) -> None:
|
|
422
422
|
r = runner.invoke(cli, ["status", "--endpoint", "http://127.0.0.1:19999"])
|
|
423
|
-
assert r.exit_code ==
|
|
423
|
+
assert r.exit_code == 1
|
|
424
424
|
|
|
425
425
|
|
|
426
426
|
class TestImportCommand:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|