Skip to content

Commit

Permalink
[components] Change CLI from verb-noun to noun-verb (#26623)
Browse files Browse the repository at this point in the history
## Summary & Motivation

Change the CLI from `dg <verb> <noun>` to `dg <noun> <verb>`. `dg
generate component` becomes `dg component generate`, etc. Made an effort
to find all instances of `dg` CLI calls in comments/docstrings/code and
update accordingly.

## How I Tested These Changes

Modified the unit tests, manually playing around with updated CLI.
  • Loading branch information
smackesey authored Dec 20, 2024
1 parent 2cea1b3 commit c78baa1
Show file tree
Hide file tree
Showing 14 changed files with 844 additions and 752 deletions.
14 changes: 8 additions & 6 deletions python_modules/libraries/dagster-dg/dagster_dg/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
import click

from dagster_dg.cache import DgCache
from dagster_dg.cli.generate import generate_cli
from dagster_dg.cli.info import info_cli
from dagster_dg.cli.list import list_cli
from dagster_dg.cli.code_location import code_location_group
from dagster_dg.cli.component import component_group
from dagster_dg.cli.component_type import component_type_group
from dagster_dg.cli.deployment import deployment_group
from dagster_dg.config import DgConfig, set_config_on_cli_context
from dagster_dg.context import (
DgContext,
Expand All @@ -22,9 +23,10 @@

def create_dg_cli():
commands = {
"generate": generate_cli,
"info": info_cli,
"list": list_cli,
"code-location": code_location_group,
"deployment": deployment_group,
"component": component_group,
"component-type": component_type_group,
}

# Defaults are defined on the DgConfig object.
Expand Down
116 changes: 116 additions & 0 deletions python_modules/libraries/dagster-dg/dagster_dg/cli/code_location.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import os
import sys
from pathlib import Path
from typing import Optional

import click

from dagster_dg.context import DeploymentDirectoryContext, DgContext, is_inside_deployment_directory
from dagster_dg.generate import generate_code_location
from dagster_dg.utils import DgClickCommand, DgClickGroup


@click.group(name="code-location", cls=DgClickGroup)
def code_location_group():
"""Commands for operating code location directories."""


# ########################
# ##### GENERATE
# ########################


@code_location_group.command(name="generate", cls=DgClickCommand)
@click.argument("name", type=str)
@click.option(
"--use-editable-dagster",
type=str,
flag_value="TRUE",
is_flag=False,
default=None,
help=(
"Install Dagster package dependencies from a local Dagster clone. Accepts a path to local Dagster clone root or"
" may be set as a flag (no value is passed). If set as a flag,"
" the location of the local Dagster clone will be read from the `DAGSTER_GIT_REPO_DIR` environment variable."
),
)
@click.option(
"--skip-venv",
is_flag=True,
default=False,
help="Do not create a virtual environment for the code location.",
)
@click.pass_context
def code_location_generate_command(
cli_context: click.Context, name: str, use_editable_dagster: Optional[str], skip_venv: bool
) -> None:
"""Generate a Dagster code location file structure and a uv-managed virtual environment scoped
to the code location.
This command can be run inside or outside of a deployment directory. If run inside a deployment,
the code location will be created within the deployment directory's code location directory.
The code location file structure defines a Python package with some pre-existing internal
structure:
├── <name>
│ ├── __init__.py
│ ├── components
│ ├── definitions.py
│ └── lib
│ └── __init__.py
├── <name>_tests
│ └── __init__.py
└── pyproject.toml
The `<name>.components` directory holds components (which can be created with `dg generate
component`). The `<name>.lib` directory holds custom component types scoped to the code
location (which can be created with `dg component-type generate`).
"""
dg_context = DgContext.from_cli_context(cli_context)
if is_inside_deployment_directory(Path.cwd()):
context = DeploymentDirectoryContext.from_path(Path.cwd(), dg_context)
if context.has_code_location(name):
click.echo(click.style(f"A code location named {name} already exists.", fg="red"))
sys.exit(1)
code_location_path = context.code_location_root_path / name
else:
code_location_path = Path.cwd() / name

if use_editable_dagster == "TRUE":
if not os.environ.get("DAGSTER_GIT_REPO_DIR"):
click.echo(
click.style(
"The `--use-editable-dagster` flag requires the `DAGSTER_GIT_REPO_DIR` environment variable to be set.",
fg="red",
)
)
sys.exit(1)
editable_dagster_root = os.environ["DAGSTER_GIT_REPO_DIR"]
elif use_editable_dagster: # a string value was passed
editable_dagster_root = use_editable_dagster
else:
editable_dagster_root = None

generate_code_location(code_location_path, dg_context, editable_dagster_root, skip_venv)


# ########################
# ##### LIST
# ########################


@code_location_group.command(name="list", cls=DgClickCommand)
@click.pass_context
def code_location_list_command(cli_context: click.Context) -> None:
"""List code locations in the current deployment."""
dg_context = DgContext.from_cli_context(cli_context)
if not is_inside_deployment_directory(Path.cwd()):
click.echo(
click.style("This command must be run inside a Dagster deployment directory.", fg="red")
)
sys.exit(1)

context = DeploymentDirectoryContext.from_path(Path.cwd(), dg_context)
for code_location in context.get_code_location_names():
click.echo(code_location)
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import os
import sys
from pathlib import Path
from typing import Any, Mapping, Optional
Expand All @@ -9,17 +8,10 @@
from dagster_dg.component import RemoteComponentType
from dagster_dg.context import (
CodeLocationDirectoryContext,
DeploymentDirectoryContext,
DgContext,
is_inside_code_location_directory,
is_inside_deployment_directory,
)
from dagster_dg.generate import (
generate_code_location,
generate_component_instance,
generate_component_type,
generate_deployment,
)
from dagster_dg.generate import generate_component_instance
from dagster_dg.utils import (
DgClickCommand,
DgClickGroup,
Expand All @@ -28,139 +20,25 @@
)


@click.group(name="generate", cls=DgClickGroup)
def generate_cli() -> None:
"""Commands for generating Dagster components and related entities."""


@generate_cli.command(name="deployment", cls=DgClickCommand)
@click.argument("path", type=Path)
def generate_deployment_command(path: Path) -> None:
"""Generate a Dagster deployment file structure.
The deployment file structure includes a directory for code locations and configuration files
for deploying to Dagster Plus.
"""
dir_abspath = os.path.abspath(path)
if os.path.exists(dir_abspath):
click.echo(
click.style(f"A file or directory at {dir_abspath} already exists. ", fg="red")
+ "\nPlease delete the contents of this path or choose another location."
)
sys.exit(1)
generate_deployment(path)


@generate_cli.command(name="code-location", cls=DgClickCommand)
@click.argument("name", type=str)
@click.option(
"--use-editable-dagster",
type=str,
flag_value="TRUE",
is_flag=False,
default=None,
help=(
"Install Dagster package dependencies from a local Dagster clone. Accepts a path to local Dagster clone root or"
" may be set as a flag (no value is passed). If set as a flag,"
" the location of the local Dagster clone will be read from the `DAGSTER_GIT_REPO_DIR` environment variable."
),
)
@click.option(
"--skip-venv",
is_flag=True,
default=False,
help="Do not create a virtual environment for the code location.",
)
@click.pass_context
def generate_code_location_command(
cli_context: click.Context, name: str, use_editable_dagster: Optional[str], skip_venv: bool
) -> None:
"""Generate a Dagster code location file structure and a uv-managed virtual environment scoped
to the code location.
This command can be run inside or outside of a deployment directory. If run inside a deployment,
the code location will be created within the deployment directory's code location directory.
The code location file structure defines a Python package with some pre-existing internal
structure:
├── <name>
│ ├── __init__.py
│ ├── components
│ ├── definitions.py
│ └── lib
│ └── __init__.py
├── <name>_tests
│ └── __init__.py
└── pyproject.toml
The `<name>.components` directory holds components (which can be created with `dg generate
component`). The `<name>.lib` directory holds custom component types scoped to the code
location (which can be created with `dg generate component-type`).
"""
dg_context = DgContext.from_cli_context(cli_context)
if is_inside_deployment_directory(Path.cwd()):
context = DeploymentDirectoryContext.from_path(Path.cwd(), dg_context)
if context.has_code_location(name):
click.echo(click.style(f"A code location named {name} already exists.", fg="red"))
sys.exit(1)
code_location_path = context.code_location_root_path / name
else:
code_location_path = Path.cwd() / name

if use_editable_dagster == "TRUE":
if not os.environ.get("DAGSTER_GIT_REPO_DIR"):
click.echo(
click.style(
"The `--use-editable-dagster` flag requires the `DAGSTER_GIT_REPO_DIR` environment variable to be set.",
fg="red",
)
)
sys.exit(1)
editable_dagster_root = os.environ["DAGSTER_GIT_REPO_DIR"]
elif use_editable_dagster: # a string value was passed
editable_dagster_root = use_editable_dagster
else:
editable_dagster_root = None

generate_code_location(code_location_path, dg_context, editable_dagster_root, skip_venv)

@click.group(name="component", cls=DgClickGroup)
def component_group():
"""Commands for operating on components."""

@generate_cli.command(name="component-type", cls=DgClickCommand)
@click.argument("name", type=str)
@click.pass_context
def generate_component_type_command(cli_context: click.Context, name: str) -> None:
"""Generate a scaffold of a custom Dagster component type.
This command must be run inside a Dagster code location directory. The component type scaffold
will be generated in submodule `<code_location_name>.lib.<name>`.
"""
dg_context = DgContext.from_cli_context(cli_context)
if not is_inside_code_location_directory(Path.cwd()):
click.echo(
click.style(
"This command must be run inside a Dagster code location directory.", fg="red"
)
)
sys.exit(1)
context = CodeLocationDirectoryContext.from_path(Path.cwd(), dg_context)
full_component_name = f"{context.name}.{name}"
if context.has_component_type(full_component_name):
click.echo(click.style(f"A component type named `{name}` already exists.", fg="red"))
sys.exit(1)

generate_component_type(context, name)
# ########################
# ##### GENERATE
# ########################


# The `dg generate component` command is special because its subcommands are dynamically generated
# The `dg component generate` command is special because its subcommands are dynamically generated
# from the registered component types in the code location. Because the registered component types
# depend on the built-in component library we are using, we cannot resolve them until we have the
# built-in component library, which can be set via a global option, e.g.:
#
# dg --builtin-component-lib dagster_components.test ...
#
# To handle this, we define a custom click.Group subclass that loads the commands on demand.
class GenerateComponentGroup(DgClickGroup):
class ComponentGenerateGroup(DgClickGroup):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._commands_defined = False
Expand Down Expand Up @@ -189,16 +67,16 @@ def _define_commands(self, cli_context: click.Context) -> None:

context = CodeLocationDirectoryContext.from_path(Path.cwd(), app_context)
for key, component_type in context.iter_component_types():
command = _create_generate_component_subcommand(key, component_type)
command = _create_component_generate_subcommand(key, component_type)
self.add_command(command)


@generate_cli.group(name="component", cls=GenerateComponentGroup)
def generate_component_group() -> None:
@component_group.group(name="generate", cls=ComponentGenerateGroup)
def component_generate_group() -> None:
"""Generate a scaffold of a Dagster component."""


def _create_generate_component_subcommand(
def _create_component_generate_subcommand(
component_key: str, component_type: RemoteComponentType
) -> DgClickCommand:
@click.command(name=component_key, cls=DgClickCommand)
Expand Down Expand Up @@ -226,11 +104,11 @@ def generate_component_command(
(1) Passing a single --json-params option with a JSON string of parameters. For example:
dg generate component foo.bar my_component --json-params '{{"param1": "value", "param2": "value"}}'`.
dg component generate foo.bar my_component --json-params '{{"param1": "value", "param2": "value"}}'`.
(2) Passing each parameter as an option. For example:
dg generate component foo.bar my_component --param1 value1 --param2 value2`
dg component generate foo.bar my_component --param1 value1 --param2 value2`
It is an error to pass both --json-params and key-value pairs as options.
"""
Expand Down Expand Up @@ -299,3 +177,26 @@ def generate_component_command(
generate_component_command.params.append(option)

return generate_component_command


# ########################
# ##### LIST
# ########################


@component_group.command(name="list", cls=DgClickCommand)
@click.pass_context
def component_list_command(cli_context: click.Context) -> None:
"""List Dagster component instances defined in the current code location."""
dg_context = DgContext.from_cli_context(cli_context)
if not is_inside_code_location_directory(Path.cwd()):
click.echo(
click.style(
"This command must be run inside a Dagster code location directory.", fg="red"
)
)
sys.exit(1)

context = CodeLocationDirectoryContext.from_path(Path.cwd(), dg_context)
for component_name in context.get_component_instance_names():
click.echo(component_name)
Loading

0 comments on commit c78baa1

Please sign in to comment.