nooa-cli 0.0.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nooa_cli/AGENTS.md +162 -0
- nooa_cli/__init__.py +70 -0
- nooa_cli/__main__.py +8 -0
- nooa_cli/_common.py +45 -0
- nooa_cli/commands/__init__.py +106 -0
- nooa_cli/commands/_otlp_helpers.py +122 -0
- nooa_cli/commands/_template.py +89 -0
- nooa_cli/commands/config.py +356 -0
- nooa_cli/commands/delete_traces.py +63 -0
- nooa_cli/commands/eval.py +68 -0
- nooa_cli/commands/import_harbor.py +512 -0
- nooa_cli/commands/import_traces.py +189 -0
- nooa_cli/commands/start_dev.py +145 -0
- nooa_cli/commands/traces.py +316 -0
- nooa_cli/completion.py +253 -0
- nooa_cli/tools/__init__.py +0 -0
- nooa_cli/tools/_tree_sitter_backend.py +351 -0
- nooa_cli/tools/pyp/__init__.py +71 -0
- nooa_cli/tools/pyp/errors.py +92 -0
- nooa_cli/tools/pyp/sources.py +458 -0
- nooa_cli/tools/pyp/stream.py +527 -0
- nooa_cli/tools/pyp/tests/__init__.py +0 -0
- nooa_cli/tools/pyp/tests/test_pyp.py +394 -0
- nooa_cli/tools/repo_tools.py +902 -0
- nooa_cli-0.0.7.dist-info/METADATA +52 -0
- nooa_cli-0.0.7.dist-info/RECORD +28 -0
- nooa_cli-0.0.7.dist-info/WHEEL +4 -0
- nooa_cli-0.0.7.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Start the development viewer.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
nooa start-dev # Start viewer on :5001
|
|
7
|
+
nooa start-dev --port 5002 # Custom port
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
NAME = "start-dev"
|
|
15
|
+
|
|
16
|
+
# Routes that are too noisy to log on every request.
|
|
17
|
+
_SUPPRESSED_PATHS = ("/v1/traces", "/api/trace", "/api/refresh")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _AccessLogFilter(logging.Filter):
|
|
21
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
22
|
+
msg = record.getMessage()
|
|
23
|
+
return not any(p in msg for p in _SUPPRESSED_PATHS)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _find_pid_on_port(port: int) -> str | None:
|
|
27
|
+
"""Return the PID of the process listening on *port*, or None."""
|
|
28
|
+
import shutil
|
|
29
|
+
import subprocess
|
|
30
|
+
|
|
31
|
+
# Try lsof first (macOS + most Linux)
|
|
32
|
+
if shutil.which("lsof"):
|
|
33
|
+
try:
|
|
34
|
+
out = subprocess.check_output(
|
|
35
|
+
["lsof", "-ti", f":{port}"],
|
|
36
|
+
text=True,
|
|
37
|
+
stderr=subprocess.DEVNULL,
|
|
38
|
+
).strip()
|
|
39
|
+
if out:
|
|
40
|
+
# May return multiple PIDs; take the first.
|
|
41
|
+
return out.splitlines()[0]
|
|
42
|
+
except subprocess.CalledProcessError:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
# Fallback: ss (Linux)
|
|
46
|
+
if shutil.which("ss"):
|
|
47
|
+
try:
|
|
48
|
+
out = subprocess.check_output(
|
|
49
|
+
["ss", "-tlnp", f"sport = :{port}"],
|
|
50
|
+
text=True,
|
|
51
|
+
stderr=subprocess.DEVNULL,
|
|
52
|
+
)
|
|
53
|
+
import re
|
|
54
|
+
|
|
55
|
+
m = re.search(r"pid=(\d+)", out)
|
|
56
|
+
if m:
|
|
57
|
+
return m.group(1)
|
|
58
|
+
except subprocess.CalledProcessError:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@click.command()
|
|
65
|
+
@click.option("--port", "-p", type=int, default=5001, help="Port number (default: 5001).")
|
|
66
|
+
@click.option("--host", "-h", default="0.0.0.0", help="Host to bind to (default: 0.0.0.0).")
|
|
67
|
+
@click.option(
|
|
68
|
+
"--db",
|
|
69
|
+
"db_path_opt",
|
|
70
|
+
type=click.Path(dir_okay=False),
|
|
71
|
+
default=None,
|
|
72
|
+
help="SQLite trace store path. Defaults to ~/.config/nooa/traces.db "
|
|
73
|
+
"(or $NOOA_TRACE_DB if set). Pass an explicit path to run a second viewer "
|
|
74
|
+
"side-by-side with the default one.",
|
|
75
|
+
)
|
|
76
|
+
def command(port: int, host: str, db_path_opt: str | None):
|
|
77
|
+
"""Start the unified trace + evaluation viewer."""
|
|
78
|
+
import os
|
|
79
|
+
from pathlib import Path
|
|
80
|
+
|
|
81
|
+
from nooa.paths import get_user_dir
|
|
82
|
+
|
|
83
|
+
# Resolve the DB path with --db winning, then $NOOA_TRACE_DB, then the
|
|
84
|
+
# legacy $NEMO_OO_TRACE_DB, then the user-dir default. Set both env vars
|
|
85
|
+
# unconditionally so the viewer module picks it up at import time.
|
|
86
|
+
if db_path_opt:
|
|
87
|
+
db_path = Path(db_path_opt).expanduser().resolve()
|
|
88
|
+
elif "NOOA_TRACE_DB" in os.environ:
|
|
89
|
+
db_path = Path(os.environ["NOOA_TRACE_DB"]).expanduser().resolve()
|
|
90
|
+
elif "NEMO_OO_TRACE_DB" in os.environ:
|
|
91
|
+
db_path = Path(os.environ["NEMO_OO_TRACE_DB"]).expanduser().resolve()
|
|
92
|
+
else:
|
|
93
|
+
db_path = get_user_dir("traces.db")
|
|
94
|
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
os.environ["NOOA_TRACE_DB"] = str(db_path)
|
|
96
|
+
os.environ["NEMO_OO_TRACE_DB"] = str(db_path)
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
from nooa.viewer.main import app
|
|
100
|
+
except ImportError:
|
|
101
|
+
click.secho(
|
|
102
|
+
"Error: viewer dependencies are not installed.\n"
|
|
103
|
+
'Install them with: uv add "nooa[viewer]"',
|
|
104
|
+
fg="red",
|
|
105
|
+
err=True,
|
|
106
|
+
)
|
|
107
|
+
raise SystemExit(1) from None
|
|
108
|
+
|
|
109
|
+
import copy
|
|
110
|
+
|
|
111
|
+
import uvicorn
|
|
112
|
+
|
|
113
|
+
logging.getLogger("uvicorn.access").addFilter(_AccessLogFilter())
|
|
114
|
+
|
|
115
|
+
log_config = copy.deepcopy(uvicorn.config.LOGGING_CONFIG)
|
|
116
|
+
log_config["loggers"]["nooa.viewer"] = {
|
|
117
|
+
"handlers": ["default"],
|
|
118
|
+
"level": "INFO",
|
|
119
|
+
"propagate": False,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
click.echo()
|
|
123
|
+
click.secho(" NVIDIA OO Agents Viewer", fg="cyan", bold=True)
|
|
124
|
+
click.echo(f" URL: http://localhost:{port}")
|
|
125
|
+
click.echo(f" DB: {db_path}")
|
|
126
|
+
click.echo()
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
uvicorn.run(app, host=host, port=port, log_config=log_config)
|
|
130
|
+
except SystemExit:
|
|
131
|
+
raise
|
|
132
|
+
except OSError as exc:
|
|
133
|
+
import errno as _errno
|
|
134
|
+
|
|
135
|
+
if exc.errno == _errno.EADDRINUSE:
|
|
136
|
+
pid = _find_pid_on_port(port)
|
|
137
|
+
pid_suffix = f" by process {pid}" if pid else ""
|
|
138
|
+
click.secho(
|
|
139
|
+
f"[Errno 48] error while attempting to bind on address "
|
|
140
|
+
f"('{host}', {port}): address already in use{pid_suffix}",
|
|
141
|
+
fg="red",
|
|
142
|
+
err=True,
|
|
143
|
+
)
|
|
144
|
+
raise SystemExit(1) from None
|
|
145
|
+
raise
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Manage trace and evaluation files.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
nooa traces delete # Delete, with confirmation
|
|
7
|
+
nooa traces delete -n # Dry run
|
|
8
|
+
nooa traces delete --older-than 7 # Only files >7 days old
|
|
9
|
+
nooa traces list # List trace directories
|
|
10
|
+
nooa traces stats # Show trace file statistics
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
from datetime import datetime, timedelta
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import click
|
|
20
|
+
|
|
21
|
+
from nooa_cli._common import find_project_root, format_size
|
|
22
|
+
|
|
23
|
+
SESSION_ID_RE = re.compile(r'"session\.id"\s*:\s*"[^"]+"')
|
|
24
|
+
|
|
25
|
+
# Directories to skip when scanning for trace files
|
|
26
|
+
EXCLUDE_DIRS = {
|
|
27
|
+
".venv",
|
|
28
|
+
"venv",
|
|
29
|
+
"node_modules",
|
|
30
|
+
".git",
|
|
31
|
+
"__pycache__",
|
|
32
|
+
".mypy_cache",
|
|
33
|
+
".ruff_cache",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# File discovery helpers
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _discover_trace_dirs(root: Path | None = None) -> list[Path]:
|
|
43
|
+
"""Discover configured file-backed trace directories without a recursive scan.
|
|
44
|
+
|
|
45
|
+
The live trace viewer stores spans in SQLite via OTLP/Journal ingestion; JSONL
|
|
46
|
+
trace files are an explicit fallback/export path. Keep this helper bounded to
|
|
47
|
+
locations NeMo OO itself can write: the project trace dir used by TUI
|
|
48
|
+
``--trace``, the legacy ``./traces`` convention at the project root, and an
|
|
49
|
+
explicit ``TRACE_DIR`` override when present.
|
|
50
|
+
"""
|
|
51
|
+
project_root = root.resolve() if root is not None else find_project_root().resolve()
|
|
52
|
+
candidates = [
|
|
53
|
+
_project_trace_dir(project_root),
|
|
54
|
+
project_root / "traces",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
env_trace_dir = os.getenv("TRACE_DIR")
|
|
58
|
+
if env_trace_dir:
|
|
59
|
+
env_path = Path(env_trace_dir).expanduser()
|
|
60
|
+
candidates.append(env_path if env_path.is_absolute() else Path.cwd() / env_path)
|
|
61
|
+
|
|
62
|
+
return sorted({path.resolve() for path in candidates if path.is_dir()})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _project_trace_dir(root: Path | None = None) -> Path:
|
|
66
|
+
"""Return the project-local JSONL trace directory used by TUI ``--trace``."""
|
|
67
|
+
project_root = root.resolve() if root is not None else find_project_root().resolve()
|
|
68
|
+
return project_root / ".nooa" / "traces"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _find_trace_files(root: Path) -> list[Path]:
|
|
72
|
+
"""Find all trace JSONL files, excluding annotation and eval files."""
|
|
73
|
+
trace_files: list[Path] = []
|
|
74
|
+
_collect_files(root, "*.jsonl", trace_files, exclude_non_trace=True)
|
|
75
|
+
return sorted(trace_files)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _find_eval_files(root: Path) -> list[Path]:
|
|
79
|
+
"""Find all .noo-eval.jsonl and .006eval.json files."""
|
|
80
|
+
eval_files: list[Path] = []
|
|
81
|
+
_collect_files(root, "*.noo-eval.jsonl", eval_files)
|
|
82
|
+
_collect_files(root, "*.006eval.json", eval_files)
|
|
83
|
+
return sorted(eval_files)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _collect_files(root: Path, pattern: str, out: list[Path], *, exclude_non_trace: bool = False):
|
|
87
|
+
"""Walk directory tree collecting files matching pattern, skipping excluded dirs."""
|
|
88
|
+
try:
|
|
89
|
+
for f in root.glob(pattern):
|
|
90
|
+
if exclude_non_trace and (
|
|
91
|
+
f.name.endswith(".annotations.jsonl")
|
|
92
|
+
or f.name.endswith(".noo-eval.jsonl")
|
|
93
|
+
or f.name.endswith(".006eval.json")
|
|
94
|
+
or f.name.endswith(".006summary.json")
|
|
95
|
+
):
|
|
96
|
+
continue
|
|
97
|
+
out.append(f)
|
|
98
|
+
for entry in root.iterdir():
|
|
99
|
+
if entry.is_dir() and entry.name not in EXCLUDE_DIRS:
|
|
100
|
+
_collect_files(entry, pattern, out, exclude_non_trace=exclude_non_trace)
|
|
101
|
+
except PermissionError:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _has_session_id(file_path: Path) -> bool:
|
|
106
|
+
"""Check if a trace file has session.id in the first 4KB."""
|
|
107
|
+
try:
|
|
108
|
+
with open(file_path, encoding="utf-8") as f:
|
|
109
|
+
chunk = f.read(4096)
|
|
110
|
+
except OSError:
|
|
111
|
+
return True # Conservatively keep
|
|
112
|
+
return bool(SESSION_ID_RE.search(chunk))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
# CLI group + subcommands
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@click.group()
|
|
121
|
+
def command():
|
|
122
|
+
"""Manage trace and evaluation files."""
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@command.command()
|
|
126
|
+
@click.argument("directory", type=click.Path(exists=True), default=".")
|
|
127
|
+
@click.option("--dry-run", "-n", is_flag=True, help="Show what would be deleted without deleting.")
|
|
128
|
+
@click.option("--older-than", type=int, metavar="DAYS", help="Only files older than N days.")
|
|
129
|
+
@click.option("--all", "delete_all", is_flag=True, help="Delete ALL traces (ignore session.id).")
|
|
130
|
+
@click.option("--evals", is_flag=True, help="Also delete eval files (.noo-eval.jsonl).")
|
|
131
|
+
@click.option("--evals-only", is_flag=True, help="Only delete eval files (skip traces).")
|
|
132
|
+
@click.option("-y", "--yes", is_flag=True, help="Skip confirmation prompt.")
|
|
133
|
+
def delete(
|
|
134
|
+
directory: str,
|
|
135
|
+
dry_run: bool,
|
|
136
|
+
older_than: int | None,
|
|
137
|
+
delete_all: bool,
|
|
138
|
+
evals: bool,
|
|
139
|
+
evals_only: bool,
|
|
140
|
+
yes: bool,
|
|
141
|
+
):
|
|
142
|
+
"""Delete trace and evaluation files.
|
|
143
|
+
|
|
144
|
+
DIRECTORY is the root directory to scan (default: current directory).
|
|
145
|
+
|
|
146
|
+
\b
|
|
147
|
+
By default, traces with a session.id are preserved (they belong to a
|
|
148
|
+
tracked session). Use --all to override this.
|
|
149
|
+
|
|
150
|
+
\b
|
|
151
|
+
Examples:
|
|
152
|
+
nooa traces delete # Delete, with confirmation
|
|
153
|
+
nooa traces delete -n # Dry run (show what would go)
|
|
154
|
+
nooa traces delete --older-than 7 # Only files >7 days old
|
|
155
|
+
nooa traces delete --all -y # Delete everything, no prompt
|
|
156
|
+
nooa traces delete --evals # Include eval files
|
|
157
|
+
"""
|
|
158
|
+
root = Path(directory).resolve()
|
|
159
|
+
click.echo(f"Scanning: {root}")
|
|
160
|
+
|
|
161
|
+
all_files: list[tuple[str, Path]] = []
|
|
162
|
+
|
|
163
|
+
if not evals_only:
|
|
164
|
+
trace_files = _find_trace_files(root)
|
|
165
|
+
click.echo(f"Found {len(trace_files)} trace files")
|
|
166
|
+
all_files.extend(("trace", f) for f in trace_files)
|
|
167
|
+
|
|
168
|
+
if evals or evals_only:
|
|
169
|
+
eval_files = _find_eval_files(root)
|
|
170
|
+
click.echo(f"Found {len(eval_files)} eval files")
|
|
171
|
+
all_files.extend(("eval", f) for f in eval_files)
|
|
172
|
+
|
|
173
|
+
if not all_files:
|
|
174
|
+
click.echo("\nNo files found.")
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
if older_than:
|
|
178
|
+
cutoff = datetime.now() - timedelta(days=older_than)
|
|
179
|
+
cutoff_ts = cutoff.timestamp()
|
|
180
|
+
all_files = [(t, f) for t, f in all_files if f.stat().st_mtime < cutoff_ts]
|
|
181
|
+
click.echo(f"Filtered to {len(all_files)} files older than {older_than} days")
|
|
182
|
+
|
|
183
|
+
to_delete = []
|
|
184
|
+
for file_type, f in all_files:
|
|
185
|
+
if file_type == "eval":
|
|
186
|
+
to_delete.append((file_type, f))
|
|
187
|
+
elif delete_all or not _has_session_id(f):
|
|
188
|
+
to_delete.append((file_type, f))
|
|
189
|
+
|
|
190
|
+
if not to_delete:
|
|
191
|
+
click.echo("\nNo files to delete.")
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
total_size = sum(f.stat().st_size for _, f in to_delete)
|
|
195
|
+
trace_count = sum(1 for t, _ in to_delete if t == "trace")
|
|
196
|
+
eval_count = sum(1 for t, _ in to_delete if t == "eval")
|
|
197
|
+
|
|
198
|
+
click.echo(f"\nFiles to delete: {len(to_delete)} ({format_size(total_size)})")
|
|
199
|
+
if trace_count:
|
|
200
|
+
click.echo(f" - {trace_count} trace files")
|
|
201
|
+
if eval_count:
|
|
202
|
+
click.echo(f" - {eval_count} eval files")
|
|
203
|
+
|
|
204
|
+
if dry_run:
|
|
205
|
+
click.secho("\n[DRY RUN] Files that would be deleted:", fg="yellow")
|
|
206
|
+
for file_type, f in to_delete[:20]:
|
|
207
|
+
click.echo(f" [{file_type}] {f}")
|
|
208
|
+
if len(to_delete) > 20:
|
|
209
|
+
click.echo(f" ... and {len(to_delete) - 20} more")
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
if not yes:
|
|
213
|
+
click.confirm(
|
|
214
|
+
f"\nDelete {len(to_delete)} files ({format_size(total_size)})?",
|
|
215
|
+
abort=True,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
click.echo("\nDeleting files...")
|
|
219
|
+
deleted = 0
|
|
220
|
+
for file_type, f in to_delete:
|
|
221
|
+
try:
|
|
222
|
+
f.unlink()
|
|
223
|
+
deleted += 1
|
|
224
|
+
if not sys.stdout.isatty() or deleted <= 20 or deleted == len(to_delete):
|
|
225
|
+
click.echo(f" Deleted [{file_type}]: {f}")
|
|
226
|
+
except OSError as e:
|
|
227
|
+
click.secho(f" Error deleting {f}: {e}", fg="red")
|
|
228
|
+
|
|
229
|
+
click.secho(f"\nDeleted {deleted} files ({format_size(total_size)} freed).", fg="green")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@command.command("list")
|
|
233
|
+
@click.option(
|
|
234
|
+
"--root",
|
|
235
|
+
type=click.Path(exists=True),
|
|
236
|
+
default=None,
|
|
237
|
+
help="Project root to scan (auto-detected by default).",
|
|
238
|
+
)
|
|
239
|
+
def list_dirs(root: str | None):
|
|
240
|
+
"""List discovered trace directories.
|
|
241
|
+
|
|
242
|
+
\b
|
|
243
|
+
Examples:
|
|
244
|
+
nooa traces list
|
|
245
|
+
nooa traces list --root /path/to/project
|
|
246
|
+
"""
|
|
247
|
+
project_root = (Path(root) if root else find_project_root()).resolve()
|
|
248
|
+
dirs = _discover_trace_dirs(project_root)
|
|
249
|
+
|
|
250
|
+
if not dirs:
|
|
251
|
+
click.echo("No trace directories found.")
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
click.echo(f"Trace directories (from {project_root}):\n")
|
|
255
|
+
for d in dirs:
|
|
256
|
+
trace_count = len(
|
|
257
|
+
[
|
|
258
|
+
f
|
|
259
|
+
for f in d.glob("*.jsonl")
|
|
260
|
+
if not f.name.endswith(".annotations.jsonl")
|
|
261
|
+
and not f.name.endswith(".noo-eval.jsonl")
|
|
262
|
+
]
|
|
263
|
+
)
|
|
264
|
+
total_size = sum(
|
|
265
|
+
f.stat().st_size
|
|
266
|
+
for f in d.glob("*.jsonl")
|
|
267
|
+
if not f.name.endswith(".annotations.jsonl") and not f.name.endswith(".noo-eval.jsonl")
|
|
268
|
+
)
|
|
269
|
+
rel = d.relative_to(project_root) if d.is_relative_to(project_root) else d
|
|
270
|
+
|
|
271
|
+
count_str = f"{trace_count} traces" if trace_count else "empty"
|
|
272
|
+
size_str = f", {format_size(total_size)}" if total_size else ""
|
|
273
|
+
|
|
274
|
+
click.echo(f" {rel}/ ({count_str}{size_str})")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@command.command()
|
|
278
|
+
@click.argument("directory", type=click.Path(exists=True), default=".")
|
|
279
|
+
def stats(directory: str):
|
|
280
|
+
"""Show statistics about trace files.
|
|
281
|
+
|
|
282
|
+
\b
|
|
283
|
+
Examples:
|
|
284
|
+
nooa traces stats
|
|
285
|
+
nooa traces stats ./traces
|
|
286
|
+
"""
|
|
287
|
+
root = Path(directory).resolve()
|
|
288
|
+
trace_files = _find_trace_files(root)
|
|
289
|
+
eval_files = _find_eval_files(root)
|
|
290
|
+
|
|
291
|
+
if not trace_files and not eval_files:
|
|
292
|
+
click.echo(f"No trace or eval files found in {root}")
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
click.echo(f"Trace statistics for: {root}\n")
|
|
296
|
+
|
|
297
|
+
if trace_files:
|
|
298
|
+
total_size = sum(f.stat().st_size for f in trace_files)
|
|
299
|
+
with_session = sum(1 for f in trace_files if _has_session_id(f))
|
|
300
|
+
without_session = len(trace_files) - with_session
|
|
301
|
+
|
|
302
|
+
now = datetime.now().timestamp()
|
|
303
|
+
ages = [(now - f.stat().st_mtime) / 86400 for f in trace_files]
|
|
304
|
+
|
|
305
|
+
click.echo(f" Trace files: {len(trace_files)}")
|
|
306
|
+
click.echo(f" Total size: {format_size(total_size)}")
|
|
307
|
+
click.echo(f" With session.id: {with_session}")
|
|
308
|
+
click.echo(f" Without session: {without_session}")
|
|
309
|
+
if ages:
|
|
310
|
+
click.echo(f" Oldest: {max(ages):.0f} days")
|
|
311
|
+
click.echo(f" Newest: {min(ages):.1f} days")
|
|
312
|
+
|
|
313
|
+
if eval_files:
|
|
314
|
+
total_size = sum(f.stat().st_size for f in eval_files)
|
|
315
|
+
click.echo(f"\n Eval files: {len(eval_files)}")
|
|
316
|
+
click.echo(f" Eval size: {format_size(total_size)}")
|