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
nooa_cli/AGENTS.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# nooa CLI — Adding New Commands
|
|
2
|
+
|
|
3
|
+
## Quick Start
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
cd src/nooa_cli/commands/
|
|
7
|
+
cp _template.py mycommand.py # copy the template
|
|
8
|
+
# edit mycommand.py # add your logic
|
|
9
|
+
nooa mycommand # it just works
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
That's it. No registration, no config files, no editing other files.
|
|
13
|
+
|
|
14
|
+
## How It Works
|
|
15
|
+
|
|
16
|
+
The CLI auto-discovers every `.py` file in `src/nooa_cli/commands/` at startup.
|
|
17
|
+
Files starting with `_` are ignored (private helpers, templates).
|
|
18
|
+
|
|
19
|
+
### The Contract
|
|
20
|
+
|
|
21
|
+
Your command module must export **one thing**: a module-level variable named `command`
|
|
22
|
+
that is a Click command or group.
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
# commands/mycommand.py
|
|
26
|
+
import click
|
|
27
|
+
|
|
28
|
+
@click.command()
|
|
29
|
+
@click.argument("name")
|
|
30
|
+
def command(name: str):
|
|
31
|
+
"""One-line description shown in `nooa --help`."""
|
|
32
|
+
click.echo(f"Hello, {name}!")
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The filename becomes the subcommand name: `mycommand.py` → `nooa mycommand`.
|
|
36
|
+
|
|
37
|
+
### Override the Name
|
|
38
|
+
|
|
39
|
+
Add `NAME` at module level if the filename doesn't match what you want:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
NAME = "my-command" # nooa my-command (instead of nooa mycommand)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Patterns
|
|
46
|
+
|
|
47
|
+
### Simple Command (leaf, no subcommands)
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import click
|
|
51
|
+
|
|
52
|
+
@click.command()
|
|
53
|
+
@click.argument("target", type=click.Path(exists=True))
|
|
54
|
+
@click.option("--verbose", "-v", is_flag=True)
|
|
55
|
+
def command(target: str, verbose: bool):
|
|
56
|
+
"""Do something to TARGET."""
|
|
57
|
+
click.echo(f"Processing {target}")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Group with Subcommands
|
|
61
|
+
|
|
62
|
+
For `nooa things list` / `nooa things create`:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import click
|
|
66
|
+
|
|
67
|
+
@click.group()
|
|
68
|
+
def command():
|
|
69
|
+
"""Manage things."""
|
|
70
|
+
|
|
71
|
+
@command.command()
|
|
72
|
+
def list():
|
|
73
|
+
"""List all things."""
|
|
74
|
+
click.echo("thing-1\nthing-2")
|
|
75
|
+
|
|
76
|
+
@command.command()
|
|
77
|
+
@click.argument("name")
|
|
78
|
+
def create(name: str):
|
|
79
|
+
"""Create a new thing."""
|
|
80
|
+
click.echo(f"Created: {name}")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Passthrough to Another CLI
|
|
84
|
+
|
|
85
|
+
For wrapping an existing tool without duplicating its options:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
import click
|
|
89
|
+
|
|
90
|
+
@click.command(
|
|
91
|
+
add_help_option=False,
|
|
92
|
+
context_settings=dict(
|
|
93
|
+
ignore_unknown_options=True,
|
|
94
|
+
allow_extra_args=True,
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
@click.argument("args", nargs=-1, type=click.UNPROCESSED)
|
|
98
|
+
def command(args: tuple[str, ...]):
|
|
99
|
+
"""Run some-other-tool (all args forwarded)."""
|
|
100
|
+
import subprocess, sys
|
|
101
|
+
subprocess.run([sys.executable, "-m", "some_other_tool", *args])
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Shared Utilities
|
|
105
|
+
|
|
106
|
+
Common helpers live in `src/nooa_cli/_common.py`:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from nooa_cli._common import find_project_root, format_size
|
|
110
|
+
|
|
111
|
+
root = find_project_root() # Path to project root (where pyproject.toml is)
|
|
112
|
+
format_size(1_500_000) # "1.4 MB"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
For API keys / env vars, use `nooa.secrets.load_secrets_into_env()`
|
|
116
|
+
(reads the layered `secrets.yaml`); for any layered config file use
|
|
117
|
+
`nooa.layered_config.load_layered_yaml(filename, env_var)`.
|
|
118
|
+
|
|
119
|
+
## Performance Rule
|
|
120
|
+
|
|
121
|
+
**Keep imports lazy.** The CLI starts in ~0.3s because it doesn't import the
|
|
122
|
+
heavy `nooa` framework at module level. Only import heavy dependencies
|
|
123
|
+
inside your command handler function:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
@click.command()
|
|
127
|
+
def command():
|
|
128
|
+
"""Do something that needs the framework."""
|
|
129
|
+
# These imports happen only when the command actually runs,
|
|
130
|
+
# not when `nooa --help` loads the CLI.
|
|
131
|
+
from nooa import Agent
|
|
132
|
+
import pandas as pd
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## File Layout
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
src/nooa_cli/
|
|
139
|
+
├── __init__.py # Root CLI group + auto-discovery wiring
|
|
140
|
+
├── __main__.py # python -m nooa_cli
|
|
141
|
+
├── _common.py # Shared utilities
|
|
142
|
+
├── completion.py # Shell completion (bash/zsh/fish)
|
|
143
|
+
├── AGENTS.md # ← You are here
|
|
144
|
+
└── commands/
|
|
145
|
+
├── __init__.py # Auto-discovery engine
|
|
146
|
+
├── _template.py # Copy-paste starter for new commands
|
|
147
|
+
├── eval.py # nooa eval ...
|
|
148
|
+
├── traces.py # nooa traces cleanup/list/stats
|
|
149
|
+
└── start_dev.py # nooa start-dev
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Shell Completion
|
|
153
|
+
|
|
154
|
+
New commands get tab-completion automatically. To enable it:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
nooa completion install # auto-detects your shell
|
|
158
|
+
|
|
159
|
+
# Or manually:
|
|
160
|
+
eval "$(_NEMO_COMPLETE=zsh_source nemo)" # zsh
|
|
161
|
+
eval "$(_NEMO_COMPLETE=bash_source nemo)" # bash
|
|
162
|
+
```
|
nooa_cli/__init__.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""NVIDIA OO Agents CLI — extensible command-line toolkit.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
nooa eval --config config.yaml # Run an eval-pipeline job
|
|
7
|
+
nooa start-dev # Start the viewer
|
|
8
|
+
nooa traces delete # Delete traces
|
|
9
|
+
nooa completion install # Set up shell completions
|
|
10
|
+
|
|
11
|
+
Adding new commands:
|
|
12
|
+
Drop a .py file in nooa_cli/commands/ — see commands/_template.py
|
|
13
|
+
|
|
14
|
+
Shell completion:
|
|
15
|
+
eval "$(_NOOA_COMPLETE=bash_source nooa)" # bash
|
|
16
|
+
eval "$(_NOOA_COMPLETE=zsh_source nooa)" # zsh
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import click
|
|
20
|
+
|
|
21
|
+
from .commands import discover_commands
|
|
22
|
+
from .completion import completion
|
|
23
|
+
|
|
24
|
+
CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# `completion` just emits a shell script and doesn't need the ~1.5s core
|
|
28
|
+
# import cost or secrets preload.
|
|
29
|
+
_SKIP_SECRETS_PRELOAD = {"completion"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@click.group(context_settings=CONTEXT_SETTINGS)
|
|
33
|
+
@click.version_option(package_name="nooa-cli")
|
|
34
|
+
@click.pass_context
|
|
35
|
+
def oo(ctx):
|
|
36
|
+
"""OO Agents — agent toolkit.
|
|
37
|
+
|
|
38
|
+
Extensible CLI for running agents, evaluations, and trace management.
|
|
39
|
+
Add new commands by dropping a .py file in nooa_cli/commands/.
|
|
40
|
+
"""
|
|
41
|
+
if ctx.invoked_subcommand in _SKIP_SECRETS_PRELOAD:
|
|
42
|
+
return
|
|
43
|
+
# Load secrets.yaml into the process env before any subcommand runs so
|
|
44
|
+
# all commands (config show, eval, …) see the same API keys.
|
|
45
|
+
# Non-clobbering (shell env wins) and best-effort: a broken or missing
|
|
46
|
+
# secrets file must never block the CLI — but log the failure at debug
|
|
47
|
+
# so it's recoverable rather than silently swallowed.
|
|
48
|
+
try:
|
|
49
|
+
from nooa.secrets import load_secrets_into_env
|
|
50
|
+
|
|
51
|
+
load_secrets_into_env()
|
|
52
|
+
except Exception:
|
|
53
|
+
import logging
|
|
54
|
+
|
|
55
|
+
logging.getLogger(__name__).debug(
|
|
56
|
+
"Failed to preload secrets.yaml into the environment", exc_info=True
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# -- Auto-discover and register all commands from commands/ -----------------
|
|
61
|
+
for _name, _cmd in discover_commands():
|
|
62
|
+
oo.add_command(_cmd, name=_name)
|
|
63
|
+
|
|
64
|
+
# -- Built-in infrastructure commands (not in commands/ because they're meta) -
|
|
65
|
+
oo.add_command(completion)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def main():
|
|
69
|
+
"""Entry point for console_scripts: nooa <subcommand>."""
|
|
70
|
+
oo()
|
nooa_cli/__main__.py
ADDED
nooa_cli/_common.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Shared utilities for nooa CLI commands.
|
|
4
|
+
|
|
5
|
+
Import from here instead of duplicating helpers across command modules:
|
|
6
|
+
|
|
7
|
+
from nooa_cli._common import find_project_root, format_size
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def find_project_root() -> Path:
|
|
14
|
+
"""Find the user's project root by walking up from the current directory.
|
|
15
|
+
|
|
16
|
+
Starts at :func:`Path.cwd` — the directory the CLI was invoked from — and
|
|
17
|
+
walks upward, returning the first ancestor (including the cwd itself) that
|
|
18
|
+
contains a ``pyproject.toml``. Falls back to the current working directory
|
|
19
|
+
if none is found.
|
|
20
|
+
|
|
21
|
+
Walking up from the cwd (rather than from this module's ``__file__``) ensures
|
|
22
|
+
the *user's* project is resolved. Resolving from ``__file__`` would instead
|
|
23
|
+
find the installed CLI package's own ``pyproject.toml`` in an editable or
|
|
24
|
+
monorepo install, which is never what a user of ``nooa`` wants.
|
|
25
|
+
|
|
26
|
+
Uses a local implementation to keep CLI startup fast (avoids importing
|
|
27
|
+
the heavy nooa core package at module level).
|
|
28
|
+
"""
|
|
29
|
+
cwd = Path.cwd().resolve()
|
|
30
|
+
for candidate in (cwd, *cwd.parents):
|
|
31
|
+
if (candidate / "pyproject.toml").exists():
|
|
32
|
+
return candidate
|
|
33
|
+
return cwd
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def format_size(size_bytes: int) -> str:
|
|
37
|
+
"""Format a byte count as human-readable (e.g. '4.2 MB')."""
|
|
38
|
+
if size_bytes < 1024:
|
|
39
|
+
return f"{size_bytes} B"
|
|
40
|
+
elif size_bytes < 1024 * 1024:
|
|
41
|
+
return f"{size_bytes / 1024:.1f} KB"
|
|
42
|
+
elif size_bytes < 1024 * 1024 * 1024:
|
|
43
|
+
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
|
44
|
+
else:
|
|
45
|
+
return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Auto-discovered command modules for the nooa CLI.
|
|
4
|
+
|
|
5
|
+
╔══════════════════════════════════════════════════════════════════════╗
|
|
6
|
+
║ HOW TO ADD A NEW COMMAND ║
|
|
7
|
+
╠══════════════════════════════════════════════════════════════════════╣
|
|
8
|
+
║ ║
|
|
9
|
+
║ 1. Create a new .py file in this directory (commands/) ║
|
|
10
|
+
║ 2. Define a click command or group ║
|
|
11
|
+
║ 3. Assign it to a module-level variable named `command` ║
|
|
12
|
+
║ 4. That's it — it's automatically registered. ║
|
|
13
|
+
║ ║
|
|
14
|
+
║ See _template.py for a copy-paste starter. ║
|
|
15
|
+
║ ║
|
|
16
|
+
╚══════════════════════════════════════════════════════════════════════╝
|
|
17
|
+
|
|
18
|
+
Convention
|
|
19
|
+
----------
|
|
20
|
+
|
|
21
|
+
Each Python file in this directory that does NOT start with ``_`` is
|
|
22
|
+
auto-discovered and registered as a top-level subcommand of ``nooa``.
|
|
23
|
+
|
|
24
|
+
A command module **must** export a module-level variable named ``command``
|
|
25
|
+
that is a ``click.BaseCommand`` (either a ``@click.command()`` or a
|
|
26
|
+
``@click.group()``).
|
|
27
|
+
|
|
28
|
+
Optionally, you can set:
|
|
29
|
+
NAME = "custom-name" # Override the command name (default: filename)
|
|
30
|
+
|
|
31
|
+
The file name (minus ``.py``) becomes the subcommand name by default::
|
|
32
|
+
|
|
33
|
+
commands/start_dev.py → nooa start-dev ...
|
|
34
|
+
|
|
35
|
+
Files starting with ``_`` are ignored (private helpers, templates, etc.).
|
|
36
|
+
|
|
37
|
+
Minimal example (commands/hello.py)::
|
|
38
|
+
|
|
39
|
+
import click
|
|
40
|
+
|
|
41
|
+
@click.command()
|
|
42
|
+
@click.argument("name", default="world")
|
|
43
|
+
def command(name):
|
|
44
|
+
\"\"\"Say hello.\"\"\"
|
|
45
|
+
click.echo(f"Hello, {name}!")
|
|
46
|
+
|
|
47
|
+
Full example with a group (commands/things.py)::
|
|
48
|
+
|
|
49
|
+
import click
|
|
50
|
+
|
|
51
|
+
@click.group()
|
|
52
|
+
def command():
|
|
53
|
+
\"\"\"Manage things.\"\"\"
|
|
54
|
+
|
|
55
|
+
@command.command()
|
|
56
|
+
def list():
|
|
57
|
+
\"\"\"List all things.\"\"\"
|
|
58
|
+
click.echo("thing-1\\nthing-2")
|
|
59
|
+
|
|
60
|
+
@command.command()
|
|
61
|
+
@click.argument("name")
|
|
62
|
+
def create(name):
|
|
63
|
+
\"\"\"Create a new thing.\"\"\"
|
|
64
|
+
click.echo(f"Created {name}")
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
import importlib
|
|
68
|
+
import pkgutil
|
|
69
|
+
from collections.abc import Iterator
|
|
70
|
+
|
|
71
|
+
import click
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def discover_commands() -> Iterator[tuple[str, click.Command]]:
|
|
75
|
+
"""Yield (name, command) pairs from all command modules in this package.
|
|
76
|
+
|
|
77
|
+
Scans this directory for Python modules, imports each one, and looks
|
|
78
|
+
for a ``command`` attribute that is a ``click.Command`` or ``click.Group``.
|
|
79
|
+
|
|
80
|
+
Modules starting with ``_`` are skipped (private / template files).
|
|
81
|
+
"""
|
|
82
|
+
package_path = __path__
|
|
83
|
+
package_name = __name__
|
|
84
|
+
|
|
85
|
+
for module_info in pkgutil.iter_modules(package_path):
|
|
86
|
+
# Skip private modules (_template, _helpers, etc.)
|
|
87
|
+
if module_info.name.startswith("_"):
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
module = importlib.import_module(f"{package_name}.{module_info.name}")
|
|
91
|
+
|
|
92
|
+
# The module must export `command`
|
|
93
|
+
cmd = getattr(module, "command", None)
|
|
94
|
+
if cmd is None:
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
if not isinstance(cmd, click.Command):
|
|
98
|
+
raise TypeError(
|
|
99
|
+
f"commands/{module_info.name}.py: `command` must be a click.Command or click.Group, "
|
|
100
|
+
f"got {type(cmd).__name__}. Use @click.command() or @click.group()."
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Allow overriding the command name
|
|
104
|
+
name = getattr(module, "NAME", module_info.name)
|
|
105
|
+
|
|
106
|
+
yield name, cmd
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Shared utilities for OTLP trace import commands."""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import urllib.parse
|
|
7
|
+
import urllib.request
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def validate_endpoint(endpoint: str) -> None:
|
|
13
|
+
"""Validate that the endpoint uses an HTTP(S) scheme."""
|
|
14
|
+
parsed = urllib.parse.urlparse(endpoint)
|
|
15
|
+
if parsed.scheme not in ("http", "https"):
|
|
16
|
+
raise click.BadParameter(
|
|
17
|
+
f"Endpoint must use http:// or https:// scheme, got: {endpoint}",
|
|
18
|
+
param_hint="'--endpoint'",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def inject_resource_attrs(body: dict, attrs: dict[str, str | bool | int]) -> dict:
|
|
23
|
+
"""Inject additional resource attributes into an OTLP body (skips existing keys).
|
|
24
|
+
|
|
25
|
+
Values are typed: str → stringValue, bool → boolValue, int → intValue.
|
|
26
|
+
"""
|
|
27
|
+
for rs in body.get("resourceSpans", []):
|
|
28
|
+
resource = rs.setdefault("resource", {})
|
|
29
|
+
existing = resource.setdefault("attributes", [])
|
|
30
|
+
existing_keys = {a["key"] for a in existing}
|
|
31
|
+
for key, val in attrs.items():
|
|
32
|
+
if key not in existing_keys:
|
|
33
|
+
if isinstance(val, bool):
|
|
34
|
+
otlp_val = {"boolValue": val}
|
|
35
|
+
elif isinstance(val, int):
|
|
36
|
+
otlp_val = {"intValue": val}
|
|
37
|
+
else:
|
|
38
|
+
otlp_val = {"stringValue": val}
|
|
39
|
+
existing.append({"key": key, "value": otlp_val})
|
|
40
|
+
return body
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def post_trace(endpoint: str, body: dict, timeout: float = 30) -> bool:
|
|
44
|
+
"""POST a single OTLP trace body to the viewer endpoint.
|
|
45
|
+
|
|
46
|
+
``timeout`` defaults to 30s so large batched bodies (see ``post_traces_batch``)
|
|
47
|
+
don't spuriously time out; per-line callers are unaffected.
|
|
48
|
+
"""
|
|
49
|
+
url = f"{endpoint.rstrip('/')}/v1/traces"
|
|
50
|
+
data = json.dumps(body, separators=(",", ":")).encode("utf-8")
|
|
51
|
+
req = urllib.request.Request(
|
|
52
|
+
url,
|
|
53
|
+
data=data,
|
|
54
|
+
headers={"Content-Type": "application/json"},
|
|
55
|
+
method="POST",
|
|
56
|
+
)
|
|
57
|
+
try:
|
|
58
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
59
|
+
return resp.status < 300
|
|
60
|
+
except Exception:
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def post_traces_batch(endpoint: str, bodies: list[dict]) -> bool:
|
|
65
|
+
"""POST multiple OTLP bodies as one request by merging their ``resourceSpans``.
|
|
66
|
+
|
|
67
|
+
Combines the ``resourceSpans`` arrays of every body into a single OTLP envelope
|
|
68
|
+
and posts it once, drastically reducing HTTP round-trips for large imports.
|
|
69
|
+
Returns True (a no-op success) when there are no spans to send.
|
|
70
|
+
"""
|
|
71
|
+
merged: dict = {"resourceSpans": []}
|
|
72
|
+
for body in bodies:
|
|
73
|
+
# Guard against malformed bodies whose ``resourceSpans`` is missing or
|
|
74
|
+
# not a list (e.g. None or a dict); skip rather than raise on extend.
|
|
75
|
+
spans = body.get("resourceSpans")
|
|
76
|
+
if isinstance(spans, list):
|
|
77
|
+
merged["resourceSpans"].extend(spans)
|
|
78
|
+
if not merged["resourceSpans"]:
|
|
79
|
+
return True
|
|
80
|
+
return post_trace(endpoint, merged)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def post_annotations(endpoint: str, annotations: list[dict]) -> int:
|
|
84
|
+
"""POST annotations to the viewer endpoint. Returns count of successfully imported."""
|
|
85
|
+
url = f"{endpoint.rstrip('/')}/api/annotations"
|
|
86
|
+
imported = 0
|
|
87
|
+
for ann in annotations:
|
|
88
|
+
ann = {k: v for k, v in ann.items() if k != "id"}
|
|
89
|
+
data = json.dumps(ann, separators=(",", ":")).encode("utf-8")
|
|
90
|
+
req = urllib.request.Request(
|
|
91
|
+
url,
|
|
92
|
+
data=data,
|
|
93
|
+
headers={"Content-Type": "application/json"},
|
|
94
|
+
method="POST",
|
|
95
|
+
)
|
|
96
|
+
try:
|
|
97
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
98
|
+
if resp.status < 300:
|
|
99
|
+
imported += 1
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
return imported
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def session_exists(endpoint: str, session_id: str) -> bool:
|
|
106
|
+
"""Check whether a session already exists in the viewer."""
|
|
107
|
+
url = f"{endpoint.rstrip('/')}/api/trace-count?session_id={urllib.parse.quote(session_id)}"
|
|
108
|
+
req = urllib.request.Request(url, method="GET")
|
|
109
|
+
try:
|
|
110
|
+
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
111
|
+
return resp.status == 200
|
|
112
|
+
except Exception:
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def check_endpoint_reachable(endpoint: str) -> bool:
|
|
117
|
+
"""Return True if the viewer API is reachable."""
|
|
118
|
+
try:
|
|
119
|
+
with urllib.request.urlopen(f"{endpoint.rstrip('/')}/api/version", timeout=5):
|
|
120
|
+
return True
|
|
121
|
+
except Exception:
|
|
122
|
+
return False
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""<One-line description of what this command does.>
|
|
4
|
+
|
|
5
|
+
Copy this file as a starting point for a new nooa CLI command.
|
|
6
|
+
|
|
7
|
+
1. Copy this file: cp _template.py mycommand.py
|
|
8
|
+
2. Rename `command` if needed (it stays as `command`)
|
|
9
|
+
3. Add your logic
|
|
10
|
+
4. Done — it's automatically discovered.
|
|
11
|
+
|
|
12
|
+
The filename becomes the subcommand name:
|
|
13
|
+
mycommand.py → nooa mycommand ...
|
|
14
|
+
|
|
15
|
+
To override the name, add at module level:
|
|
16
|
+
NAME = "custom-name"
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
nooa mycommand
|
|
20
|
+
nooa mycommand --verbose
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import click
|
|
24
|
+
|
|
25
|
+
# Optional: override the subcommand name (default: filename without .py)
|
|
26
|
+
# NAME = "custom-name"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# SIMPLE COMMAND — use this for leaf commands (no subcommands)
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@click.command()
|
|
35
|
+
@click.argument("target", default="world")
|
|
36
|
+
@click.option("--verbose", "-v", is_flag=True, help="Verbose output.")
|
|
37
|
+
def command(target: str, verbose: bool):
|
|
38
|
+
"""Short description shown in `nooa --help`.
|
|
39
|
+
|
|
40
|
+
TARGET is the thing to operate on.
|
|
41
|
+
|
|
42
|
+
\b
|
|
43
|
+
Examples:
|
|
44
|
+
nooa mycommand
|
|
45
|
+
nooa mycommand foo --verbose
|
|
46
|
+
"""
|
|
47
|
+
click.echo(f"Hello, {target}!")
|
|
48
|
+
if verbose:
|
|
49
|
+
click.echo("(verbose mode)")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# GROUP COMMAND — uncomment this instead if you need subcommands
|
|
54
|
+
# (e.g. nooa things list / nooa things create)
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
# @click.group()
|
|
58
|
+
# def command():
|
|
59
|
+
# """Manage things."""
|
|
60
|
+
#
|
|
61
|
+
#
|
|
62
|
+
# @command.command()
|
|
63
|
+
# def list():
|
|
64
|
+
# """List all things."""
|
|
65
|
+
# click.echo("thing-1")
|
|
66
|
+
# click.echo("thing-2")
|
|
67
|
+
#
|
|
68
|
+
#
|
|
69
|
+
# @command.command()
|
|
70
|
+
# @click.argument("name")
|
|
71
|
+
# def create(name: str):
|
|
72
|
+
# """Create a new thing."""
|
|
73
|
+
# click.echo(f"Created: {name}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# TIPS
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
#
|
|
80
|
+
# Shared utilities:
|
|
81
|
+
# from nooa_cli._common import find_project_root, format_size
|
|
82
|
+
#
|
|
83
|
+
# Lazy imports (keep CLI startup fast — only import heavy deps inside handlers):
|
|
84
|
+
# def command(...):
|
|
85
|
+
# import pandas as pd # ← lazy, not at module level
|
|
86
|
+
#
|
|
87
|
+
# Import the nooa framework (lazy — only when the command actually runs):
|
|
88
|
+
# def command(...):
|
|
89
|
+
# from nooa import Agent
|