content: #  Copyright (c) ZenML GmbH 2021. All Rights Reserved.
  #
  #  Licensed under the Apache License, Version 2.0 (the "License");
  #  you may not use this file except in compliance with the License.
  #  You may obtain a copy of the License at:
  #
  #       https://www.apache.org/licenses/LICENSE-2.0
  #
  #  Unless required by applicable law or agreed to in writing, software
  #  distributed under the License is distributed on an "AS IS" BASIS,
  #  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
  #  or implied. See the License for the specific language governing
  #  permissions and limitations under the License.
  """Functionality to generate stack component CLI commands."""
  
  import time
  from importlib import import_module
  from typing import Any, Callable, List, Optional, Tuple, cast
  from uuid import UUID
  
  import click
  from rich.markdown import Markdown
  
  from zenml.cli import utils as cli_utils
  from zenml.cli.annotator import register_annotator_subcommands
  from zenml.cli.cli import TagGroup, cli
  from zenml.cli.feature import register_feature_store_subcommands
  from zenml.cli.model_registry import register_model_registry_subcommands
  from zenml.cli.served_model import register_model_deployer_subcommands
  from zenml.cli.utils import (
      OutputFormat,
      _component_display_name,
      is_sorted_or_filtered,
      list_options,
      print_model_url,
  )
  from zenml.client import Client
  from zenml.console import console
  from zenml.enums import CliCategories, StackComponentType
  from zenml.exceptions import AuthorizationException, IllegalOperationError
  from zenml.io import fileio
  from zenml.models import (
      ComponentFilter,
      ServiceConnectorResourcesModel,
  )
  from zenml.utils import source_utils
  from zenml.utils.dashboard_utils import get_component_url
  
  
  def generate_stack_component_get_command(
      component_type: StackComponentType,
  ) -> Callable[[], None]:
      """Generates a `get` command for the specific stack component type.
  
      Args:
          component_type: Type of the component to generate the command for.
  
      Returns:
          A function that can be used as a `click` command.
      """
  
      def get_stack_component_command() -> None:
          """Prints the name of the active component."""
          client = Client()
          display_name = _component_display_name(component_type)
  
          with console.status(f"Getting the active `{display_name}`...\n"):
              active_stack = client.active_stack_model
              components = active_stack.components.get(component_type, None)
  
              if components:
                  cli_utils.declare(
                      f"Active {display_name}: '{components[0].name}'"
                  )
                  print_model_url(get_component_url(components[0]))
              else:
                  cli_utils.warning(
                      f"No {display_name} set for active stack "
                      f"('{active_stack.name}')."
                  )
  
      return get_stack_component_command
  
  
  def generate_stack_component_describe_command(
      component_type: StackComponentType,
  ) -> Callable[[str], None]:
      """Generates a `describe` command for the specific stack component type.
  
      Args:
          component_type: Type of the component to generate the command for.
  
      Returns:
          A function that can be used as a `click` command.
      """
  
      @click.argument(
          "name_id_or_prefix",
          type=str,
          required=False,
      )
      def describe_stack_component_command(name_id_or_prefix: str) -> None:
          """Prints details about the active/specified component.
  
          Args:
              name_id_or_prefix: Name or id of the component to describe.
          """
          client = Client()
          try:
              component_ = client.get_stack_component(
                  name_id_or_prefix=name_id_or_prefix,
                  component_type=component_type,
              )
          except KeyError as err:
              cli_utils.exception(err)
  
          with console.status(f"Describing component '{component_.name}'..."):
              active_component_id = None
              active_components = client.active_stack_model.components.get(
                  component_type, None
              )
              if active_components:
                  active_component_id = active_components[0].id
  
              if component_.connector:
                  connector_requirements = (
                      component_.flavor.connector_requirements
                  )
              else:
                  connector_requirements = None
  
              cli_utils.print_stack_component_configuration(
                  component=component_,
                  active_status=component_.id == active_component_id,
                  connector_requirements=connector_requirements,
              )
  
              print_model_url(get_component_url(component_))
  
      return describe_stack_component_command
  
  
  def generate_stack_component_list_command(
      component_type: StackComponentType,
  ) -> Callable[..., None]:
      """Generates a `list` command for the specific stack component type.
  
      Args:
          component_type: Type of the component to generate the command for.
  
      Returns:
          A function that can be used as a `click` command.
      """
  
      @list_options(
          ComponentFilter,
          default_columns=["active", "id", "name", "flavor", "owner"],
      )
      @click.pass_context
      def list_stack_components_command(
          ctx: click.Context,
          /,
          columns: str,
          output_format: OutputFormat,
          **kwargs: Any,
      ) -> None:
          """Prints a table of stack components.
  
          Args:
              ctx: The click context object
              columns: Columns to display in output.
              output_format: Format for output (table/json/yaml/csv/tsv).
              kwargs: Keyword arguments to filter the components.
          """
          client = Client()
          with console.status(f"Listing {component_type.plural}..."):
              kwargs["type"] = component_type
              components = client.list_stack_components(**kwargs)
  
              show_active = not is_sorted_or_filtered(ctx)
              if show_active and components.items:
                  active_stack = client.active_stack_model
                  active_component = None
                  if component_type in active_stack.components.keys():
                      active_components = active_stack.components[component_type]
                      active_component = (
                          active_components[0] if active_components else None
                      )
  
                  if active_component is not None:
                      active_component_id = active_component.id
                      if active_component_id not in {
                          c.id for c in components.items
                      }:
                          components.items.insert(0, active_component)
                      components.items.sort(
                          key=lambda c: c.id != active_component_id
                      )
                  else:
                      active_component_id = None
              else:
                  active_component_id = None
  
              cli_utils.print_page(
                  components,
                  columns,
                  output_format,
                  empty_message="No components found for the given filters.",
                  row_generator=cli_utils.generate_component_row,
                  active_id=active_component_id,
              )
  
      return list_stack_components_command
  
  
  def generate_stack_component_register_command(
      component_type: StackComponentType,
  ) -> Callable[[str, str, List[str]], None]:
      """Generates a `register` command for the specific stack component type.
  
      Args:
          component_type: Type of the component to generate the command for.
  
      Returns:
          A function that can be used as a `click` command.
      """
      display_name = _component_display_name(component_type)
  
      @click.argument(
          "name",
          type=str,
      )
      @click.option(
          "--flavor",
          "-f",
          "flavor",
          help=f"The flavor of the {display_name} to register.",
          required=True,
          type=str,
      )
      @click.option(
          "--label",
          "-l",
          "labels",
          help="Labels to be associated with the component, in the form "
          "-l key1=value1 -l key2=value2.",
          multiple=True,
      )
      @click.option(
          "--connector",
          "-c",
          "connector",
          help="Use this flag to connect this stack component to a service connector.",
          type=str,
      )
      @click.option(
          "--resource-id",
          "-r",
          "resource_id",
          help="The resource ID to use with the connector. Only required for "
          "multi-instance connectors that are not already configured with a "
          "particular resource ID.",
          required=False,
          type=str,
      )
      @click.option(
          "--secret",
          "secrets",
          help="Secrets to attach to the component.",
          type=str,
          required=False,
          multiple=True,
      )
      @click.option(
          "--env",
          "environment_variables",
          help="Environment variables to set when running on this component. "
          "Must be of the format 'KEY=VALUE'.",
          type=str,
          required=False,
          multiple=True,
      )
      @click.argument("args", nargs=-1, type=click.UNPROCESSED)
      def register_stack_component_command(
          name: str,
          flavor: str,
          args: List[str],
          labels: Optional[List[str]] = None,
          connector: Optional[str] = None,
          resource_id: Optional[str] = None,
          secrets: List[str] = [],
          environment_variables: List[str] = [],
      ) -> None:
          """Registers a stack component.
  
          Args:
              name: Name of the component to register.
              flavor: Flavor of the component to register.
              args: Additional arguments to pass to the component.
              labels: Labels to be associated with the component.
              connector: Name of the service connector to connect the component to.
              resource_id: The resource ID to use with the connector.
              secrets: Secrets to attach to the component.
              environment_variables: Environment variables to set when running
                  on this component.
          """
          client = Client()
  
          # Parse the given args
          # name is guaranteed to be set by parse_name_and_extra_arguments
          name, parsed_args = cli_utils.parse_name_and_extra_arguments(  # type: ignore[assignment]
              list(args) + [name], expand_args=True
          )
  
          parsed_labels = cli_utils.get_parsed_labels(labels)
  
          if connector:
              try:
                  client.get_service_connector(connector)
              except KeyError as err:
                  cli_utils.error(
                      f"Could not find a connector '{connector}': {str(err)}"
                  )
  
          environment = {}
          for environment_variable in environment_variables:
              key, value = environment_variable.split("=", 1)
              environment[key] = value
  
          with console.status(f"Registering {display_name} '{name}'...\n"):
              # Create a new stack component model
              component = client.create_stack_component(
                  name=name,
                  flavor=flavor,
                  component_type=component_type,
                  configuration=parsed_args,
                  labels=parsed_labels,
                  secrets=secrets,
                  environment=environment,
              )
  
              cli_utils.declare(
                  f"Successfully registered {component.type} `{component.name}`."
              )
              print_model_url(get_component_url(component))
  
          if connector:
              connect_stack_component_with_service_connector(
                  component_type=component_type,
                  name_id_or_prefix=name,
                  connector=connector,
                  interactive=False,
                  no_verify=False,
                  resource_id=resource_id,
              )
  
      return register_stack_component_command
  
  
  def generate_stack_component_update_command(
      component_type: StackComponentType,
  ) -> Callable[[str, List[str]], None]:
      """Generates an `update` command for the specific stack component type.
  
      Args:
          component_type: Type of the component to generate the command for.
  
      Returns:
          A function that can be used as a `click` command.
      """
      display_name = _component_display_name(component_type)
  
      @click.argument(
          "name_id_or_prefix",
          type=str,
          required=False,
      )
      @click.option(
          "--label",
          "-l",
          "labels",
          help="Labels to be associated with the component, in the form "
          "-l key1=value1 -l key2=value2.",
          multiple=True,
      )
      @click.option(
          "--secret",
          "secrets",
          help="Secrets to attach to the component.",
          type=str,
          required=False,
          multiple=True,
      )
      @click.option(
          "--remove-secret",
          "remove_secrets",
          help="Secrets to remove from the component.",
          type=str,
          required=False,
          multiple=True,
      )
      @click.option(
          "--env",
          "environment_variables",
          help="Environment variables to set when running on this component. "
          "Must be of the format 'KEY=VALUE'. To remove an environment variable "
          "from the component, use an empty value, e.g. 'KEY='",
          type=str,
          required=False,
          multiple=True,
      )
      @click.argument("args", nargs=-1, type=click.UNPROCESSED)
      def update_stack_component_command(
          name_id_or_prefix: Optional[str],
          args: List[str],
          labels: Optional[List[str]] = None,
          secrets: List[str] = [],
          remove_secrets: List[str] = [],
          environment_variables: List[str] = [],
      ) -> None:
          """Updates a stack component.
  
          Args:
              name_id_or_prefix: The name or id of the stack component to update.
              args: Additional arguments to pass to the update command.
              labels: Labels to be associated with the component.
              secrets: Secrets to attach to the component.
              remove_secrets: Secrets to remove from the component.
              environment_variables: Environment variables to set when running
                  on this component.
          """
          client = Client()
  
          # Parse the given args
          args = list(args)
          if name_id_or_prefix:
              args.append(name_id_or_prefix)
  
          name_or_id, parsed_args = cli_utils.parse_name_and_extra_arguments(
              args,
              expand_args=True,
              name_mandatory=False,
          )
  
          parsed_labels = cli_utils.get_parsed_labels(labels)
  
          environment = {}
          for environment_variable in environment_variables:
              key, value = environment_variable.split("=", 1)
              # Fallback to None if the value is empty so the existing environment
              # variable is removed
              environment[key] = value or None
  
          with console.status(f"Updating {display_name}...\n"):
              try:
                  updated_component = client.update_stack_component(
                      name_id_or_prefix=name_or_id,
                      component_type=component_type,
                      configuration=parsed_args,
                      labels=parsed_labels,
                      add_secrets=secrets,
                      remove_secrets=remove_secrets,
                      environment=environment,
                  )
              except KeyError as err:
                  cli_utils.exception(err)
  
              cli_utils.declare(
                  f"Successfully updated {display_name} "
                  f"`{updated_component.name}`."
              )
              print_model_url(get_component_url(updated_component))
  
      return update_stack_component_command
  
  
  def generate_stack_component_remove_attribute_command(
      component_type: StackComponentType,
  ) -> Callable[[str, List[str]], None]:
      """Generates `remove_attribute` command for a specific stack component type.
  
      Args:
          component_type: Type of the component to generate the command for.
  
      Returns:
          A function that can be used as a `click` command.
      """
      display_name = _component_display_name(component_type)
  
      @click.argument(
          "name_id_or_prefix",
          type=str,
          required=True,
      )
      @click.option(
          "--label",
          "-l",
          "labels",
          help="Labels to be removed from the component.",
          multiple=True,
      )
      @click.argument("args", nargs=-1, type=click.UNPROCESSED)
      def remove_attribute_stack_component_command(
          name_id_or_prefix: str,
          args: List[str],
          labels: Optional[List[str]] = None,
      ) -> None:
          """Removes one or more attributes from a stack component.
  
          Args:
              name_id_or_prefix: The name of the stack component to remove the
                  attribute from.
              args: Additional arguments to pass to the remove_attribute command.
              labels: Labels to be removed from the component.
          """
          client = Client()
  
          with console.status(
              f"Updating {display_name} '{name_id_or_prefix}'...\n"
          ):
              try:
                  updated_component = client.update_stack_component(
                      name_id_or_prefix=name_id_or_prefix,
                      component_type=component_type,
                      configuration={k: None for k in args},
                      labels={k: None for k in labels} if labels else None,
                  )
              except (KeyError, IllegalOperationError) as err:
                  cli_utils.exception(err)
  
              cli_utils.declare(
                  f"Successfully updated {display_name} `{name_id_or_prefix}`."
              )
              print_model_url(get_component_url(updated_component))
  
      return remove_attribute_stack_component_command
  
  
  def generate_stack_component_rename_command(
      component_type: StackComponentType,
  ) -> Callable[[str, str], None]:
      """Generates a `rename` command for the specific stack component type.
  
      Args:
          component_type: Type of the component to generate the command for.
  
      Returns:
          A function that can be used as a `click` command.
      """
      display_name = _component_display_name(component_type)
  
      @click.argument(
          "name_id_or_prefix",
          type=str,
          required=True,
      )
      @click.argument(
          "new_name",
          type=str,
          required=True,
      )
      def rename_stack_component_command(
          name_id_or_prefix: str, new_name: str
      ) -> None:
          """Rename a stack component.
  
          Args:
              name_id_or_prefix: The name of the stack component to rename.
              new_name: The new name of the stack component.
          """
          client = Client()
  
          with console.status(
              f"Renaming {display_name} '{name_id_or_prefix}'...\n"
          ):
              try:
                  updated_component = client.update_stack_component(
                      name_id_or_prefix=name_id_or_prefix,
                      component_type=component_type,
                      name=new_name,
                  )
              except (KeyError, IllegalOperationError) as err:
                  cli_utils.exception(err)
  
              cli_utils.declare(
                  f"Successfully renamed {display_name} `{name_id_or_prefix}` to"
                  f" `{new_name}`."
              )
              print_model_url(get_component_url(updated_component))
  
      return rename_stack_component_command
  
  
  def generate_stack_component_delete_command(
      component_type: StackComponentType,
  ) -> Callable[[str], None]:
      """Generates a `delete` command for the specific stack component type.
  
      Args:
          component_type: Type of the component to generate the command for.
  
      Returns:
          A function that can be used as a `click` command.
      """
      display_name = _component_display_name(component_type)
  
      @click.argument("name_id_or_prefix", type=str)
      def delete_stack_component_command(name_id_or_prefix: str) -> None:
          """Deletes a stack component.
  
          Args:
              name_id_or_prefix: The name of the stack component to delete.
          """
          client = Client()
  
          with console.status(
              f"Deleting {display_name} '{name_id_or_prefix}'...\n"
          ):
              try:
                  client.delete_stack_component(
                      name_id_or_prefix=name_id_or_prefix,
                      component_type=component_type,
                  )
              except (KeyError, IllegalOperationError) as err:
                  cli_utils.exception(err)
              cli_utils.declare(f"Deleted {display_name}: {name_id_or_prefix}")
  
      return delete_stack_component_command
  
  
  def generate_stack_component_copy_command(
      component_type: StackComponentType,
  ) -> Callable[[str, str], None]:
      """Generates a `copy` command for the specific stack component type.
  
      Args:
          component_type: Type of the component to generate the command for.
  
      Returns:
          A function that can be used as a `click` command.
      """
      display_name = _component_display_name(component_type)
  
      @click.argument(
          "source_component_name_id_or_prefix", type=str, required=True
      )
      @click.argument("target_component", type=str, required=True)
      def copy_stack_component_command(
          source_component_name_id_or_prefix: str,
          target_component: str,
      ) -> None:
          """Copies a stack component.
  
          Args:
              source_component_name_id_or_prefix: Name or id prefix of the
                                           component to copy.
              target_component: Name of the copied component.
          """
          client = Client()
  
          with console.status(
              f"Copying {display_name} "
              f"`{source_component_name_id_or_prefix}`..\n"
          ):
              try:
                  component_to_copy = client.get_stack_component(
                      name_id_or_prefix=source_component_name_id_or_prefix,
                      component_type=component_type,
                  )
              except KeyError as err:
                  cli_utils.exception(err)
  
              copied_component = client.create_stack_component(
                  name=target_component,
                  flavor=component_to_copy.flavor_name,
                  component_type=component_to_copy.type,
                  configuration=component_to_copy.configuration,
                  labels=component_to_copy.labels,
              )
              print_model_url(get_component_url(copied_component))
  
      return copy_stack_component_command
  
  
  def generate_stack_component_logs_command(
      component_type: StackComponentType,
  ) -> Callable[[str, bool], None]:
      """Generates a `logs` command for the specific stack component type.
  
      Args:
          component_type: Type of the component to generate the command for.
  
      Returns:
          A function that can be used as a `click` command.
      """
      display_name = _component_display_name(component_type)
  
      @click.argument("name_id_or_prefix", type=str, required=False)
      @click.option(
          "--follow",
          "-f",
          is_flag=True,
          help="Follow the log file instead of just displaying the current logs.",
      )
      def stack_component_logs_command(
          name_id_or_prefix: str, follow: bool = False
      ) -> None:
          """Displays stack component logs.
  
          Args:
              name_id_or_prefix: The name of the stack component to display logs
                  for.
              follow: Follow the log file instead of just displaying the current
                  logs.
          """
          client = Client()
  
          with console.status(
              f"Fetching the logs for the {display_name} "
              f"'{name_id_or_prefix}'...\n"
          ):
              try:
                  component_model = client.get_stack_component(
                      name_id_or_prefix=name_id_or_prefix,
                      component_type=component_type,
                  )
              except KeyError as err:
                  cli_utils.exception(err)
  
              from zenml.stack import StackComponent
  
              component = StackComponent.from_model(
                  component_model=component_model
              )
              log_file = component.log_file
  
              if not log_file or not fileio.exists(log_file):
                  cli_utils.warning(
                      f"Unable to find log file for {display_name} "
                      f"'{name_id_or_prefix}'."
                  )
                  return
  
          if not log_file or not fileio.exists(log_file):
              cli_utils.warning(
                  f"Unable to find log file for {display_name} "
                  f"'{component.name}'."
              )
              return
  
          if follow:
              try:
                  with open(log_file, "r") as f:
                      # seek to the end of the file
                      f.seek(0, 2)
  
                      while True:
                          line = f.readline()
                          if not line:
                              time.sleep(0.1)
                              continue
                          line = line.rstrip("\n")
                          click.echo(line)
              except KeyboardInterrupt:
                  cli_utils.declare(f"Stopped following {display_name} logs.")
          else:
              with open(log_file, "r") as f:
                  click.echo(f.read())
  
      return stack_component_logs_command
  
  
  def generate_stack_component_explain_command(
      component_type: StackComponentType,
  ) -> Callable[[], None]:
      """Generates an `explain` command for the specific stack component type.
  
      Args:
          component_type: Type of the component to generate the command for.
  
      Returns:
          A function that can be used as a `click` command.
      """
  
      def explain_stack_components_command() -> None:
          """Explains the concept of the stack component."""
          component_module = import_module(f"zenml.{component_type.plural}")
  
          if component_module.__doc__ is not None:
              md = Markdown(component_module.__doc__)
              console.print(md)
          else:
              console.print(
                  "The explain subcommand is yet not available for "
                  "this stack component. For more information, you can "
                  "visit our docs page: https://docs.zenml.io/ and "
                  "stay tuned for future releases."
              )
  
      return explain_stack_components_command
  
  
  def generate_stack_component_flavor_list_command(
      component_type: StackComponentType,
  ) -> Callable[[], None]:
      """Generates a `list` command for the flavors of a stack component.
  
      Args:
          component_type: Type of the component to generate the command for.
  
      Returns:
          A function that can be used as a `click` command.
      """
      display_name = _component_display_name(component_type)
  
      def list_stack_component_flavor_command() -> None:
          """Lists the flavors for a single type of stack component."""
          client = Client()
  
          with console.status(f"Listing {display_name} flavors`...\n"):
              flavors = client.get_flavors_by_type(component_type=component_type)
  
              cli_utils.print_flavor_list(flavors=flavors)
              cli_utils.print_page_info(flavors)
  
      return list_stack_component_flavor_command
  
  
  def generate_stack_component_flavor_register_command(
      component_type: StackComponentType,
  ) -> Callable[[str], None]:
      """Generates a `register` command for the flavors of a stack component.
  
      Args:
          component_type: Type of the component to generate the command for.
  
      Returns:
          A function that can be used as a `click` command.
      """
      command_name = component_type.value.replace("_", "-")
      display_name = _component_display_name(component_type)
  
      @click.argument(
          "source",
          type=str,
          required=True,
      )
      def register_stack_component_flavor_command(source: str) -> None:
          """Adds a flavor for a stack component type.
  
          Example:
              Let's say you create an artifact store flavor class `MyArtifactStoreFlavor`
              in the file path `flavors/my_flavor.py`. You would register it as:
  
              ```shell
              zenml artifact-store flavor register flavors.my_flavor.MyArtifactStoreFlavor
              ```
  
          Args:
              source: The source path of the flavor class in dot notation format.
          """
          client = Client()
  
          if not client.root:
              cli_utils.warning(
                  f"You're running the `zenml {command_name} flavor register` "
                  "command without a ZenML repository. Your current working "
                  "directory will be used as the source root relative to which "
                  "the `source` argument is expected. To silence this warning, "
                  "run `zenml init` at your source code root."
              )
  
          with console.status(f"Registering a new {display_name} flavor`...\n"):
              try:
                  # Register the new model
                  new_flavor = client.create_flavor(
                      source=source,
                      component_type=component_type,
                  )
              except ValueError as e:
                  source_root = source_utils.get_source_root()
  
                  cli_utils.error(
                      f"Flavor registration failed! ZenML tried loading the "
                      f"module `{source}` from path `{source_root}`. If this is "
                      "not what you expect, then please ensure you have run "
                      "`zenml init` at the root of your repository.\n\n"
                      f"Original exception: {str(e)}"
                  )
  
              cli_utils.declare(
                  f"Successfully registered new flavor '{new_flavor.name}' "
                  f"for stack component '{new_flavor.type}'."
              )
  
      return register_stack_component_flavor_command
  
  
  def generate_stack_component_flavor_describe_command(
      component_type: StackComponentType,
  ) -> Callable[[str], None]:
      """Generates a `describe` command for a single flavor of a component.
  
      Args:
          component_type: Type of the component to generate the command for.
  
      Returns:
          A function that can be used as a `click` command.
      """
      display_name = _component_display_name(component_type)
  
      @click.argument(
          "name",
          type=str,
          required=True,
      )
      def describe_stack_component_flavor_command(name: str) -> None:
          """Describes a flavor based on its config schema.
  
          Args:
              name: The name of the flavor.
          """
          client = Client()
  
          with console.status(f"Describing {display_name} flavor: {name}`...\n"):
              flavor_model = client.get_flavor_by_name_and_type(
                  name=name, component_type=component_type
              )
  
              cli_utils.describe_pydantic_object(flavor_model.config_schema)
              resources = flavor_model.connector_requirements
              if resources:
                  resources_str = f"a '{resources.resource_type}' resource"
                  cli_args = f"--resource-type {resources.resource_type}"
                  if resources.connector_type:
                      resources_str += (
                          f" provided by a '{resources.connector_type}' "
                          "connector"
                      )
                      cli_args += f"--connector-type {resources.connector_type}"
  
                  cli_utils.declare(
                      f"This flavor supports connecting to external resources "
                      f"with a Service Connector. It requires {resources_str}. "
                      "You can get a list of all available connectors and the "
                      "compatible resources that they can access by running:\n\n"
                      f"'zenml service-connector list-resources {cli_args}'\n"
                      "If no compatible Service Connectors are yet registered, "
                      "you can can register a new one by running:\n\n"
                      f"'zenml service-connector register -i'"
                  )
              else:
                  cli_utils.declare(
                      "This flavor does not support connecting to external "
                      "resources with a Service Connector."
                  )
  
      return describe_stack_component_flavor_command
  
  
  def generate_stack_component_flavor_delete_command(
      component_type: StackComponentType,
  ) -> Callable[[str], None]:
  