Source code for cursus.cli.project_cli
"""
CLI command for discovering and inspecting Cursus pipeline projects.
The ``cursus projects`` command group answers "what pipeline projects exist under this
root, and what does each contain?" — for the ``projects/*`` folders under the
Cursus dev root, or the per-pipeline folders under a consumer repo such as
BuyerAbuseModsTemplate.
A pipeline project is the standard deployable Cursus layout: a configuration directory
(``pipeline_config`` / ``pipeline_configs``) of config JSON files, alongside
``dockers``/``scripts`` and a pipeline-definition module. This command reads each
project's config JSON (no SageMaker/engine objects are constructed), so it is cheap and
safe to run anywhere.
Examples:
cursus projects list --root ./projects
cursus projects list --root ~/mods/src/BuyerAbuseModsTemplate/src/buyer_abuse_mods_template
cursus projects show --root ./projects rnr_pytorch_bedrock
cursus projects show munged_address_pytorch # locate by name (generic search)
"""
import json
import logging
from typing import Optional
import click
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger(__name__)
@click.group(name="projects")
def projects_cli():
"""Discover and inspect Cursus pipeline projects."""
pass
@projects_cli.command(name="list")
@click.option(
"--root",
"-r",
type=click.Path(exists=True, file_okay=False),
help="Directory to scan for pipeline-project subdirectories.",
)
@click.option(
"--format",
type=click.Choice(["text", "json"], case_sensitive=False),
default="text",
help="Output format.",
)
def list_projects(root: Optional[str], format: str):
"""
List pipeline projects under a root directory.
Scans each immediate subdirectory of --root and reports those that are recognizable
Cursus pipeline projects (have a pipeline_config / pipeline_configs directory).
"""
from ..core.utils import discover_pipeline_projects
if not root:
click.echo(
"❌ --root is required for 'list' (the directory to scan).", err=True
)
raise SystemExit(1)
fmt = format.lower()
try:
projects = discover_pipeline_projects(root=root)
except Exception as e:
click.echo(f"❌ Failed to discover projects under {root}: {e}", err=True)
logger.error("Project discovery error", exc_info=True)
raise SystemExit(1)
if fmt == "json":
click.echo(json.dumps([p.to_dict() for p in projects], indent=2))
return 0
if not projects:
click.echo(f"No pipeline projects found under: {root}")
return 0
click.echo(f"Found {len(projects)} pipeline project(s) under {root}:\n")
for p in projects:
# Largest config (by node count) is the representative node total.
max_nodes = max((c.node_count for c in p.config_files), default=0)
click.echo(f" {p.name}")
click.echo(
f" configs: {p.config_file_count}"
f" | nodes: {max_nodes}"
f" | config types: {len(p.distinct_config_types)}"
f" | dockers: {'yes' if p.has_dockers else 'no'}"
)
return 0
@projects_cli.command(name="show")
@click.argument("name")
@click.option(
"--root",
"-r",
type=click.Path(exists=True, file_okay=False),
help="Directory containing the project (omit to locate it by name via generic search).",
)
@click.option(
"--format",
type=click.Choice(["text", "json"], case_sensitive=False),
default="text",
help="Output format.",
)
def show_project(name: str, root: Optional[str], format: str):
"""
Show details for one pipeline project NAME (nodes, config types, modules).
With --root, looks for NAME under that directory; without --root, locates the
uniquely-named project folder via cross-deployment generic discovery.
"""
from ..core.utils import discover_pipeline_projects
fmt = format.lower()
try:
matches = discover_pipeline_projects(root=root, names=[name])
except Exception as e:
click.echo(f"❌ Failed to discover project '{name}': {e}", err=True)
logger.error("Project discovery error", exc_info=True)
raise SystemExit(1)
if not matches:
click.echo(f"❌ Pipeline project '{name}' not found.", err=True)
raise SystemExit(1)
info = matches[0]
if fmt == "json":
click.echo(json.dumps(info.to_dict(), indent=2))
return 0
click.echo(f"Project: {info.name}")
click.echo(f" path: {info.path}")
click.echo(f" config dir: {info.config_dir}")
click.echo(
f" dockers: {'yes' if info.has_dockers else 'no'} | scripts: {'yes' if info.has_scripts else 'no'}"
)
if info.pipeline_modules:
click.echo(f" pipeline modules: {', '.join(info.pipeline_modules)}")
if info.distinct_config_types:
click.echo(f" config types ({len(info.distinct_config_types)}):")
for t in info.distinct_config_types:
click.echo(f" - {t}")
click.echo(f" config files ({info.config_file_count}):")
for c in info.config_files:
if c.error:
click.echo(f" {c.file}: ⚠️ {c.error}")
else:
click.echo(f" {c.file}: {c.node_count} node(s)")
return 0
[docs]
def main():
"""Main entry point for the projects CLI."""
return projects_cli()
if __name__ == "__main__":
main()