09e9f3545f
Test / Code Quality (push) Has been cancelled
Test / Test (macos-latest, Python 3.10) (push) Has been cancelled
Test / Test (macos-latest, Python 3.11) (push) Has been cancelled
Test / Test (macos-latest, Python 3.12) (push) Has been cancelled
Test / Test (macos-latest, Python 3.13) (push) Has been cancelled
Test / Test (macos-latest, Python 3.14) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.10) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.11) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.12) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.13) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.14) (push) Has been cancelled
Test / Test (windows-latest, Python 3.10) (push) Has been cancelled
Test / Test (windows-latest, Python 3.11) (push) Has been cancelled
Test / Test (windows-latest, Python 3.12) (push) Has been cancelled
Test / Test (windows-latest, Python 3.13) (push) Has been cancelled
Test / Test (windows-latest, Python 3.14) (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
dependency-audit / pip-audit (push) Has been cancelled
221 lines
7.6 KiB
Python
221 lines
7.6 KiB
Python
"""Public CLI contract introspection (the ``cli_contract`` baseline derive).
|
|
|
|
``build_cli_contract()`` walks the Click command tree and returns the
|
|
deterministic public CLI inventory (command tree, options, defaults, help,
|
|
aliases) that is frozen as the ``cli_contract`` regenerable baseline (ADR-0022,
|
|
``tests/fixtures/cli_contract_baseline.json``).
|
|
|
|
This pure-introspection core was extracted out of
|
|
``tests/unit/cli/test_cli_contract.py`` into this ``_``-prefixed non-test module
|
|
so the baseline registry (``tests/_baselines/registry.py``) can import
|
|
``build_cli_contract`` without one ``test_*`` module importing from another
|
|
(forbidden by ``tests/_guardrails/test_no_cross_test_imports.py``). The behavioural
|
|
CLI-contract tests still live in ``test_cli_contract.py`` and import these names
|
|
from here.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import click
|
|
|
|
from notebooklm.notebooklm_cli import cli
|
|
|
|
ROOT_COMMAND = "notebooklm"
|
|
|
|
TRACKED_GROUPS = (
|
|
"download",
|
|
"source",
|
|
"generate",
|
|
"artifact",
|
|
"session",
|
|
"profile",
|
|
"notebook",
|
|
"chat",
|
|
"note",
|
|
"label",
|
|
"share",
|
|
"research",
|
|
)
|
|
|
|
CLICK_GROUPS = (
|
|
"agent",
|
|
"download",
|
|
"source",
|
|
"generate",
|
|
"artifact",
|
|
"language",
|
|
"profile",
|
|
"note",
|
|
"label",
|
|
"share",
|
|
"research",
|
|
"skill",
|
|
)
|
|
|
|
TOP_LEVEL_SURFACES = {
|
|
"session": ("login", "auth", "use", "status", "clear"),
|
|
"notebook": ("list", "create", "delete", "rename", "metadata", "summary"),
|
|
"chat": ("ask", "suggest-prompts", "configure", "history"),
|
|
}
|
|
|
|
EXTRA_TOP_LEVEL_COMMANDS = ("completion", "doctor")
|
|
|
|
|
|
def _command_for(path: str) -> click.Command:
|
|
cmd: click.Command = cli
|
|
if not path or path == ROOT_COMMAND:
|
|
return cmd
|
|
for part in path.split():
|
|
if not isinstance(cmd, click.Group):
|
|
raise AssertionError(f"{path!r} traversed through non-group {cmd!r}")
|
|
next_cmd = cmd.get_command(click.Context(cmd), part)
|
|
if next_cmd is None:
|
|
raise AssertionError(f"missing command path: {path!r}")
|
|
cmd = next_cmd
|
|
return cmd
|
|
|
|
|
|
def _json_default(value):
|
|
if value is None or isinstance(value, (str, int, float, bool)):
|
|
return value
|
|
if isinstance(value, (tuple, list)):
|
|
return [_json_default(v) for v in value]
|
|
return str(value)
|
|
|
|
|
|
def _type_contract(param_type: click.ParamType) -> dict[str, object]:
|
|
data: dict[str, object] = {"name": param_type.name}
|
|
if isinstance(param_type, click.Choice):
|
|
data["choices"] = list(param_type.choices)
|
|
data["case_sensitive"] = param_type.case_sensitive
|
|
if isinstance(param_type, click.IntRange):
|
|
data["min"] = param_type.min
|
|
data["max"] = param_type.max
|
|
data["clamp"] = param_type.clamp
|
|
if isinstance(param_type, click.Path):
|
|
data["exists"] = param_type.exists
|
|
data["file_okay"] = param_type.file_okay
|
|
data["dir_okay"] = param_type.dir_okay
|
|
data["writable"] = param_type.writable
|
|
data["readable"] = param_type.readable
|
|
data["executable"] = param_type.executable
|
|
data["resolve_path"] = param_type.resolve_path
|
|
data["allow_dash"] = param_type.allow_dash
|
|
return data
|
|
|
|
|
|
def _has_custom_shell_complete(param: click.Option) -> bool:
|
|
return getattr(param, "_custom_shell_complete", None) is not None
|
|
|
|
|
|
def _visible_command_names(group: click.Group) -> list[str]:
|
|
ctx = click.Context(group)
|
|
names = group.list_commands(ctx)
|
|
return [name for name in names if not getattr(group.get_command(ctx, name), "hidden", False)]
|
|
|
|
|
|
def _param_contract(param: click.Parameter) -> dict[str, object]:
|
|
base: dict[str, object] = {
|
|
"name": param.name,
|
|
"required": param.required,
|
|
"type": _type_contract(param.type),
|
|
}
|
|
if isinstance(param, click.Option):
|
|
base.update(
|
|
{
|
|
"kind": "option",
|
|
"opts": list(param.opts),
|
|
"secondary_opts": list(param.secondary_opts),
|
|
"default": _json_default(param.default),
|
|
"envvar": _json_default(param.envvar),
|
|
"is_flag": param.is_flag,
|
|
"multiple": param.multiple,
|
|
"help": param.help,
|
|
"has_custom_shell_complete": _has_custom_shell_complete(param),
|
|
}
|
|
)
|
|
else:
|
|
base.update({"kind": "argument", "nargs": param.nargs})
|
|
return base
|
|
|
|
|
|
def _command_contract(path: str) -> dict[str, object]:
|
|
cmd = _command_for(path)
|
|
data: dict[str, object] = {
|
|
"class": type(cmd).__name__,
|
|
"params": [_param_contract(param) for param in cmd.params],
|
|
"short_help": cmd.get_short_help_str(),
|
|
}
|
|
if isinstance(cmd, click.Group):
|
|
data["commands"] = _visible_command_names(cmd)
|
|
return data
|
|
|
|
|
|
def _option_by_name(path: str, name: str) -> click.Option:
|
|
for param in _command_for(path).params:
|
|
if isinstance(param, click.Option) and param.name == name:
|
|
return param
|
|
raise AssertionError(f"{path!r} has no option named {name!r}")
|
|
|
|
|
|
def _iter_command_paths(path: str) -> list[str]:
|
|
cmd = _command_for(path)
|
|
paths = [path]
|
|
if isinstance(cmd, click.Group):
|
|
for child in _visible_command_names(cmd):
|
|
child_path = f"{path} {child}" if path else child
|
|
paths.extend(_iter_command_paths(child_path))
|
|
return paths
|
|
|
|
|
|
def _tracked_command_paths() -> list[str]:
|
|
paths: list[str] = [ROOT_COMMAND]
|
|
for group in CLICK_GROUPS:
|
|
paths.extend(_iter_command_paths(group))
|
|
for commands in TOP_LEVEL_SURFACES.values():
|
|
for name in commands:
|
|
paths.extend(_iter_command_paths(name))
|
|
for name in EXTRA_TOP_LEVEL_COMMANDS:
|
|
paths.extend(_iter_command_paths(name))
|
|
return sorted(set(paths))
|
|
|
|
|
|
def _same_params(left: click.Command, right: click.Command) -> bool:
|
|
return [_param_contract(p) for p in left.params] == [_param_contract(p) for p in right.params]
|
|
|
|
|
|
def build_cli_contract() -> dict[str, object]:
|
|
"""Return the deterministic public CLI inventory used by the baseline."""
|
|
download_cinematic_video = _command_for("download cinematic-video")
|
|
download_video = _command_for("download video")
|
|
generate_cinematic_video = _command_for("generate cinematic-video")
|
|
generate_video = _command_for("generate video")
|
|
return {
|
|
"schema_version": 1,
|
|
"tracked_surfaces": list(TRACKED_GROUPS),
|
|
"root_commands": _visible_command_names(cli),
|
|
"top_level_surfaces": {key: list(value) for key, value in TOP_LEVEL_SURFACES.items()},
|
|
"click_groups": {
|
|
group: _visible_command_names(_command_for(group)) for group in CLICK_GROUPS
|
|
},
|
|
"aliases": {
|
|
"download cinematic-video": {
|
|
"canonical": "download video",
|
|
"same_callback": download_cinematic_video.callback is download_video.callback,
|
|
"same_params": _same_params(download_cinematic_video, download_video),
|
|
},
|
|
"generate cinematic-video": {
|
|
"canonical": "generate video --format cinematic",
|
|
"same_callback": generate_cinematic_video.callback is generate_video.callback,
|
|
"same_params": _same_params(generate_cinematic_video, generate_video),
|
|
},
|
|
},
|
|
"completion_callbacks": {
|
|
"notebook": _has_custom_shell_complete(_option_by_name("source list", "notebook_id")),
|
|
"download_artifact": _has_custom_shell_complete(
|
|
_option_by_name("download audio", "artifact_id")
|
|
),
|
|
},
|
|
"commands": {path: _command_contract(path) for path in _tracked_command_paths()},
|
|
}
|