Skip to content

Commit 3f3c473

Browse files
feat: add CLI commands for browsing OpenML flows (#1486)
1 parent 7feb2a3 commit 3f3c473

File tree

2 files changed

+132
-2
lines changed

2 files changed

+132
-2
lines changed

openml/cli.py

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Command Line Interface for `openml` to configure its settings."""
1+
"""Command Line Interface for `openml` to configure its settings and browse resources."""
22

33
from __future__ import annotations
44

@@ -9,6 +9,7 @@
99
from pathlib import Path
1010
from urllib.parse import urlparse
1111

12+
import openml
1213
from openml import config
1314
from openml.__version__ import __version__
1415

@@ -300,6 +301,40 @@ def configure_field( # noqa: PLR0913
300301
verbose_set(field, value)
301302

302303

304+
def list_flows_cli(args: argparse.Namespace) -> None:
305+
"""List OpenML flows with optional filtering."""
306+
df = openml.flows.list_flows(
307+
offset=args.offset,
308+
size=args.size,
309+
tag=args.tag,
310+
uploader=args.uploader,
311+
)
312+
if df.empty:
313+
print("No flows found matching the given criteria.")
314+
else:
315+
print(df.to_string())
316+
317+
318+
def info_flow_cli(args: argparse.Namespace) -> None:
319+
"""Display detailed information about a specific OpenML flow."""
320+
flow = openml.flows.get_flow(args.flow_id)
321+
print(flow)
322+
323+
324+
def handle_flows(args: argparse.Namespace) -> None:
325+
"""Dispatch flows subcommands."""
326+
actions = {
327+
"list": list_flows_cli,
328+
"info": info_flow_cli,
329+
}
330+
action = getattr(args, "flows_action", None)
331+
if action is None:
332+
# Print help when no subcommand is given
333+
args._parser_flows.print_help()
334+
else:
335+
actions[action](args)
336+
337+
303338
def configure(args: argparse.Namespace) -> None:
304339
"""Calls the right submenu(s) to edit `args.field` in the configuration file."""
305340
set_functions = {
@@ -329,7 +364,7 @@ def not_supported_yet(_: str) -> None:
329364

330365

331366
def main() -> None:
332-
subroutines = {"configure": configure}
367+
subroutines = {"configure": configure, "flows": handle_flows}
333368

334369
parser = argparse.ArgumentParser()
335370
# Add a global --version flag to display installed version and exit
@@ -368,7 +403,55 @@ def main() -> None:
368403
help="The value to set the FIELD to.",
369404
)
370405

406+
# --- flows subcommand ---
407+
parser_flows = subparsers.add_parser(
408+
"flows",
409+
description="Browse and search OpenML flows (models).",
410+
)
411+
flows_subparsers = parser_flows.add_subparsers(dest="flows_action")
412+
413+
parser_flows_list = flows_subparsers.add_parser(
414+
"list",
415+
description="List OpenML flows with optional filtering.",
416+
)
417+
parser_flows_list.add_argument(
418+
"--size",
419+
type=int,
420+
default=10,
421+
help="Maximum number of flows to return (default: 10).",
422+
)
423+
parser_flows_list.add_argument(
424+
"--offset",
425+
type=int,
426+
default=None,
427+
help="Number of flows to skip, for pagination.",
428+
)
429+
parser_flows_list.add_argument(
430+
"--tag",
431+
type=str,
432+
default=None,
433+
help="Only list flows with this tag.",
434+
)
435+
parser_flows_list.add_argument(
436+
"--uploader",
437+
type=str,
438+
default=None,
439+
help="Only list flows uploaded by this user.",
440+
)
441+
442+
parser_flows_info = flows_subparsers.add_parser(
443+
"info",
444+
description="Display detailed information about a specific flow.",
445+
)
446+
parser_flows_info.add_argument(
447+
"flow_id",
448+
type=int,
449+
help="The ID of the flow to display.",
450+
)
451+
371452
args = parser.parse_args()
453+
# Attach parser_flows so handle_flows can print help when no action is given
454+
args._parser_flows = parser_flows
372455
subroutines.get(args.subroutine, lambda _: parser.print_help())(args)
373456

374457

tests/test_openml/test_cli.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,50 @@ def test_console_script_version_prints_package_version():
4242
assert result.returncode == 0
4343
assert result.stderr == ""
4444
assert openml.__version__ in result.stdout
45+
46+
47+
@pytest.mark.production_server()
48+
def test_cli_flows_list():
49+
"""Test that 'openml flows list --size 5' returns a table of flows."""
50+
result = subprocess.run(
51+
[sys.executable, "-m", "openml.cli", "flows", "list", "--size", "5"],
52+
stdout=subprocess.PIPE,
53+
stderr=subprocess.PIPE,
54+
text=True,
55+
check=False,
56+
)
57+
58+
assert result.returncode == 0
59+
# Output should contain at least one flow entry with a name column
60+
assert "name" in result.stdout.lower() or len(result.stdout.strip()) > 0
61+
62+
63+
@pytest.mark.production_server()
64+
def test_cli_flows_info():
65+
"""Test that 'openml flows info <id>' prints flow details."""
66+
result = subprocess.run(
67+
[sys.executable, "-m", "openml.cli", "flows", "info", "5"],
68+
stdout=subprocess.PIPE,
69+
stderr=subprocess.PIPE,
70+
text=True,
71+
check=False,
72+
)
73+
74+
assert result.returncode == 0
75+
# The output should contain the flow name or ID
76+
assert "Flow Name" in result.stdout or "5" in result.stdout
77+
78+
79+
def test_cli_flows_no_action_prints_help():
80+
"""Test that 'openml flows' with no subcommand prints help text."""
81+
result = subprocess.run(
82+
[sys.executable, "-m", "openml.cli", "flows"],
83+
stdout=subprocess.PIPE,
84+
stderr=subprocess.PIPE,
85+
text=True,
86+
check=False,
87+
)
88+
89+
assert result.returncode == 0
90+
# Should print help text mentioning available subcommands
91+
assert "list" in result.stdout or "info" in result.stdout

0 commit comments

Comments
 (0)