Repository: skypilot-org/skypilot
Pull request: Reformat `sky status` codepath and minor fixes
This solves #716 as well moving `REGION` next to `RESOURCE` as suggested by @concretevitamin 

Examples:

<img width="853" alt="image" src="https://user-images.githubusercontent.com/8518898/161877067-4d4947f9-f7e2-4356-862d-53b2b2839c96.png">

<img width="1038" alt="image" src="https://user-images.githubusercontent.com/8518898/161877193-b4aa70fc-eccb-416a-be52-7b64bf912cf8.png">

Also, a refactor of `sky status` to contain most of the code in `sky/utils/cli_utils` Although this increases the code hierarchy I feel like the `cli.py` file was getting quite large and it's worth for performance & organization to split it into utils for each function.

Problem statement:
UX: `sky status` should hint about --refresh if there are clusters with autostop scheduled
Users may see an UP cluster with AUTOSTOP column set, and are confused by it "why is it still showing as UP even though the instance has stopped"?

Proposal:
- Detect if `sky status` shows any UP cluster with AUTOSTOP scheduled 
- If so, output a hint line at the bottom, along the lines of "You have X clusters with autostop scheduled. Refresh statuses with: `sky status --refresh`."

Hints from issue discussion:

Commit message: refactor sky status codepath and minor fixes

Patch to review:
diff --git a/format.sh b/format.sh
index 8954d68ebf2..79d2b805ee8 100755
--- a/format.sh
+++ b/format.sh
@@ -89,7 +89,10 @@ else
 fi
 
 # Run Pylint
+echo 'Sky Pylint:'
 pylint --load-plugins pylint_quotes sky
+echo 'Sky Utils Pylint:'
+pylint --load-plugins pylint_quotes sky/utils
 
 if ! git diff --quiet &>/dev/null; then
     echo 'Reformatted files. Please review and stage the changes.'
diff --git a/sky/cli.py b/sky/cli.py
index 7f5211446bf..0b75a0f3fd9 100644
--- a/sky/cli.py
+++ b/sky/cli.py
@@ -33,14 +33,12 @@
 import os
 import shlex
 import sys
-import time
 import typing
 from typing import Any, List, Optional, Tuple
 import yaml
 
 import click
 import colorama
-import pendulum
 from rich import progress as rich_progress
 
 import sky
@@ -56,6 +54,9 @@
 from sky.skylet import job_lib
 from sky.skylet import util_lib
 
+from sky.utils.cli_utils import cli_utils
+from sky.utils.cli_utils import status_utils
+
 if typing.TYPE_CHECKING:
     from sky.backends import backend as backend_lib
 
@@ -83,22 +84,6 @@
 }
 
 
-def _truncate_long_string(s: str, max_length: int = 35) -> str:
-    if len(s) <= max_length:
-        return s
-    splits = s.split(' ')
-    if len(splits[0]) > max_length:
-        return splits[0][:max_length] + '...'  # Use '…'?
-    # Truncate on word boundary.
-    i = 0
-    total = 0
-    for i, part in enumerate(splits):
-        total += len(part)
-        if total >= max_length:
-            break
-    return ' '.join(splits[:i]) + ' ...'
-
-
 def _get_cloud(cloud: str) -> Optional[clouds.Cloud]:
     """Check if cloud is registered and return cloud object."""
     if cloud is not None and cloud not in clouds.CLOUD_REGISTRY:
