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 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
@@ -0,0 +1,8 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Enable running as: python -m nooa_cli"""
4
+
5
+ from . import main
6
+
7
+ if __name__ == "__main__":
8
+ main()
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