From 987cc1701f740835cb2b3c7f9c970471d709a37e Mon Sep 17 00:00:00 2001 From: Tim Huff Date: Mon, 27 Apr 2026 14:06:28 -0700 Subject: [PATCH 01/10] first commit --- pyproject.toml | 2 +- src/groundlight/cli.py | 97 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 85 insertions(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7d2b03af..39d3da11 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ packages = [ {include = "**/*.py", from = "src"}, ] readme = "README.md" -version = "0.27.0" +version = "0.28.0" [tool.poetry.dependencies] # For certifi, use ">=" instead of "^" since it upgrades its "major version" every year, not really following semver diff --git a/src/groundlight/cli.py b/src/groundlight/cli.py index de33e741..f81bcda3 100644 --- a/src/groundlight/cli.py +++ b/src/groundlight/cli.py @@ -1,17 +1,33 @@ +import logging +import sys +from enum import Enum from functools import wraps -from typing import Union +from typing import Any, Union import typer from typing_extensions import get_origin -from groundlight import Groundlight +from groundlight import ExperimentalApi, Groundlight from groundlight.client import ApiTokenError +logger = logging.getLogger("groundlight.sdk") + cli_app = typer.Typer( no_args_is_help=True, context_settings={"help_option_names": ["-h", "--help"], "max_content_width": 800}, ) +experimental_app = typer.Typer( + no_args_is_help=True, + help="Experimental commands — may change or be removed without notice.", + context_settings={"help_option_names": ["-h", "--help"], "max_content_width": 800}, +) +cli_app.add_typer(experimental_app, name="experimental") +cli_app.add_typer(experimental_app, name="exp") + + +_CLI_PRIMITIVE_TYPES = (str, int, float, bool) + def is_cli_supported_type(annotation): """ @@ -21,15 +37,45 @@ def is_cli_supported_type(annotation): return annotation in (int, float, bool) -def class_func_to_cli(method): +def is_cli_representable(annotation) -> bool: + """Returns True if the annotation is a type Typer can natively represent as a CLI argument. + + Primitive scalar types, Enum subclasses, and Union types (handled separately) are considered + representable. Complex types like dict, list, bytes, and custom model classes are not. + """ + if annotation in _CLI_PRIMITIVE_TYPES: + return True + if isinstance(annotation, type) and issubclass(annotation, Enum): + return True + if get_origin(annotation) is Union: + return True + return False + + +def _format_result(result: Any) -> str: + """Format a method return value for CLI output. + + Pydantic models are serialized to indented JSON. Everything else falls back to str(). + """ + try: + return result.model_dump_json(indent=2) + except AttributeError: + return str(result) + + +def class_func_to_cli(method, is_experimental: bool = False): """ - Given the class method, create a method with the identical signature to provide the help documentation and - but only instantiates the class when the method is actually called. + Given a class method, return a wrapper function with the same signature that Typer can + register as a CLI command. The wrapper instantiates ExperimentalApi at call time (which + also provides all stable Groundlight methods via inheritance), so a single instantiation + path serves both stable and experimental commands. + + If is_experimental is True, a warning is printed to stderr before the method runs. """ - # We create a fake class and fake method so we have the correct annotations for typer to use - # When we wrap the fake method, we only use the fake method's name to access the real method - # and attach it to a Groundlight instance that we create at function call time + # We create a fake class and fake method so we have the correct annotations for typer to use. + # When we wrap the fake method, we only use the fake method's name to look up and call the + # real method on an ExperimentalApi instance created at call time. class FakeClass: pass @@ -38,14 +84,22 @@ class FakeClass: @wraps(fake_method) def wrapper(*args, **kwargs): - gl = Groundlight() - gl_method = vars(Groundlight)[fake_method.__name__] - gl_bound_method = gl_method.__get__(gl, Groundlight) # pylint: disable=all - print(gl_bound_method(*args, **kwargs)) # this is where we output to the console + if is_experimental: + print( + f"Warning: '{fake_method.__name__}' is an experimental command and may change without notice.", + file=sys.stderr, + ) + gl = ExperimentalApi() + bound_method = getattr(gl, fake_method.__name__) + result = bound_method(*args, **kwargs) + if result is not None: + print(_format_result(result)) # not recommended practice to directly change annotations, but gets around Typer not supporting Union types cli_unsupported_params = [] for name, annotation in method.__annotations__.items(): + if name == "return": + continue if get_origin(annotation) is Union: # If we can submit a string, we take the string from the cli if str in annotation.__args__: @@ -60,6 +114,11 @@ def wrapper(*args, **kwargs): break if not found_supported_type: cli_unsupported_params.append(name) + elif is_experimental and not is_cli_representable(annotation): + # For experimental methods only: proactively flag non-Union types that Typer cannot + # represent (e.g. dict, list, custom models) so the caller can skip them gracefully + # before Typer raises a deferred RuntimeError at cli_app() invocation time. + cli_unsupported_params.append(name) # Ideally we could just not list the unsupported params, but it doesn't seem natively supported by Typer # and requires more metaprogamming than makes sense at the moment. For now, we require methods to support str for param in cli_unsupported_params: @@ -72,12 +131,24 @@ def wrapper(*args, **kwargs): def groundlight(): + """Entry point for the groundlight CLI.""" try: - # For each method in the Groundlight class, create a function that can be called from the command line + stable_names = {n for n, m in vars(Groundlight).items() if callable(m) and not n.startswith("_")} + for name, method in vars(Groundlight).items(): if callable(method) and not name.startswith("_"): cli_func = class_func_to_cli(method) cli_app.command()(cli_func) + + for name, method in vars(ExperimentalApi).items(): + if not callable(method) or name.startswith("_") or name in stable_names: + continue + try: + cli_func = class_func_to_cli(method, is_experimental=True) + experimental_app.command()(cli_func) + except Exception as e: # pylint: disable=broad-except + logger.debug("Skipping experimental CLI command '%s': %s", name, e) + cli_app() except ApiTokenError as e: print(e) From de74ef74e6aa08e41abaef075450fa4fcc7cabc3 Mon Sep 17 00:00:00 2001 From: Tim Huff Date: Mon, 27 Apr 2026 15:05:15 -0700 Subject: [PATCH 02/10] adding pretty print for return values --- src/groundlight/cli.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/groundlight/cli.py b/src/groundlight/cli.py index f81bcda3..602c58ed 100644 --- a/src/groundlight/cli.py +++ b/src/groundlight/cli.py @@ -1,9 +1,14 @@ +import json import logging import sys +from datetime import datetime from enum import Enum from functools import wraps from typing import Any, Union +from groundlight_openapi_client.model_utils import OpenApiModel +from pydantic import BaseModel + import typer from typing_extensions import get_origin @@ -52,15 +57,26 @@ def is_cli_representable(annotation) -> bool: return False +def _json_default(obj: Any) -> Any: + """Fallback serializer for json.dumps — handles datetime values.""" + if isinstance(obj, datetime): + return obj.isoformat() + raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable") + + def _format_result(result: Any) -> str: - """Format a method return value for CLI output. + """Format a CLI result value as a human-readable, jq-compatible string. - Pydantic models are serialized to indented JSON. Everything else falls back to str(). + Pydantic models and OpenAPI client objects are serialized to indented JSON. + Plain dicts and lists are also JSON. Everything else falls back to str(). """ - try: + if isinstance(result, BaseModel): return result.model_dump_json(indent=2) - except AttributeError: - return str(result) + if isinstance(result, OpenApiModel): + return json.dumps(result.to_dict(), indent=2, default=_json_default) + if isinstance(result, (dict, list)): + return json.dumps(result, indent=2, default=_json_default) + return str(result) def class_func_to_cli(method, is_experimental: bool = False): From a56e831983a0b5072d7dda0de8660769a3143a4e Mon Sep 17 00:00:00 2001 From: Auto-format Bot Date: Mon, 27 Apr 2026 22:05:56 +0000 Subject: [PATCH 03/10] Automatically reformatting code --- src/groundlight/cli.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/groundlight/cli.py b/src/groundlight/cli.py index 602c58ed..6185f86e 100644 --- a/src/groundlight/cli.py +++ b/src/groundlight/cli.py @@ -6,10 +6,9 @@ from functools import wraps from typing import Any, Union +import typer from groundlight_openapi_client.model_utils import OpenApiModel from pydantic import BaseModel - -import typer from typing_extensions import get_origin from groundlight import ExperimentalApi, Groundlight From e0d97342b4b3303dcff22d9dbf048d117d6673e7 Mon Sep 17 00:00:00 2001 From: Tim Huff Date: Mon, 27 Apr 2026 16:01:19 -0700 Subject: [PATCH 04/10] organizing commands into groupings --- src/groundlight/cli.py | 114 ++++++++++++++++++++++++++++++++++++----- test/unit/test_cli.py | 25 +++++---- 2 files changed, 118 insertions(+), 21 deletions(-) diff --git a/src/groundlight/cli.py b/src/groundlight/cli.py index 602c58ed..787951fe 100644 --- a/src/groundlight/cli.py +++ b/src/groundlight/cli.py @@ -27,11 +27,8 @@ help="Experimental commands — may change or be removed without notice.", context_settings={"help_option_names": ["-h", "--help"], "max_content_width": 800}, ) -cli_app.add_typer(experimental_app, name="experimental") -cli_app.add_typer(experimental_app, name="exp") - - -_CLI_PRIMITIVE_TYPES = (str, int, float, bool) +cli_app.add_typer(experimental_app, name="exp", rich_help_panel="Subcommands") +cli_app.add_typer(experimental_app, name="experimental", hidden=True) def is_cli_supported_type(annotation): @@ -48,7 +45,7 @@ def is_cli_representable(annotation) -> bool: Primitive scalar types, Enum subclasses, and Union types (handled separately) are considered representable. Complex types like dict, list, bytes, and custom model classes are not. """ - if annotation in _CLI_PRIMITIVE_TYPES: + if annotation in (str, int, float, bool): return True if isinstance(annotation, type) and issubclass(annotation, Enum): return True @@ -146,22 +143,115 @@ def wrapper(*args, **kwargs): return wrapper +# Methods to exclude from the CLI entirely. These may be too complex to express +# as CLI commands, deprecated, or otherwise not useful from a shell context. +_CLI_EXCLUDED_METHODS = { + "make_action", + "create_rule", + "get_rule", + "delete_rule", + "list_rules", + "delete_all_rules", + "start_inspection", + "update_inspection_metadata", + "stop_inspection", +} + +# Desired display order of command groups in the CLI help output. +# Groups not listed here appear after the listed ones. +_GROUP_ORDER = [ + "Account", + "Detectors", + "Image Queries", + "ML Pipelines & Priming", + "Notes", + "Utilities", +] + +# Maps method names to their rich_help_panel group label for the CLI help output. +# Applies to both stable and experimental commands. Methods not listed here fall +# into the default "Commands" panel. +_COMMAND_GROUPS: dict = { + # Account + "whoami": "Account", + "get_month_to_date_usage": "Account", + # Detectors + "get_detector": "Detectors", + "get_detector_by_name": "Detectors", + "list_detectors": "Detectors", + "create_detector": "Detectors", + "get_or_create_detector": "Detectors", + "delete_detector": "Detectors", + "create_binary_detector": "Detectors", + "create_counting_detector": "Detectors", + "create_multiclass_detector": "Detectors", + "create_bounding_box_detector": "Detectors", + "create_detector_group": "Detectors", + "list_detector_groups": "Detectors", + "create_roi": "Detectors", + "update_detector_confidence_threshold": "Detectors", + "update_detector_status": "Detectors", + "update_detector_escalation_type": "Detectors", + "reset_detector": "Detectors", + "update_detector_name": "Detectors", + "create_text_recognition_detector": "Detectors", + "get_detector_evaluation": "Detectors", + "get_detector_metrics": "Detectors", + "download_mlbinary": "Detectors", + # Image Queries + "get_image_query": "Image Queries", + "list_image_queries": "Image Queries", + "submit_image_query": "Image Queries", + "ask_confident": "Image Queries", + "ask_ml": "Image Queries", + "ask_async": "Image Queries", + "wait_for_confident_result": "Image Queries", + "wait_for_ml_result": "Image Queries", + "get_image": "Image Queries", + "add_label": "Image Queries", + # Notes + "get_notes": "Notes", + "create_note": "Notes", + # ML Pipelines & Priming + "list_detector_pipelines": "ML Pipelines & Priming", + "list_priming_groups": "ML Pipelines & Priming", + "create_priming_group": "ML Pipelines & Priming", + "get_priming_group": "ML Pipelines & Priming", + "delete_priming_group": "ML Pipelines & Priming", + # Utilities + "edge_base_url": "Utilities", + "get_raw_headers": "Utilities", +} + + +def _cli_sort_key(item: tuple) -> tuple: + """Sort key for CLI command registration that controls group and within-group ordering. + + Commands are ordered first by their group's position in _GROUP_ORDER (ungrouped last), + then alphabetically by method name within each group. + """ + name, _ = item + group = _COMMAND_GROUPS.get(name) + group_rank = _GROUP_ORDER.index(group) if group in _GROUP_ORDER else len(_GROUP_ORDER) + return (group_rank, name) + + def groundlight(): """Entry point for the groundlight CLI.""" try: stable_names = {n for n, m in vars(Groundlight).items() if callable(m) and not n.startswith("_")} - for name, method in vars(Groundlight).items(): - if callable(method) and not name.startswith("_"): + for name, method in sorted(vars(Groundlight).items(), key=_cli_sort_key): + if callable(method) and not name.startswith("_") and name not in _CLI_EXCLUDED_METHODS: cli_func = class_func_to_cli(method) - cli_app.command()(cli_func) + cli_app.command(rich_help_panel=_COMMAND_GROUPS.get(name))(cli_func) - for name, method in vars(ExperimentalApi).items(): - if not callable(method) or name.startswith("_") or name in stable_names: + for name, method in sorted(vars(ExperimentalApi).items(), key=_cli_sort_key): + if not callable(method) or name.startswith("_") or name in stable_names or name in _CLI_EXCLUDED_METHODS: continue try: cli_func = class_func_to_cli(method, is_experimental=True) - experimental_app.command()(cli_func) + experimental_app.command(rich_help_panel=_COMMAND_GROUPS.get(name))(cli_func) except Exception as e: # pylint: disable=broad-except logger.debug("Skipping experimental CLI command '%s': %s", name, e) diff --git a/test/unit/test_cli.py b/test/unit/test_cli.py index 47911b70..f5ea9d07 100644 --- a/test/unit/test_cli.py +++ b/test/unit/test_cli.py @@ -41,16 +41,9 @@ def test_detector_and_image_queries(detector_name: Callable): check=False, ) assert completed_process.returncode == 0 - match = re.search("id='([^']+)'", completed_process.stdout) + match = re.search(r'"id":\s*"([^"]+)"', completed_process.stdout) assert match is not None det_id_on_create = match.group(1) - # The output of the create-detector command looks something like: - # id='det_abc123' - # type= - # created_at=datetime.datetime(2023, 8, 30, 18, 3, 9, 489794, - # tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200))) - # name='testdetector 2023-08-31 01:03:09.039448' query='testdetector' - # group_name='__DEFAULT' confidence_threshold=0.9 # test getting detectors completed_process = subprocess.run( @@ -61,7 +54,7 @@ def test_detector_and_image_queries(detector_name: Callable): check=False, ) assert completed_process.returncode == 0 - match = re.search("id='([^']+)'", completed_process.stdout) + match = re.search(r'"id":\s*"([^"]+)"', completed_process.stdout) assert match is not None det_id_on_get = match.group(1) assert det_id_on_create == det_id_on_get @@ -110,6 +103,20 @@ def test_help(): assert completed_process.returncode == 0 +def test_experimental_subcommand(): + # Both 'experimental' and 'exp' should resolve to the same subcommand group + for alias in ("experimental", "exp"): + completed_process = subprocess.run( + ["groundlight", alias, "--help"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False, + ) + assert completed_process.returncode == 0 + assert "list-priming-groups" in completed_process.stdout + + def test_bad_commands(): completed_process = subprocess.run( ["groundlight", "wat"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False From fbe190a8db5c08d5287305710ac40f4ddc50f416 Mon Sep 17 00:00:00 2001 From: Tim Huff Date: Mon, 27 Apr 2026 16:08:20 -0700 Subject: [PATCH 05/10] adding --version --- src/groundlight/cli.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/groundlight/cli.py b/src/groundlight/cli.py index 33702072..32c29379 100644 --- a/src/groundlight/cli.py +++ b/src/groundlight/cli.py @@ -4,6 +4,7 @@ from datetime import datetime from enum import Enum from functools import wraps +from importlib.metadata import version as importlib_version from typing import Any, Union import typer @@ -21,6 +22,18 @@ context_settings={"help_option_names": ["-h", "--help"], "max_content_width": 800}, ) + +@cli_app.callback(invoke_without_command=True) +def _main( + ctx: typer.Context, + version: bool = typer.Option(False, "--version", "-v", is_eager=True, help="Show the SDK version and exit."), +): + if version: + print(importlib_version("groundlight")) + raise typer.Exit() + if ctx.invoked_subcommand is None: + print(ctx.get_help()) + experimental_app = typer.Typer( no_args_is_help=True, help="Experimental commands — may change or be removed without notice.", From 518b6085882d623a4741d2eaa5f8674557667eb5 Mon Sep 17 00:00:00 2001 From: Auto-format Bot Date: Mon, 27 Apr 2026 23:09:05 +0000 Subject: [PATCH 06/10] Automatically reformatting code --- src/groundlight/cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/groundlight/cli.py b/src/groundlight/cli.py index 32c29379..9c24cdf9 100644 --- a/src/groundlight/cli.py +++ b/src/groundlight/cli.py @@ -34,6 +34,7 @@ def _main( if ctx.invoked_subcommand is None: print(ctx.get_help()) + experimental_app = typer.Typer( no_args_is_help=True, help="Experimental commands — may change or be removed without notice.", From ef71903edf6717562a3c2ae27fc37bb7fb86d75c Mon Sep 17 00:00:00 2001 From: Tim Huff Date: Mon, 27 Apr 2026 16:11:41 -0700 Subject: [PATCH 07/10] code cleanup --- src/groundlight/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/groundlight/cli.py b/src/groundlight/cli.py index 32c29379..8c9a2c0e 100644 --- a/src/groundlight/cli.py +++ b/src/groundlight/cli.py @@ -18,7 +18,6 @@ logger = logging.getLogger("groundlight.sdk") cli_app = typer.Typer( - no_args_is_help=True, context_settings={"help_option_names": ["-h", "--help"], "max_content_width": 800}, ) @@ -34,6 +33,7 @@ def _main( if ctx.invoked_subcommand is None: print(ctx.get_help()) + experimental_app = typer.Typer( no_args_is_help=True, help="Experimental commands — may change or be removed without notice.", @@ -183,7 +183,7 @@ def wrapper(*args, **kwargs): # Maps method names to their rich_help_panel group label for the CLI help output. # Applies to both stable and experimental commands. Methods not listed here fall # into the default "Commands" panel. -_COMMAND_GROUPS: dict = { +_COMMAND_GROUPS: dict[str, str] = { # Account "whoami": "Account", "get_month_to_date_usage": "Account", From cccccd322098b53bdcc3730b3472ea5cef338ae4 Mon Sep 17 00:00:00 2001 From: Tim Huff Date: Mon, 27 Apr 2026 16:27:17 -0700 Subject: [PATCH 08/10] respondign to PR feedback --- src/groundlight/cli.py | 28 ++++++++++++++++++++-------- test/unit/test_cli.py | 13 +++++++++++++ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/groundlight/cli.py b/src/groundlight/cli.py index 8c9a2c0e..2776cb4c 100644 --- a/src/groundlight/cli.py +++ b/src/groundlight/cli.py @@ -1,11 +1,13 @@ import json import logging import sys -from datetime import datetime +from datetime import date, datetime +from decimal import Decimal from enum import Enum from functools import wraps from importlib.metadata import version as importlib_version from typing import Any, Union +from uuid import UUID import typer from groundlight_openapi_client.model_utils import OpenApiModel @@ -15,7 +17,7 @@ from groundlight import ExperimentalApi, Groundlight from groundlight.client import ApiTokenError -logger = logging.getLogger("groundlight.sdk") +logger = logging.getLogger(__name__) cli_app = typer.Typer( context_settings={"help_option_names": ["-h", "--help"], "max_content_width": 800}, @@ -40,7 +42,6 @@ def _main( context_settings={"help_option_names": ["-h", "--help"], "max_content_width": 800}, ) cli_app.add_typer(experimental_app, name="exp", rich_help_panel="Subcommands") -cli_app.add_typer(experimental_app, name="experimental", hidden=True) def is_cli_supported_type(annotation): @@ -67,10 +68,20 @@ def is_cli_representable(annotation) -> bool: def _json_default(obj: Any) -> Any: - """Fallback serializer for json.dumps — handles datetime values.""" - if isinstance(obj, datetime): + """Fallback serializer for json.dumps for types the stdlib encoder doesn't handle. + + Covers common types that appear in OpenAPI client to_dict() output. Unknown types + fall back to str() rather than raising, so CLI output is always usable. + """ + if isinstance(obj, (datetime, date)): return obj.isoformat() - raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable") + if isinstance(obj, Decimal): + return float(obj) + if isinstance(obj, UUID): + return str(obj) + if isinstance(obj, Enum): + return obj.value + return str(obj) def _format_result(result: Any) -> str: @@ -178,6 +189,7 @@ def wrapper(*args, **kwargs): "ML Pipelines & Priming", "Notes", "Utilities", + "Other", ] # Maps method names to their rich_help_panel group label for the CLI help output. @@ -256,14 +268,14 @@ def groundlight(): for name, method in sorted(vars(Groundlight).items(), key=_cli_sort_key): if callable(method) and not name.startswith("_") and name not in _CLI_EXCLUDED_METHODS: cli_func = class_func_to_cli(method) - cli_app.command(rich_help_panel=_COMMAND_GROUPS.get(name))(cli_func) + cli_app.command(rich_help_panel=_COMMAND_GROUPS.get(name, "Other"))(cli_func) for name, method in sorted(vars(ExperimentalApi).items(), key=_cli_sort_key): if not callable(method) or name.startswith("_") or name in stable_names or name in _CLI_EXCLUDED_METHODS: continue try: cli_func = class_func_to_cli(method, is_experimental=True) - experimental_app.command(rich_help_panel=_COMMAND_GROUPS.get(name))(cli_func) + experimental_app.command(rich_help_panel=_COMMAND_GROUPS.get(name, "Other"))(cli_func) except Exception as e: # pylint: disable=broad-except logger.debug("Skipping experimental CLI command '%s': %s", name, e) diff --git a/test/unit/test_cli.py b/test/unit/test_cli.py index f5ea9d07..85e208cd 100644 --- a/test/unit/test_cli.py +++ b/test/unit/test_cli.py @@ -103,6 +103,19 @@ def test_help(): assert completed_process.returncode == 0 +def test_version(): + for flag in ("--version", "-v"): + completed_process = subprocess.run( + ["groundlight", flag], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False, + ) + assert completed_process.returncode == 0 + assert re.match(r"\d+\.\d+\.\d+", completed_process.stdout.strip()) + + def test_experimental_subcommand(): # Both 'experimental' and 'exp' should resolve to the same subcommand group for alias in ("experimental", "exp"): From 75e97f13dff0ce18f742427448213343343afdb3 Mon Sep 17 00:00:00 2001 From: Tim Huff Date: Mon, 27 Apr 2026 16:31:07 -0700 Subject: [PATCH 09/10] fixing a broken test --- test/unit/test_cli.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/test/unit/test_cli.py b/test/unit/test_cli.py index 85e208cd..4783c749 100644 --- a/test/unit/test_cli.py +++ b/test/unit/test_cli.py @@ -117,17 +117,15 @@ def test_version(): def test_experimental_subcommand(): - # Both 'experimental' and 'exp' should resolve to the same subcommand group - for alias in ("experimental", "exp"): - completed_process = subprocess.run( - ["groundlight", alias, "--help"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - check=False, - ) - assert completed_process.returncode == 0 - assert "list-priming-groups" in completed_process.stdout + completed_process = subprocess.run( + ["groundlight", "exp", "--help"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False, + ) + assert completed_process.returncode == 0 + assert "list-priming-groups" in completed_process.stdout def test_bad_commands(): From 88cabec6cfed294cdb70603229305ca656242581 Mon Sep 17 00:00:00 2001 From: Tim Huff Date: Mon, 27 Apr 2026 16:35:40 -0700 Subject: [PATCH 10/10] removing unnecessary comments --- src/groundlight/cli.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/groundlight/cli.py b/src/groundlight/cli.py index 2776cb4c..d2416973 100644 --- a/src/groundlight/cli.py +++ b/src/groundlight/cli.py @@ -166,8 +166,7 @@ def wrapper(*args, **kwargs): return wrapper -# Methods to exclude from the CLI entirely. These may be too complex to express -# as CLI commands, deprecated, or otherwise not useful from a shell context. +# Methods to exclude from the CLI entirely _CLI_EXCLUDED_METHODS = { "make_action", "create_rule", @@ -181,7 +180,6 @@ def wrapper(*args, **kwargs): } # Desired display order of command groups in the CLI help output. -# Groups not listed here appear after the listed ones. _GROUP_ORDER = [ "Account", "Detectors", @@ -193,8 +191,7 @@ def wrapper(*args, **kwargs): ] # Maps method names to their rich_help_panel group label for the CLI help output. -# Applies to both stable and experimental commands. Methods not listed here fall -# into the default "Commands" panel. +# Applies to both stable and experimental commands. _COMMAND_GROUPS: dict[str, str] = { # Account "whoami": "Account",