Skip to content

Commit 593edd0

Browse files
agnikagnik
authored andcommitted
feat: Add CLI commands for browsing and searching OpenML flows (models)
1 parent 4b1bdf4 commit 593edd0

File tree

2 files changed

+394
-5
lines changed

2 files changed

+394
-5
lines changed

openml/cli.py

Lines changed: 235 additions & 5 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 settings and browse models."""
22

33
from __future__ import annotations
44

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

12+
import openml
1213
from openml import config
1314

1415

@@ -327,12 +328,144 @@ def not_supported_yet(_: str) -> None:
327328
set_field_function(args.value)
328329

329330

331+
def models_list(args: argparse.Namespace) -> None:
332+
"""List flows (models) from OpenML with optional filtering."""
333+
try:
334+
flows_df = openml.flows.list_flows(
335+
offset=args.offset,
336+
size=args.size,
337+
tag=args.tag,
338+
uploader=args.uploader,
339+
)
340+
341+
if flows_df.empty:
342+
print("No flows found matching the criteria.")
343+
return
344+
345+
# Display flows in a readable format
346+
if args.format == "table":
347+
# Print as a formatted table
348+
print(f"\nFound {len(flows_df)} flow(s):\n")
349+
print(flows_df.to_string(index=False))
350+
else:
351+
# Print in a more compact format
352+
print(f"\nFound {len(flows_df)} flow(s):\n")
353+
for _, row in flows_df.iterrows():
354+
print(f"ID: {row['id']:>6} | {row['name']:<40} | Version: {row['version']}")
355+
if args.verbose:
356+
print(f" Full Name: {row['full_name']}")
357+
print(f" External Version: {row['external_version']}")
358+
print(f" Uploader: {row['uploader']}")
359+
print()
360+
361+
except Exception as e: # noqa: BLE001
362+
print(f"Error listing flows: {e}", file=sys.stderr)
363+
sys.exit(1)
364+
365+
366+
def models_info(args: argparse.Namespace) -> None:
367+
"""Display detailed information about a specific flow (model)."""
368+
try:
369+
flow_id = int(args.flow_id)
370+
flow = openml.flows.get_flow(flow_id)
371+
372+
# Display flow information using its __repr__ method
373+
print(flow)
374+
375+
# Additional information
376+
if flow.parameters:
377+
print("\nParameters:")
378+
for param_name, param_value in flow.parameters.items():
379+
meta_info = flow.parameters_meta_info.get(param_name, {})
380+
param_desc = meta_info.get("description", "No description")
381+
param_type = meta_info.get("data_type", "unknown")
382+
print(f" {param_name}: {param_value} ({param_type})")
383+
if param_desc != "No description":
384+
print(f" Description: {param_desc}")
385+
386+
if flow.components:
387+
print(f"\nComponents: {len(flow.components)}")
388+
for comp_name, comp_flow in flow.components.items():
389+
comp_id = comp_flow.flow_id if comp_flow.flow_id else "Not uploaded"
390+
print(f" {comp_name}: Flow ID {comp_id}")
391+
392+
if flow.tags:
393+
print(f"\nTags: {', '.join(flow.tags)}")
394+
395+
except ValueError:
396+
print(
397+
f"Error: '{args.flow_id}' is not a valid flow ID. Please provide a number.",
398+
file=sys.stderr,
399+
)
400+
sys.exit(1)
401+
except Exception as e: # noqa: BLE001
402+
print(f"Error retrieving flow information: {e}", file=sys.stderr)
403+
sys.exit(1)
404+
405+
406+
def models_search(args: argparse.Namespace) -> None:
407+
"""Search for flows (models) by name."""
408+
try:
409+
# Get all flows (or a reasonable subset)
410+
flows_df = openml.flows.list_flows(
411+
offset=0,
412+
size=args.max_results if args.max_results else 1000,
413+
tag=args.tag,
414+
)
415+
416+
if flows_df.empty:
417+
print("No flows found.")
418+
return
419+
420+
# Filter by search query (case-insensitive)
421+
query_lower = args.query.lower()
422+
matching_flows = flows_df[
423+
flows_df["name"].str.lower().str.contains(query_lower, na=False)
424+
| flows_df["full_name"].str.lower().str.contains(query_lower, na=False)
425+
]
426+
427+
if matching_flows.empty:
428+
print(f"No flows found matching '{args.query}'.")
429+
return
430+
431+
print(f"\nFound {len(matching_flows)} flow(s) matching '{args.query}':\n")
432+
for _, row in matching_flows.iterrows():
433+
print(f"ID: {row['id']:>6} | {row['name']:<40} | Version: {row['version']}")
434+
if args.verbose:
435+
print(f" Full Name: {row['full_name']}")
436+
print(f" External Version: {row['external_version']}")
437+
print(f" Uploader: {row['uploader']}")
438+
print()
439+
440+
except Exception as e: # noqa: BLE001
441+
print(f"Error searching flows: {e}", file=sys.stderr)
442+
sys.exit(1)
443+
444+
445+
def models(args: argparse.Namespace) -> None:
446+
"""Handle models subcommands."""
447+
subcommands = {
448+
"list": models_list,
449+
"info": models_info,
450+
"search": models_search,
451+
}
452+
453+
if args.models_subcommand in subcommands:
454+
subcommands[args.models_subcommand](args)
455+
else:
456+
print(f"Unknown models subcommand: {args.models_subcommand}")
457+
sys.exit(1)
458+
459+
330460
def main() -> None:
331-
subroutines = {"configure": configure}
461+
subroutines = {"configure": configure, "models": models}
332462

333-
parser = argparse.ArgumentParser()
334-
subparsers = parser.add_subparsers(dest="subroutine")
463+
parser = argparse.ArgumentParser(
464+
description="OpenML Python CLI - Access OpenML datasets, tasks, flows, and more.",
465+
)
466+
subparsers = parser.add_subparsers(dest="subroutine", help="Available commands")
335467

468+
# Configure command
336469
parser_configure = subparsers.add_parser(
337470
"configure",
338471
description="Set or read variables in your configuration file. For more help also see "
@@ -360,8 +493,105 @@ def main() -> None:
360493
help="The value to set the FIELD to.",
361494
)
362495

496+
# Models command
497+
parser_models = subparsers.add_parser(
498+
"models",
499+
description="Browse and search OpenML flows (models).",
500+
)
501+
502+
models_subparsers = parser_models.add_subparsers(
503+
dest="models_subcommand",
504+
help="Available models subcommands",
505+
)
506+
507+
# Models list command
508+
parser_models_list = models_subparsers.add_parser(
509+
"list",
510+
description="List flows (models) from OpenML.",
511+
)
512+
parser_models_list.add_argument(
513+
"--offset",
514+
type=int,
515+
default=None,
516+
help="Number of flows to skip (for pagination).",
517+
)
518+
parser_models_list.add_argument(
519+
"--size",
520+
type=int,
521+
default=None,
522+
help="Maximum number of flows to return.",
523+
)
524+
parser_models_list.add_argument(
525+
"--tag",
526+
type=str,
527+
default=None,
528+
help="Filter flows by tag.",
529+
)
530+
parser_models_list.add_argument(
531+
"--uploader",
532+
type=str,
533+
default=None,
534+
help="Filter flows by uploader ID.",
535+
)
536+
parser_models_list.add_argument(
537+
"--format",
538+
type=str,
539+
choices=["table", "compact"],
540+
default="compact",
541+
help="Output format (default: compact).",
542+
)
543+
parser_models_list.add_argument(
544+
"--verbose",
545+
"-v",
546+
action="store_true",
547+
help="Show detailed information for each flow.",
548+
)
549+
550+
# Models info command
551+
parser_models_info = models_subparsers.add_parser(
552+
"info",
553+
description="Display detailed information about a specific flow (model).",
554+
)
555+
parser_models_info.add_argument(
556+
"flow_id",
557+
type=str,
558+
help="The ID of the flow to display.",
559+
)
560+
561+
# Models search command
562+
parser_models_search = models_subparsers.add_parser(
563+
"search",
564+
description="Search for flows (models) by name.",
565+
)
566+
parser_models_search.add_argument(
567+
"query",
568+
type=str,
569+
help="Search query (searches in flow names).",
570+
)
571+
parser_models_search.add_argument(
572+
"--max-results",
573+
type=int,
574+
default=None,
575+
help="Maximum number of results to search through (default: 1000).",
576+
)
577+
parser_models_search.add_argument(
578+
"--tag",
579+
type=str,
580+
default=None,
581+
help="Filter flows by tag before searching.",
582+
)
583+
parser_models_search.add_argument(
584+
"--verbose",
585+
"-v",
586+
action="store_true",
587+
help="Show detailed information for each flow.",
588+
)
589+
363590
args = parser.parse_args()
364-
subroutines.get(args.subroutine, lambda _: parser.print_help())(args)
591+
if args.subroutine:
592+
subroutines.get(args.subroutine, lambda _: parser.print_help())(args)
593+
else:
594+
parser.print_help()
365595

366596

367597
if __name__ == "__main__":

0 commit comments

Comments
 (0)