"""Phi Workspace Cli This is the entrypoint for the `phi ws` application. """ from pathlib import Path from typing import Optional, cast, List import typer from phi.cli.console import ( print_info, print_heading, log_config_not_available_msg, log_active_workspace_not_available, print_available_workspaces, ) from phi.utils.log import logger, set_log_level_to_debug from phi.infra.type import InfraType ws_cli = typer.Typer( name="ws", short_help="Manage workspaces", help="""\b Use `phi ws [COMMAND]` to create, setup, start or stop your workspace. Run `phi ws [COMMAND] --help` for more info. """, no_args_is_help=True, add_completion=False, invoke_without_command=True, options_metavar="", subcommand_metavar="[COMMAND] [OPTIONS]", ) @ws_cli.command(short_help="Create a new workspace in the current directory.") def create( name: Optional[str] = typer.Option( None, "-n", "--name", help="Name of the new workspace.", show_default=False, ), template: Optional[str] = typer.Option( None, "-t", "--template", help="Starter template for the workspace.", show_default=False, ), url: Optional[str] = typer.Option( None, "-u", "--url", help="URL of the starter template.", show_default=False, ), print_debug_log: bool = typer.Option( False, "-d", "--debug", help="Print debug logs.", ), ): """\b Create a new workspace in the current directory using a starter template or url \b Examples: > phi ws create -t llm-app -> Create an `llm-app` in the current directory > phi ws create -t llm-app -n llm -> Create an `llm-app` named `llm` in the current directory """ if print_debug_log: set_log_level_to_debug() from phi.workspace.operator import create_workspace create_workspace(name=name, template=template, url=url) @ws_cli.command(short_help="Setup workspace from the current directory") def setup( path: Optional[str] = typer.Argument( None, help="Path to workspace [default: current directory]", show_default=False, ), print_debug_log: bool = typer.Option( False, "-d", "--debug", help="Print debug logs.", ), ): """\b Setup a workspace. This command can be run from the workspace directory OR using the workspace path. \b Examples: > `phi ws setup` -> Setup the current directory as a workspace > `phi ws setup llm-app` -> Setup the `llm-app` folder as a workspace """ if print_debug_log: set_log_level_to_debug() from phi.workspace.operator import setup_workspace # By default, we assume this command is run from the workspace directory ws_root_path: Path = Path(".").resolve() # If the user provides a path, use that to setup the workspace if path is not None: ws_root_path = Path(".").joinpath(path).resolve() setup_workspace(ws_root_path=ws_root_path) @ws_cli.command(short_help="Create resources for the active workspace") def up( resource_filter: Optional[str] = typer.Argument( None, help="Resource filter. Format - ENV:INFRA:GROUP:NAME:TYPE", ), env_filter: Optional[str] = typer.Option(None, "-e", "--env", metavar="", help="Filter the environment to deploy."), infra_filter: Optional[str] = typer.Option(None, "-i", "--infra", metavar="", help="Filter the infra to deploy."), group_filter: Optional[str] = typer.Option( None, "-g", "--group", metavar="", help="Filter resources using group name." ), name_filter: Optional[str] = typer.Option(None, "-n", "--name", metavar="", help="Filter resource using name."), type_filter: Optional[str] = typer.Option( None, "-t", "--type", metavar="", help="Filter resource using type", ), dry_run: bool = typer.Option( False, "-dr", "--dry-run", help="Print resources and exit.", ), auto_confirm: bool = typer.Option( False, "-y", "--yes", help="Skip confirmation before deploying resources.", ), print_debug_log: bool = typer.Option( False, "-d", "--debug", help="Print debug logs.", ), force: Optional[bool] = typer.Option( None, "-f", "--force", help="Force create resources where applicable.", ), pull: Optional[bool] = typer.Option( None, "-p", "--pull", help="Pull images where applicable.", ), ): """\b Create resources for the active workspace Options can be used to limit the resources to create. --env : Env (dev, stg, prd) --infra : Infra type (docker, aws, k8s) --group : Group name --name : Resource name --type : Resource type \b Options can also be provided as a RESOURCE_FILTER in the format: ENV:INFRA:GROUP:NAME:TYPE \b Examples: > `phi ws up` -> Deploy all resources > `phi ws up dev` -> Deploy all dev resources > `phi ws up prd` -> Deploy all prd resources > `phi ws up prd:aws` -> Deploy all prd aws resources > `phi ws up prd:::s3` -> Deploy prd resources matching name s3 """ if print_debug_log: set_log_level_to_debug() from phi.cli.config import PhiCliConfig from phi.workspace.config import WorkspaceConfig from phi.workspace.operator import start_workspace from phi.utils.resource_filter import parse_resource_filter phi_config: Optional[PhiCliConfig] = PhiCliConfig.from_saved_config() if not phi_config: log_config_not_available_msg() return active_ws_config: Optional[WorkspaceConfig] = phi_config.get_active_ws_config() if active_ws_config is None: log_active_workspace_not_available() avl_ws = phi_config.available_ws if avl_ws: print_available_workspaces(avl_ws) return current_path: Path = Path(".").resolve() if active_ws_config.ws_root_path != current_path and not auto_confirm: ws_at_current_path = phi_config.get_ws_config_by_path(current_path) if ws_at_current_path is not None: active_ws_dir_name = active_ws_config.ws_root_path.stem ws_at_current_path_dir_name = ws_at_current_path.ws_root_path.stem print_info( f"Workspace at the current directory ({ws_at_current_path_dir_name}) " + f"is not the Active Workspace ({active_ws_dir_name})" ) update_active_workspace = typer.confirm( f"Update active workspace to {ws_at_current_path_dir_name}", default=True ) if update_active_workspace: phi_config.set_active_ws_dir(ws_at_current_path.ws_root_path) active_ws_config = ws_at_current_path target_env: Optional[str] = None target_infra_str: Optional[str] = None target_infra: Optional[InfraType] = None target_group: Optional[str] = None target_name: Optional[str] = None target_type: Optional[str] = None # derive env:infra:name:type:group from ws_filter if resource_filter is not None: if not isinstance(resource_filter, str): raise TypeError(f"Invalid resource_filter. Expected: str, Received: {type(resource_filter)}") ( target_env, target_infra_str, target_group, target_name, target_type, ) = parse_resource_filter(resource_filter) # derive env:infra:name:type:group from command options if target_env is None and env_filter is not None and isinstance(env_filter, str): target_env = env_filter if target_infra_str is None and infra_filter is not None and isinstance(infra_filter, str): target_infra_str = infra_filter if target_group is None and group_filter is not None and isinstance(group_filter, str): target_group = group_filter if target_name is None and name_filter is not None and isinstance(name_filter, str): target_name = name_filter if target_type is None and type_filter is not None and isinstance(type_filter, str): target_type = type_filter # derive env:infra:name:type:group from defaults if target_env is None: target_env = active_ws_config.workspace_settings.default_env if active_ws_config.workspace_settings else None if target_infra_str is None: target_infra_str = ( active_ws_config.workspace_settings.default_infra if active_ws_config.workspace_settings else None ) if target_infra_str is not None: try: target_infra = cast(InfraType, InfraType(target_infra_str.lower())) except KeyError: logger.error(f"{target_infra_str} is not supported") return logger.debug("Starting workspace") logger.debug(f"\ttarget_env : {target_env}") logger.debug(f"\ttarget_infra : {target_infra}") logger.debug(f"\ttarget_group : {target_group}") logger.debug(f"\ttarget_name : {target_name}") logger.debug(f"\ttarget_type : {target_type}") logger.debug(f"\tdry_run : {dry_run}") logger.debug(f"\tauto_confirm : {auto_confirm}") logger.debug(f"\tforce : {force}") logger.debug(f"\tpull : {pull}") print_heading("Starting workspace: {}".format(str(active_ws_config.ws_root_path.stem))) start_workspace( phi_config=phi_config, ws_config=active_ws_config, target_env=target_env, target_infra=target_infra, target_group=target_group, target_name=target_name, target_type=target_type, dry_run=dry_run, auto_confirm=auto_confirm, force=force, pull=pull, ) @ws_cli.command(short_help="Delete resources for active workspace") def down( resource_filter: Optional[str] = typer.Argument( None, help="Resource filter. Format - ENV:INFRA:GROUP:NAME:TYPE", ), env_filter: str = typer.Option(None, "-e", "--env", metavar="", help="Filter the environment to shut down."), infra_filter: Optional[str] = typer.Option( None, "-i", "--infra", metavar="", help="Filter the infra to shut down." ), group_filter: Optional[str] = typer.Option( None, "-g", "--group", metavar="", help="Filter resources using group name." ), name_filter: Optional[str] = typer.Option(None, "-n", "--name", metavar="", help="Filter resource using name."), type_filter: Optional[str] = typer.Option( None, "-t", "--type", metavar="", help="Filter resource using type", ), dry_run: bool = typer.Option( False, "-dr", "--dry-run", help="Print resources and exit.", ), auto_confirm: bool = typer.Option( False, "-y", "--yes", help="Skip the confirmation before deleting resources.", ), print_debug_log: bool = typer.Option( False, "-d", "--debug", help="Print debug logs.", ), force: bool = typer.Option( None, "-f", "--force", help="Force", ), ): """\b Delete resources for the active workspace. Options can be used to limit the resources to delete. --env : Env (dev, stg, prd) --infra : Infra type (docker, aws, k8s) --group : Group name --name : Resource name --type : Resource type \b Options can also be provided as a RESOURCE_FILTER in the format: ENV:INFRA:GROUP:NAME:TYPE \b Examples: > `phi ws down` -> Delete all resources """ if print_debug_log: set_log_level_to_debug() from phi.cli.config import PhiCliConfig from phi.workspace.config import WorkspaceConfig from phi.workspace.operator import stop_workspace from phi.utils.resource_filter import parse_resource_filter phi_config: Optional[PhiCliConfig] = PhiCliConfig.from_saved_config() if not phi_config: log_config_not_available_msg() return active_ws_config: Optional[WorkspaceConfig] = phi_config.get_active_ws_config() if active_ws_config is None: log_active_workspace_not_available() avl_ws = phi_config.available_ws if avl_ws: print_available_workspaces(avl_ws) return current_path: Path = Path(".").resolve() if active_ws_config.ws_root_path != current_path and not auto_confirm: ws_at_current_path = phi_config.get_ws_config_by_path(current_path) if ws_at_current_path is not None: active_ws_dir_name = active_ws_config.ws_root_path.stem ws_at_current_path_dir_name = ws_at_current_path.ws_root_path.stem print_info( f"Workspace at the current directory ({ws_at_current_path_dir_name}) " + f"is not the Active Workspace ({active_ws_dir_name})" ) update_active_workspace = typer.confirm( f"Update active workspace to {ws_at_current_path_dir_name}", default=True ) if update_active_workspace: phi_config.set_active_ws_dir(ws_at_current_path.ws_root_path) active_ws_config = ws_at_current_path target_env: Optional[str] = None target_infra_str: Optional[str] = None target_infra: Optional[InfraType] = None target_group: Optional[str] = None target_name: Optional[str] = None target_type: Optional[str] = None # derive env:infra:name:type:group from ws_filter if resource_filter is not None: if not isinstance(resource_filter, str): raise TypeError(f"Invalid resource_filter. Expected: str, Received: {type(resource_filter)}") ( target_env, target_infra_str, target_group, target_name, target_type, ) = parse_resource_filter(resource_filter) # derive env:infra:name:type:group from command options if target_env is None and env_filter is not None and isinstance(env_filter, str): target_env = env_filter if target_infra_str is None and infra_filter is not None and isinstance(infra_filter, str): target_infra_str = infra_filter if target_group is None and group_filter is not None and isinstance(group_filter, str): target_group = group_filter if target_name is None and name_filter is not None and isinstance(name_filter, str): target_name = name_filter if target_type is None and type_filter is not None and isinstance(type_filter, str): target_type = type_filter # derive env:infra:name:type:group from defaults if target_env is None: target_env = active_ws_config.workspace_settings.default_env if active_ws_config.workspace_settings else None if target_infra_str is None: target_infra_str = ( active_ws_config.workspace_settings.default_infra if active_ws_config.workspace_settings else None ) if target_infra_str is not None: try: target_infra = cast(InfraType, InfraType(target_infra_str.lower())) except KeyError: logger.error(f"{target_infra_str} is not supported") return logger.debug("Stopping workspace") logger.debug(f"\ttarget_env : {target_env}") logger.debug(f"\ttarget_infra : {target_infra}") logger.debug(f"\ttarget_group : {target_group}") logger.debug(f"\ttarget_name : {target_name}") logger.debug(f"\ttarget_type : {target_type}") logger.debug(f"\tdry_run : {dry_run}") logger.debug(f"\tauto_confirm : {auto_confirm}") logger.debug(f"\tforce : {force}") print_heading("Stopping workspace: {}".format(str(active_ws_config.ws_root_path.stem))) stop_workspace( phi_config=phi_config, ws_config=active_ws_config, target_env=target_env, target_infra=target_infra, target_group=target_group, target_name=target_name, target_type=target_type, dry_run=dry_run, auto_confirm=auto_confirm, force=force, ) @ws_cli.command(short_help="Update resources for active workspace") def patch( resource_filter: Optional[str] = typer.Argument( None, help="Resource filter. Format - ENV:INFRA:GROUP:NAME:TYPE", ), env_filter: str = typer.Option(None, "-e", "--env", metavar="", help="Filter the environment to patch."), infra_filter: Optional[str] = typer.Option(None, "-i", "--infra", metavar="", help="Filter the infra to patch."), group_filter: Optional[str] = typer.Option( None, "-g", "--group", metavar="", help="Filter resources using group name." ), name_filter: Optional[str] = typer.Option(None, "-n", "--name", metavar="", help="Filter resource using name."), type_filter: Optional[str] = typer.Option( None, "-t", "--type", metavar="", help="Filter resource using type", ), dry_run: bool = typer.Option( False, "-dr", "--dry-run", help="Print resources and exit.", ), auto_confirm: bool = typer.Option( False, "-y", "--yes", help="Skip the confirmation before patching resources.", ), print_debug_log: bool = typer.Option( False, "-d", "--debug", help="Print debug logs.", ), force: bool = typer.Option( None, "-f", "--force", help="Force", ), pull: Optional[bool] = typer.Option( None, "-p", "--pull", help="Pull images where applicable.", ), ): """\b Update resources for the active workspace. Options can be used to limit the resources to update. --env : Env (dev, stg, prd) --infra : Infra type (docker, aws, k8s) --group : Group name --name : Resource name --type : Resource type \b Options can also be provided as a RESOURCE_FILTER in the format: ENV:INFRA:GROUP:NAME:TYPE Examples: \b > `phi ws patch` -> Patch all resources """ if print_debug_log: set_log_level_to_debug() from phi.cli.config import PhiCliConfig from phi.workspace.config import WorkspaceConfig from phi.workspace.operator import update_workspace from phi.utils.resource_filter import parse_resource_filter phi_config: Optional[PhiCliConfig] = PhiCliConfig.from_saved_config() if not phi_config: log_config_not_available_msg() return active_ws_config: Optional[WorkspaceConfig] = phi_config.get_active_ws_config() if active_ws_config is None: log_active_workspace_not_available() avl_ws = phi_config.available_ws if avl_ws: print_available_workspaces(avl_ws) return current_path: Path = Path(".").resolve() if active_ws_config.ws_root_path != current_path and not auto_confirm: ws_at_current_path = phi_config.get_ws_config_by_path(current_path) if ws_at_current_path is not None: active_ws_dir_name = active_ws_config.ws_root_path.stem ws_at_current_path_dir_name = ws_at_current_path.ws_root_path.stem print_info( f"Workspace at the current directory ({ws_at_current_path_dir_name}) " + f"is not the Active Workspace ({active_ws_dir_name})" ) update_active_workspace = typer.confirm( f"Update active workspace to {ws_at_current_path_dir_name}", default=True ) if update_active_workspace: phi_config.set_active_ws_dir(ws_at_current_path.ws_root_path) active_ws_config = ws_at_current_path target_env: Optional[str] = None target_infra_str: Optional[str] = None target_infra: Optional[InfraType] = None target_group: Optional[str] = None target_name: Optional[str] = None target_type: Optional[str] = None # derive env:infra:name:type:group from ws_filter if resource_filter is not None: if not isinstance(resource_filter, str): raise TypeError(f"Invalid resource_filter. Expected: str, Received: {type(resource_filter)}") ( target_env, target_infra_str, target_group, target_name, target_type, ) = parse_resource_filter(resource_filter) # derive env:infra:name:type:group from command options if target_env is None and env_filter is not None and isinstance(env_filter, str): target_env = env_filter if target_infra_str is None and infra_filter is not None and isinstance(infra_filter, str): target_infra_str = infra_filter if target_group is None and group_filter is not None and isinstance(group_filter, str): target_group = group_filter if target_name is None and name_filter is not None and isinstance(name_filter, str): target_name = name_filter if target_type is None and type_filter is not None and isinstance(type_filter, str): target_type = type_filter # derive env:infra:name:type:group from defaults if target_env is None: target_env = active_ws_config.workspace_settings.default_env if active_ws_config.workspace_settings else None if target_infra_str is None: target_infra_str = ( active_ws_config.workspace_settings.default_infra if active_ws_config.workspace_settings else None ) if target_infra_str is not None: try: target_infra = cast(InfraType, InfraType(target_infra_str.lower())) except KeyError: logger.error(f"{target_infra_str} is not supported") return logger.debug("Patching workspace") logger.debug(f"\ttarget_env : {target_env}") logger.debug(f"\ttarget_infra : {target_infra}") logger.debug(f"\ttarget_group : {target_group}") logger.debug(f"\ttarget_name : {target_name}") logger.debug(f"\ttarget_type : {target_type}") logger.debug(f"\tdry_run : {dry_run}") logger.debug(f"\tauto_confirm : {auto_confirm}") logger.debug(f"\tforce : {force}") logger.debug(f"\tpull : {pull}") print_heading("Updating workspace: {}".format(str(active_ws_config.ws_root_path.stem))) update_workspace( phi_config=phi_config, ws_config=active_ws_config, target_env=target_env, target_infra=target_infra, target_group=target_group, target_name=target_name, target_type=target_type, dry_run=dry_run, auto_confirm=auto_confirm, force=force, pull=pull, ) @ws_cli.command(short_help="Restart resources for active workspace") def restart( resource_filter: Optional[str] = typer.Argument( None, help="Resource filter. Format - ENV:INFRA:GROUP:NAME:TYPE", ), env_filter: str = typer.Option(None, "-e", "--env", metavar="", help="Filter the environment to restart."), infra_filter: Optional[str] = typer.Option(None, "-i", "--infra", metavar="", help="Filter the infra to restart."), group_filter: Optional[str] = typer.Option( None, "-g", "--group", metavar="", help="Filter resources using group name." ), name_filter: Optional[str] = typer.Option(None, "-n", "--name", metavar="", help="Filter resource using name."), type_filter: Optional[str] = typer.Option( None, "-t", "--type", metavar="", help="Filter resource using type", ), dry_run: bool = typer.Option( False, "-dr", "--dry-run", help="Print resources and exit.", ), auto_confirm: bool = typer.Option( False, "-y", "--yes", help="Skip the confirmation before restarting resources.", ), print_debug_log: bool = typer.Option( False, "-d", "--debug", help="Print debug logs.", ), force: bool = typer.Option( None, "-f", "--force", help="Force", ), pull: Optional[bool] = typer.Option( None, "-p", "--pull", help="Pull images where applicable.", ), ): """\b Restarts the active workspace. i.e. runs `phi ws down` and then `phi ws up`. \b Examples: > `phi ws restart` """ if print_debug_log: set_log_level_to_debug() from time import sleep down( resource_filter=resource_filter, env_filter=env_filter, group_filter=group_filter, infra_filter=infra_filter, name_filter=name_filter, type_filter=type_filter, dry_run=dry_run, auto_confirm=auto_confirm, print_debug_log=print_debug_log, force=force, ) print_info("Sleeping for 2 seconds..") sleep(2) up( resource_filter=resource_filter, env_filter=env_filter, infra_filter=infra_filter, group_filter=group_filter, name_filter=name_filter, type_filter=type_filter, dry_run=dry_run, auto_confirm=auto_confirm, print_debug_log=print_debug_log, force=force, pull=pull, ) @ws_cli.command(short_help="Prints active workspace config") def config( print_debug_log: bool = typer.Option( False, "-d", "--debug", help="Print debug logs.", ), ): """\b Prints the active workspace config \b Examples: $ `phi ws config` -> Print the active workspace config """ if print_debug_log: set_log_level_to_debug() from phi.cli.config import PhiCliConfig from phi.workspace.config import WorkspaceConfig from phi.utils.load_env import load_env phi_config: Optional[PhiCliConfig] = PhiCliConfig.from_saved_config() if not phi_config: log_config_not_available_msg() return active_ws_config: Optional[WorkspaceConfig] = phi_config.get_active_ws_config() if active_ws_config is None: log_active_workspace_not_available() avl_ws = phi_config.available_ws if avl_ws: print_available_workspaces(avl_ws) return # Load environment from .env load_env( dotenv_dir=active_ws_config.ws_root_path, ) print_info(active_ws_config.model_dump_json(include={"ws_name", "ws_root_path"}, indent=2)) @ws_cli.command(short_help="Delete workspace record") def delete( ws_name: Optional[str] = typer.Option(None, "-ws", help="Name of the workspace to delete"), all_workspaces: bool = typer.Option( False, "-a", "--all", help="Delete all workspaces from phidata", ), print_debug_log: bool = typer.Option( False, "-d", "--debug", help="Print debug logs.", ), ): """\b Deletes the workspace record from phi. NOTE: Does not delete any physical files. \b Examples: $ `phi ws delete` -> Delete the active workspace from phidata $ `phi ws delete -a` -> Delete all workspaces from phidata """ if print_debug_log: set_log_level_to_debug() from phi.cli.config import PhiCliConfig from phi.workspace.operator import delete_workspace phi_config: Optional[PhiCliConfig] = PhiCliConfig.from_saved_config() if not phi_config: log_config_not_available_msg() return ws_to_delete: List[Path] = [] # Delete workspace by name if provided if ws_name is not None: ws_config = phi_config.get_ws_config_by_dir_name(ws_name) if ws_config is None: logger.error(f"Workspace {ws_name} not found") return ws_to_delete.append(ws_config.ws_root_path) else: # Delete all workspaces if flag is set if all_workspaces: ws_to_delete = [ws.ws_root_path for ws in phi_config.available_ws if ws.ws_root_path is not None] else: # By default, we assume this command is run for the active workspace if phi_config.active_ws_dir is not None: ws_to_delete.append(Path(phi_config.active_ws_dir)) delete_workspace(phi_config, ws_to_delete)