@@ -786,15 +771,6 @@ def exec(
     sky.exec(dag, backend=backend, cluster_name=cluster, detach_run=detach_run)
 
 
-def _readable_time_duration(start_time: int):
-    duration = pendulum.now().subtract(seconds=time.time() - start_time)
-    diff = duration.diff_for_humans()
-    diff = diff.replace('second', 'sec')
-    diff = diff.replace('minute', 'min')
-    diff = diff.replace('hour', 'hr')
-    return diff
-
-
 @cli.command()
 @click.option('--all',
               '-a',
@@ -810,78 +786,7 @@ def _readable_time_duration(start_time: int):
               help='Query remote clusters for their latest autostop settings.')
 def status(all: bool, refresh: bool):  # pylint: disable=redefined-builtin
     """Show clusters."""
-    # TODO(zhwu): Update the information for auto-stop clusters.
-    show_all = all
-    clusters_status = backend_utils.get_clusters(refresh)
-    columns = [
-        'NAME',
-        'LAUNCHED',
-        'RESOURCES',
-        'STATUS',
-        'AUTOSTOP',
-        'COMMAND',
-    ]
-
-    if all:
-        columns.extend([
-            'HOURLY_PRICE',
-            'REGION',
-        ])
-
-    cluster_table = util_lib.create_table(columns)
-
-    for cluster_status in clusters_status:
-        launched_at = cluster_status['launched_at']
-        handle = cluster_status['handle']
-        resources_str = '<initializing>'
-        if isinstance(handle, backends.LocalDockerBackend.ResourceHandle):
-            resources_str = 'docker'
-        elif isinstance(handle, backends.CloudVmRayBackend.ResourceHandle):
-            if (handle.launched_nodes is not None and
-                    handle.launched_resources is not None):
-                launched_resource_str = str(handle.launched_resources)
-                if not show_all:
-                    launched_resource_str = _truncate_long_string(
-                        launched_resource_str)
-                resources_str = (f'{handle.launched_nodes}x '
-                                 f'{launched_resource_str}')
-        else:
-            raise ValueError(f'Unknown handle type {type(handle)} encountered.')
-        autostop_str = '-'
-        if cluster_status['autostop'] >= 0:
-            # TODO(zhwu): check the status of the autostop cluster.
-            autostop_str = str(cluster_status['autostop']) + ' min'
-        row = [
-            # NAME
-            cluster_status['name'],
-            # LAUNCHED
-            _readable_time_duration(launched_at),
-            # RESOURCES
-            resources_str,
-            # STATUS
-            cluster_status['status'].value,
-            # AUTOSTOP
-            autostop_str,
-            # COMMAND
-            cluster_status['last_use']
-            if show_all else _truncate_long_string(cluster_status['last_use']),
-        ]
-        if all:
-            hourly_cost = handle.launched_resources.get_cost(3600) \
-                * handle.launched_nodes
-            price_str = f'$ {hourly_cost:.3f}'
-            region = handle.get_cluster_region()
-            row.extend([
-                # HOURLY PRICE
-                price_str,
-                # REGION
-                region,
-            ])
-        cluster_table.add_row(row)
-    if clusters_status:
-        click.echo(cluster_table)
-    else:
-        click.echo('No existing clusters.')
+    status_utils.show_status_table(all, refresh)
 
 
 @cli.command()
@@ -1854,7 +1759,7 @@ def storage_ls():
             # NAME
             row['name'],
             # LAUNCHED
-            _readable_time_duration(launched_at),
+            cli_utils.readable_time_duration(launched_at),
             # CLOUDS
             ', '.join([s.value for s in row['handle'].sky_stores.keys()]),
             # COMMAND
diff --git a/sky/utils/cli_utils/cli_utils.py b/sky/utils/cli_utils/cli_utils.py
new file mode 100644
index 00000000000..fb54b7f2f43
--- /dev/null
+++ b/sky/utils/cli_utils/cli_utils.py
@@ -0,0 +1,28 @@
+"""Utilities shared by CLI functions."""
+import pendulum
+import time
+
+
+def readable_time_duration(start_time: int):
+    duration = pendulum.now().subtract(seconds=time.time() - start_time)
+    diff = duration.diff_for_humans()
+    diff = diff.replace('second', 'sec')
+    diff = diff.replace('minute', 'min')
+    diff = diff.replace('hour', 'hr')
+    return diff
+
+
+def truncate_long_string(s: str, max_length: int = 35) -> str:
+    if len(s) <= max_length:
+        return s
+    splits = s.split(' ')
+    if len(splits[0]) > max_length:
+        return splits[0][:max_length] + '...'  # Use '…'?
+    # Truncate on word boundary.
+    i = 0
+    total = 0
+    for i, part in enumerate(splits):
+        total += len(part)
+        if total >= max_length:
+            break
+    return ' '.join(splits[:i]) + ' ...'
diff --git a/sky/utils/cli_utils/status_utils.py b/sky/utils/cli_utils/status_utils.py
new file mode 100644
index 00000000000..79aa6ba72e4
--- /dev/null
+++ b/sky/utils/cli_utils/status_utils.py
@@ -0,0 +1,118 @@
+"""Utilities for sky status."""
+import click
+
+from sky import backends
+from sky.backends import backend_utils
+from sky.utils.cli_utils import cli_utils
+from sky.skylet import util_lib
+
+
+class StatusColumn:
+    """One column of the displayed cluster table"""
+
+    def __init__(self,
+                 name: str,
+                 calc_func,
+                 trunc_length: int = 0,
+                 only_all: bool = False):
+        self.name = name
+        self.calc_func = calc_func
+        self.trunc_length = trunc_length
+        self.only_all = only_all
+
+    def calc(self, cluster_status):
+        val = self.calc_func(cluster_status)
+        if self.trunc_length != 0:
+            val = cli_utils.truncate_long_string(str(val), self.trunc_length)
+
+
+def show_status_table(show_all: bool, refresh: bool):
+    """Compute cluster table values and display."""
+    # TODO(zhwu): Update the information for auto-stop clusters.
+    clusters_status = backend_utils.get_clusters(refresh)
+
+    status_columns = [
+        StatusColumn('NAME', _get_name),
+        StatusColumn('LAUNCHED', _get_launched),
+        StatusColumn('RESOURCES', _get_resources),
+        StatusColumn('REGION', _get_region, only_all=True),
+        StatusColumn('STATUS', _get_status),
+        StatusColumn('AUTOSTOP', _get_autostop),
+        StatusColumn('COMMAND',
+                     _get_command,
+                     trunc_length=35 if not show_all else 0),
+        StatusColumn('HOURLY_PRICE', _get_price, only_all=True)
+    ]
+
+    columns = []
+    for status_column in status_columns:
+        if not status_column.only_all or show_all:
+            columns.append(status_column.name)
+    cluster_table = util_lib.create_table(columns)
+
+    pending_autostop = 0
+    for cluster_status in clusters_status:
+        row = []
+        for status_column in status_columns:
+            if not status_column.only_all or show_all:
+                row.append(status_column.calc_func(cluster_status))
+        cluster_table.add_row(row)
+        pending_autostop += _check_pending_autostop(cluster_status)
+
+    if clusters_status:
+        click.echo(cluster_table)
+        if pending_autostop:
+            click.echo(
+                '\n'
+                f'You have {pending_autostop} clusters with autostop scheduled.'
+                ' Refresh statuses with: `sky status --refresh`.')
+    else:
+        click.echo('No existing clusters.')
+
+_get_name = lambda cluster_status: \
+    cluster_status['name']
+_get_launched = lambda cluster_status: \
+    cli_utils.readable_time_duration(cluster_status['launched_at'])
+_get_region = lambda clusters_status: \
+    clusters_status['handle'].get_cluster_region()
+_get_status = lambda cluster_status: \
+    cluster_status['status'].value
+_get_command = lambda cluster_status: \
+    cluster_status['last_use']
+
+
+def _get_resources(cluster_status):
+    handle = cluster_status['handle']
+    resources_str = '<initializing>'
+    if isinstance(handle, backends.LocalDockerBackend.ResourceHandle):
+        resources_str = 'docker'
+    elif isinstance(handle, backends.CloudVmRayBackend.ResourceHandle):
+        if (handle.launched_nodes is not None and
+                handle.launched_resources is not None):
+            launched_resource_str = str(handle.launched_resources)
+            resources_str = (f'{handle.launched_nodes}x '
+                             f'{launched_resource_str}')
+    else:
+        raise ValueError(f'Unknown handle type {type(handle)} encountered.')
+    return resources_str
+
+
+def _get_autostop(cluster_status):
+    autostop_str = '-'
+    if cluster_status['autostop'] >= 0:
+        # TODO(zhwu): check the status of the autostop cluster.
+        autostop_str = str(cluster_status['autostop']) + ' min'
+    return autostop_str
+
+
+def _get_price(cluster_status):
+    handle = cluster_status['handle']
+    hourly_cost = handle.launched_resources.get_cost(3600) \
+                * handle.launched_nodes
+    price_str = f'$ {hourly_cost:.3f}'
+    return price_str
+
+
+def _check_pending_autostop(cluster_status):
+    return _get_autostop(cluster_status) != '-' and _get_status(
+        cluster_status) == 'UP'


Code review comments:
diff_hunk: @@ -0,0 +1,118 @@
+"""Utilities for sky status."""
+import click
+
+from sky import backends
+from sky.backends import backend_utils
+from sky.utils.cli_utils import cli_utils
+from sky.skylet import util_lib
+
+
+class StatusColumn:
+    """One column of the displayed cluster table"""
+
+    def __init__(self,
+                 name: str,
+                 calc_func,
+                 trunc_length: int = 0,
+                 only_all: bool = False):
+        self.name = name
+        self.calc_func = calc_func
+        self.trunc_length = trunc_length
+        self.only_all = only_all
+
+    def calc(self, cluster_status):
+        val = self.calc_func(cluster_status)
+        if self.trunc_length != 0:
+            val = cli_utils.truncate_long_string(str(val), self.trunc_length)
+
+
+def show_status_table(show_all: bool, refresh: bool):
+    """Compute cluster table values and display."""
+    # TODO(zhwu): Update the information for auto-stop clusters.
+    clusters_status = backend_utils.get_clusters(refresh)
+
+    status_columns = [
+        StatusColumn('NAME', _get_name),
+        StatusColumn('LAUNCHED', _get_launched),
+        StatusColumn('RESOURCES', _get_resources),
+        StatusColumn('REGION', _get_region, only_all=True),
+        StatusColumn('STATUS', _get_status),
+        StatusColumn('AUTOSTOP', _get_autostop),
+        StatusColumn('COMMAND',
+                     _get_command,
+                     trunc_length=35 if not show_all else 0),
+        StatusColumn('HOURLY_PRICE', _get_price, only_all=True)
+    ]
+
+    columns = []
+    for status_column in status_columns:
+        if not status_column.only_all or show_all:
+            columns.append(status_column.name)
+    cluster_table = util_lib.create_table(columns)
+
+    pending_autostop = 0
+    for cluster_status in clusters_status:
+        row = []
+        for status_column in status_columns:
+            if not status_column.only_all or show_all:
+                row.append(status_column.calc_func(cluster_status))
+        cluster_table.add_row(row)
+        pending_autostop += _check_pending_autostop(cluster_status)
+
+    if clusters_status:
+        click.echo(cluster_table)
+        if pending_autostop:
+            click.echo(
+                '\n'
+                f'You have {pending_autostop} clusters with autostop scheduled.'
+                ' Refresh statuses with: `sky status --refresh`.')
+    else:
+        click.echo('No existing clusters.')
+
+_get_name = lambda cluster_status: \
+    cluster_status['name']
+_get_launched = lambda cluster_status: \
+    cli_utils.readable_time_duration(cluster_status['launched_at'])
+_get_region = lambda clusters_status: \
+    clusters_status['handle'].get_cluster_region()
+_get_status = lambda cluster_status: \
+    cluster_status['status'].value
+_get_command = lambda cluster_status: \
+    cluster_status['last_use']
path: sky/utils/cli_utils/status_utils.py
text: @user1:
are backslashes necessary? or can we use parens + yapf?

diff_hunk: @@ -0,0 +1,118 @@
+"""Utilities for sky status."""
+import click
+
+from sky import backends
+from sky.backends import backend_utils
+from sky.utils.cli_utils import cli_utils
+from sky.skylet import util_lib
+
+
+class StatusColumn:
+    """One column of the displayed cluster table"""
+
+    def __init__(self,
+                 name: str,
+                 calc_func,
+                 trunc_length: int = 0,
+                 only_all: bool = False):
+        self.name = name
+        self.calc_func = calc_func
+        self.trunc_length = trunc_length
+        self.only_all = only_all
+
+    def calc(self, cluster_status):
+        val = self.calc_func(cluster_status)
+        if self.trunc_length != 0:
+            val = cli_utils.truncate_long_string(str(val), self.trunc_length)
+
+
+def show_status_table(show_all: bool, refresh: bool):
+    """Compute cluster table values and display."""
+    # TODO(zhwu): Update the information for auto-stop clusters.
+    clusters_status = backend_utils.get_clusters(refresh)
+
+    status_columns = [
+        StatusColumn('NAME', _get_name),
+        StatusColumn('LAUNCHED', _get_launched),
+        StatusColumn('RESOURCES', _get_resources),
+        StatusColumn('REGION', _get_region, only_all=True),
+        StatusColumn('STATUS', _get_status),
+        StatusColumn('AUTOSTOP', _get_autostop),
+        StatusColumn('COMMAND',
+                     _get_command,
+                     trunc_length=35 if not show_all else 0),
+        StatusColumn('HOURLY_PRICE', _get_price, only_all=True)
+    ]
+
+    columns = []
+    for status_column in status_columns:
+        if not status_column.only_all or show_all:
+            columns.append(status_column.name)
+    cluster_table = util_lib.create_table(columns)
+
+    pending_autostop = 0
+    for cluster_status in clusters_status:
+        row = []
+        for status_column in status_columns:
+            if not status_column.only_all or show_all:
+                row.append(status_column.calc_func(cluster_status))
+        cluster_table.add_row(row)
+        pending_autostop += _check_pending_autostop(cluster_status)
path: sky/utils/cli_utils/status_utils.py
text: @user1:
use "or equal"?

_check_pending_autostop -> _is_autostop_pending? (Check may suggest "make sure it has a pending autostop, assert/fail otherwise".)

@author:
i think it's nice to know how many clusters you have pending autostop and i think daniel also asked for that in his suggestion so += seems okay

diff_hunk: @@ -0,0 +1,118 @@
+"""Utilities for sky status."""
+import click
+
+from sky import backends
+from sky.backends import backend_utils
+from sky.utils.cli_utils import cli_utils
+from sky.skylet import util_lib
+
+
+class StatusColumn:
+    """One column of the displayed cluster table"""
+
+    def __init__(self,
+                 name: str,
+                 calc_func,
path: sky/utils/cli_utils/status_utils.py
text: @user1:
`show: Callable`?

diff_hunk: @@ -0,0 +1,118 @@
+"""Utilities for sky status."""
+import click
+
+from sky import backends
+from sky.backends import backend_utils
+from sky.utils.cli_utils import cli_utils
+from sky.skylet import util_lib
+
+
+class StatusColumn:
+    """One column of the displayed cluster table"""
+
+    def __init__(self,
+                 name: str,
+                 calc_func,
+                 trunc_length: int = 0,
+                 only_all: bool = False):
+        self.name = name
+        self.calc_func = calc_func
+        self.trunc_length = trunc_length
+        self.only_all = only_all
+
+    def calc(self, cluster_status):
+        val = self.calc_func(cluster_status)
+        if self.trunc_length != 0:
+            val = cli_utils.truncate_long_string(str(val), self.trunc_length)
+
+
+def show_status_table(show_all: bool, refresh: bool):
+    """Compute cluster table values and display."""
+    # TODO(zhwu): Update the information for auto-stop clusters.
+    clusters_status = backend_utils.get_clusters(refresh)
+
+    status_columns = [
+        StatusColumn('NAME', _get_name),
+        StatusColumn('LAUNCHED', _get_launched),
+        StatusColumn('RESOURCES', _get_resources),
+        StatusColumn('REGION', _get_region, only_all=True),
+        StatusColumn('STATUS', _get_status),
+        StatusColumn('AUTOSTOP', _get_autostop),
+        StatusColumn('COMMAND',
+                     _get_command,
+                     trunc_length=35 if not show_all else 0),
+        StatusColumn('HOURLY_PRICE', _get_price, only_all=True)
+    ]
+
+    columns = []
+    for status_column in status_columns:
+        if not status_column.only_all or show_all:
+            columns.append(status_column.name)
+    cluster_table = util_lib.create_table(columns)
+
+    pending_autostop = 0
+    for cluster_status in clusters_status:
+        row = []
+        for status_column in status_columns:
+            if not status_column.only_all or show_all:
+                row.append(status_column.calc_func(cluster_status))
path: sky/utils/cli_utils/status_utils.py
text: @user1:
How about: `status_column.show(cluster_status)` or .display()?

@author:
hmm i think `.calc` seems better because it's not actually displaying anything, just returning the value

diff_hunk: @@ -0,0 +1,118 @@
+"""Utilities for sky status."""
+import click
+
+from sky import backends
+from sky.backends import backend_utils
+from sky.utils.cli_utils import cli_utils
+from sky.skylet import util_lib
+
+
+class StatusColumn:
+    """One column of the displayed cluster table"""
+
+    def __init__(self,
+                 name: str,
+                 calc_func,
+                 trunc_length: int = 0,
+                 only_all: bool = False):
+        self.name = name
+        self.calc_func = calc_func
+        self.trunc_length = trunc_length
+        self.only_all = only_all
+
+    def calc(self, cluster_status):
+        val = self.calc_func(cluster_status)
+        if self.trunc_length != 0:
+            val = cli_utils.truncate_long_string(str(val), self.trunc_length)
+
+
+def show_status_table(show_all: bool, refresh: bool):
+    """Compute cluster table values and display."""
+    # TODO(zhwu): Update the information for auto-stop clusters.
+    clusters_status = backend_utils.get_clusters(refresh)
+
+    status_columns = [
+        StatusColumn('NAME', _get_name),
+        StatusColumn('LAUNCHED', _get_launched),
+        StatusColumn('RESOURCES', _get_resources),
+        StatusColumn('REGION', _get_region, only_all=True),
path: sky/utils/cli_utils/status_utils.py
text: @user1:
naming: `only_all=True` -> `show_by_default=False`?

diff_hunk: @@ -0,0 +1,118 @@
+"""Utilities for sky status."""
+import click
+
+from sky import backends
+from sky.backends import backend_utils
+from sky.utils.cli_utils import cli_utils
+from sky.skylet import util_lib
+
+
+class StatusColumn:
+    """One column of the displayed cluster table"""
+
+    def __init__(self,
+                 name: str,
+                 calc_func,
+                 trunc_length: int = 0,
+                 only_all: bool = False):
+        self.name = name
+        self.calc_func = calc_func
+        self.trunc_length = trunc_length
+        self.only_all = only_all
+
+    def calc(self, cluster_status):
+        val = self.calc_func(cluster_status)
+        if self.trunc_length != 0:
+            val = cli_utils.truncate_long_string(str(val), self.trunc_length)
+
+
+def show_status_table(show_all: bool, refresh: bool):
+    """Compute cluster table values and display."""
+    # TODO(zhwu): Update the information for auto-stop clusters.
+    clusters_status = backend_utils.get_clusters(refresh)
+
+    status_columns = [
+        StatusColumn('NAME', _get_name),
+        StatusColumn('LAUNCHED', _get_launched),
+        StatusColumn('RESOURCES', _get_resources),
+        StatusColumn('REGION', _get_region, only_all=True),
+        StatusColumn('STATUS', _get_status),
+        StatusColumn('AUTOSTOP', _get_autostop),
+        StatusColumn('COMMAND',
+                     _get_command,
+                     trunc_length=35 if not show_all else 0),
+        StatusColumn('HOURLY_PRICE', _get_price, only_all=True)
+    ]
+
+    columns = []
+    for status_column in status_columns:
+        if not status_column.only_all or show_all:
+            columns.append(status_column.name)
+    cluster_table = util_lib.create_table(columns)
+
+    pending_autostop = 0
+    for cluster_status in clusters_status:
+        row = []
+        for status_column in status_columns:
+            if not status_column.only_all or show_all:
+                row.append(status_column.calc_func(cluster_status))
+        cluster_table.add_row(row)
+        pending_autostop += _check_pending_autostop(cluster_status)
+
+    if clusters_status:
+        click.echo(cluster_table)
+        if pending_autostop:
+            click.echo(
+                '\n'
+                f'You have {pending_autostop} clusters with autostop scheduled.'
+                ' Refresh statuses with: `sky status --refresh`.')
+    else:
+        click.echo('No existing clusters.')
+
+_get_name = lambda cluster_status: \
+    cluster_status['name']
+_get_launched = lambda cluster_status: \
+    cli_utils.readable_time_duration(cluster_status['launched_at'])
+_get_region = lambda clusters_status: \
+    clusters_status['handle'].get_cluster_region()
+_get_status = lambda cluster_status: \
+    cluster_status['status'].value
+_get_command = lambda cluster_status: \
+    cluster_status['last_use']
+
+
+def _get_resources(cluster_status):
+    handle = cluster_status['handle']
+    resources_str = '<initializing>'
+    if isinstance(handle, backends.LocalDockerBackend.ResourceHandle):
+        resources_str = 'docker'
+    elif isinstance(handle, backends.CloudVmRayBackend.ResourceHandle):
+        if (handle.launched_nodes is not None and
+                handle.launched_resources is not None):
+            launched_resource_str = str(handle.launched_resources)
+            resources_str = (f'{handle.launched_nodes}x '
+                             f'{launched_resource_str}')
+    else:
+        raise ValueError(f'Unknown handle type {type(handle)} encountered.')
+    return resources_str
+
+
+def _get_autostop(cluster_status):
+    autostop_str = '-'
+    if cluster_status['autostop'] >= 0:
+        # TODO(zhwu): check the status of the autostop cluster.
+        autostop_str = str(cluster_status['autostop']) + ' min'
+    return autostop_str
+
+
+def _get_price(cluster_status):
+    handle = cluster_status['handle']
+    hourly_cost = handle.launched_resources.get_cost(3600) \
+                * handle.launched_nodes
path: sky/utils/cli_utils/status_utils.py
text: @user1:
Use parens - will be reformatted automatically:

    hourly_cost = (handle.launched_resources.get_cost(3600) * handle.launched_nodes)