Skip to content

Commit e0d9734

Browse files
author
Tim Huff
committed
organizing commands into groupings
1 parent de74ef7 commit e0d9734

2 files changed

Lines changed: 118 additions & 21 deletions

File tree

src/groundlight/cli.py

Lines changed: 102 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,8 @@
2727
help="Experimental commands — may change or be removed without notice.",
2828
context_settings={"help_option_names": ["-h", "--help"], "max_content_width": 800},
2929
)
30-
cli_app.add_typer(experimental_app, name="experimental")
31-
cli_app.add_typer(experimental_app, name="exp")
32-
33-
34-
_CLI_PRIMITIVE_TYPES = (str, int, float, bool)
30+
cli_app.add_typer(experimental_app, name="exp", rich_help_panel="Subcommands")
31+
cli_app.add_typer(experimental_app, name="experimental", hidden=True)
3532

3633

3734
def is_cli_supported_type(annotation):
@@ -48,7 +45,7 @@ def is_cli_representable(annotation) -> bool:
4845
Primitive scalar types, Enum subclasses, and Union types (handled separately) are considered
4946
representable. Complex types like dict, list, bytes, and custom model classes are not.
5047
"""
51-
if annotation in _CLI_PRIMITIVE_TYPES:
48+
if annotation in (str, int, float, bool):
5249
return True
5350
if isinstance(annotation, type) and issubclass(annotation, Enum):
5451
return True
@@ -146,22 +143,115 @@ def wrapper(*args, **kwargs):
146143
return wrapper
147144

148145

146+
# Methods to exclude from the CLI entirely. These may be too complex to express
147+
# as CLI commands, deprecated, or otherwise not useful from a shell context.
148+
_CLI_EXCLUDED_METHODS = {
149+
"make_action",
150+
"create_rule",
151+
"get_rule",
152+
"delete_rule",
153+
"list_rules",
154+
"delete_all_rules",
155+
"start_inspection",
156+
"update_inspection_metadata",
157+
"stop_inspection",
158+
}
159+
160+
# Desired display order of command groups in the CLI help output.
161+
# Groups not listed here appear after the listed ones.
162+
_GROUP_ORDER = [
163+
"Account",
164+
"Detectors",
165+
"Image Queries",
166+
"ML Pipelines & Priming",
167+
"Notes",
168+
"Utilities",
169+
]
170+
171+
# Maps method names to their rich_help_panel group label for the CLI help output.
172+
# Applies to both stable and experimental commands. Methods not listed here fall
173+
# into the default "Commands" panel.
174+
_COMMAND_GROUPS: dict = {
175+
# Account
176+
"whoami": "Account",
177+
"get_month_to_date_usage": "Account",
178+
# Detectors
179+
"get_detector": "Detectors",
180+
"get_detector_by_name": "Detectors",
181+
"list_detectors": "Detectors",
182+
"create_detector": "Detectors",
183+
"get_or_create_detector": "Detectors",
184+
"delete_detector": "Detectors",
185+
"create_binary_detector": "Detectors",
186+
"create_counting_detector": "Detectors",
187+
"create_multiclass_detector": "Detectors",
188+
"create_bounding_box_detector": "Detectors",
189+
"create_detector_group": "Detectors",
190+
"list_detector_groups": "Detectors",
191+
"create_roi": "Detectors",
192+
"update_detector_confidence_threshold": "Detectors",
193+
"update_detector_status": "Detectors",
194+
"update_detector_escalation_type": "Detectors",
195+
"reset_detector": "Detectors",
196+
"update_detector_name": "Detectors",
197+
"create_text_recognition_detector": "Detectors",
198+
"get_detector_evaluation": "Detectors",
199+
"get_detector_metrics": "Detectors",
200+
"download_mlbinary": "Detectors",
201+
# Image Queries
202+
"get_image_query": "Image Queries",
203+
"list_image_queries": "Image Queries",
204+
"submit_image_query": "Image Queries",
205+
"ask_confident": "Image Queries",
206+
"ask_ml": "Image Queries",
207+
"ask_async": "Image Queries",
208+
"wait_for_confident_result": "Image Queries",
209+
"wait_for_ml_result": "Image Queries",
210+
"get_image": "Image Queries",
211+
"add_label": "Image Queries",
212+
# Notes
213+
"get_notes": "Notes",
214+
"create_note": "Notes",
215+
# ML Pipelines & Priming
216+
"list_detector_pipelines": "ML Pipelines & Priming",
217+
"list_priming_groups": "ML Pipelines & Priming",
218+
"create_priming_group": "ML Pipelines & Priming",
219+
"get_priming_group": "ML Pipelines & Priming",
220+
"delete_priming_group": "ML Pipelines & Priming",
221+
# Utilities
222+
"edge_base_url": "Utilities",
223+
"get_raw_headers": "Utilities",
224+
}
225+
226+
227+
def _cli_sort_key(item: tuple) -> tuple:
228+
"""Sort key for CLI command registration that controls group and within-group ordering.
229+
230+
Commands are ordered first by their group's position in _GROUP_ORDER (ungrouped last),
231+
then alphabetically by method name within each group.
232+
"""
233+
name, _ = item
234+
group = _COMMAND_GROUPS.get(name)
235+
group_rank = _GROUP_ORDER.index(group) if group in _GROUP_ORDER else len(_GROUP_ORDER)
236+
return (group_rank, name)
237+
238+
149239
def groundlight():
150240
"""Entry point for the groundlight CLI."""
151241
try:
152242
stable_names = {n for n, m in vars(Groundlight).items() if callable(m) and not n.startswith("_")}
153243

154-
for name, method in vars(Groundlight).items():
155-
if callable(method) and not name.startswith("_"):
244+
for name, method in sorted(vars(Groundlight).items(), key=_cli_sort_key):
245+
if callable(method) and not name.startswith("_") and name not in _CLI_EXCLUDED_METHODS:
156246
cli_func = class_func_to_cli(method)
157-
cli_app.command()(cli_func)
247+
cli_app.command(rich_help_panel=_COMMAND_GROUPS.get(name))(cli_func)
158248

159-
for name, method in vars(ExperimentalApi).items():
160-
if not callable(method) or name.startswith("_") or name in stable_names:
249+
for name, method in sorted(vars(ExperimentalApi).items(), key=_cli_sort_key):
250+
if not callable(method) or name.startswith("_") or name in stable_names or name in _CLI_EXCLUDED_METHODS:
161251
continue
162252
try:
163253
cli_func = class_func_to_cli(method, is_experimental=True)
164-
experimental_app.command()(cli_func)
254+
experimental_app.command(rich_help_panel=_COMMAND_GROUPS.get(name))(cli_func)
165255
except Exception as e: # pylint: disable=broad-except
166256
logger.debug("Skipping experimental CLI command '%s': %s", name, e)
167257

test/unit/test_cli.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,9 @@ def test_detector_and_image_queries(detector_name: Callable):
4141
check=False,
4242
)
4343
assert completed_process.returncode == 0
44-
match = re.search("id='([^']+)'", completed_process.stdout)
44+
match = re.search(r'"id":\s*"([^"]+)"', completed_process.stdout)
4545
assert match is not None
4646
det_id_on_create = match.group(1)
47-
# The output of the create-detector command looks something like:
48-
# id='det_abc123'
49-
# type=<DetectorTypeEnum.detector: 'detector'>
50-
# created_at=datetime.datetime(2023, 8, 30, 18, 3, 9, 489794,
51-
# tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
52-
# name='testdetector 2023-08-31 01:03:09.039448' query='testdetector'
53-
# group_name='__DEFAULT' confidence_threshold=0.9
5447

5548
# test getting detectors
5649
completed_process = subprocess.run(
@@ -61,7 +54,7 @@ def test_detector_and_image_queries(detector_name: Callable):
6154
check=False,
6255
)
6356
assert completed_process.returncode == 0
64-
match = re.search("id='([^']+)'", completed_process.stdout)
57+
match = re.search(r'"id":\s*"([^"]+)"', completed_process.stdout)
6558
assert match is not None
6659
det_id_on_get = match.group(1)
6760
assert det_id_on_create == det_id_on_get
@@ -110,6 +103,20 @@ def test_help():
110103
assert completed_process.returncode == 0
111104

112105

106+
def test_experimental_subcommand():
107+
# Both 'experimental' and 'exp' should resolve to the same subcommand group
108+
for alias in ("experimental", "exp"):
109+
completed_process = subprocess.run(
110+
["groundlight", alias, "--help"],
111+
stdout=subprocess.PIPE,
112+
stderr=subprocess.PIPE,
113+
text=True,
114+
check=False,
115+
)
116+
assert completed_process.returncode == 0
117+
assert "list-priming-groups" in completed_process.stdout
118+
119+
113120
def test_bad_commands():
114121
completed_process = subprocess.run(
115122
["groundlight", "wat"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False

0 commit comments

Comments
 (0)