diff --git a/modal/app.py b/modal/app.py index 0e1b70922..4f0445da6 100644 --- a/modal/app.py +++ b/modal/app.py @@ -18,10 +18,6 @@ from ._utils.function_utils import FunctionInfo, is_global_object, is_top_level_function from ._utils.grpc_utils import unary_stream from ._utils.mount_utils import validate_volumes -from .app_utils import ( # noqa: F401 - _list_apps, - list_apps, -) from .client import _Client from .cloud_bucket_mount import _CloudBucketMount from .cls import _Cls diff --git a/modal/app_utils.py b/modal/app_utils.py deleted file mode 100644 index 59f76dc62..000000000 --- a/modal/app_utils.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright Modal Labs 2024 -# Note: this is a temporary module until we've (1) deleted the current app.py (3) renamed stub.py to app.py -from typing import List, Optional - -from modal_proto import api_pb2 - -from ._utils.async_utils import synchronize_api -from .client import _Client -from .object import _get_environment_name - - -async def _list_apps(env: Optional[str] = None, client: Optional[_Client] = None) -> List[api_pb2.AppStats]: - """List apps in a given Modal environment.""" - if client is None: - client = await _Client.from_env() - resp: api_pb2.AppListResponse = await client.stub.AppList( - api_pb2.AppListRequest(environment_name=_get_environment_name(env)) - ) - return list(resp.apps) - - -list_apps = synchronize_api(_list_apps) diff --git a/modal/cli/app.py b/modal/cli/app.py index 88dde7432..b7d558786 100644 --- a/modal/cli/app.py +++ b/modal/cli/app.py @@ -1,5 +1,4 @@ # Copyright Modal Labs 2022 -import time from typing import List, Optional, Union import typer @@ -9,9 +8,9 @@ from typer import Argument, Option from modal._utils.async_utils import synchronizer -from modal.app_utils import _list_apps from modal.client import _Client from modal.environments import ensure_env +from modal.object import _get_environment_name from modal_proto import api_pb2 from .utils import ENV_OPTION, display_table, get_app_id_from_name, stream_app_logs, timestamp_to_local @@ -34,6 +33,11 @@ async def list(env: Optional[str] = ENV_OPTION, json: bool = False): """List Modal apps that are currently deployed/running or recently stopped.""" env = ensure_env(env) + client = await _Client.from_env() + + resp: api_pb2.AppListResponse = await client.stub.AppList( + api_pb2.AppListRequest(environment_name=_get_environment_name(env)) + ) columns: List[Union[Column, str]] = [ Column("App ID", min_width=25), # Ensure that App ID is not truncated in slim terminals @@ -44,23 +48,7 @@ async def list(env: Optional[str] = ENV_OPTION, json: bool = False): "Stopped at", ] rows: List[List[Union[Text, str]]] = [] - apps: List[api_pb2.AppStats] = await _list_apps(env) - now = time.time() - for app_stats in apps: - if ( - # Previously, all deployed objects (Dicts, Volumes, etc.) created an entry in the App table. - # We are waiting to roll off support for old clients before we can clean up the database. - # Until then, we filter deployed "single-object apps" from this output based on the object entity. - (app_stats.object_entity and app_stats.object_entity != "ap") - # AppList always returns up to the 250 most-recently stopped apps, which is a lot for the CLI - # (it is also used in the web interface, where apps are organized by tabs and paginated). - # So we semi-arbitrarily limit the stopped apps to those stopped within the past 2 hours. - or ( - app_stats.state in {api_pb2.AppState.APP_STATE_STOPPED} and (now - app_stats.stopped_at) > (2 * 60 * 60) - ) - ): - continue - + for app_stats in resp.apps: state = APP_STATE_TO_MESSAGE.get(app_stats.state, Text("unknown", style="gray")) rows.append( [ diff --git a/modal_proto/api.proto b/modal_proto/api.proto index 157556212..94e433158 100644 --- a/modal_proto/api.proto +++ b/modal_proto/api.proto @@ -293,7 +293,16 @@ message AppListRequest { } message AppListResponse { - repeated AppStats apps = 1; + message AppListItem { + string app_id = 1; + string description = 3; + AppState state = 4; + double created_at = 5; + double stopped_at = 6; + int32 n_running_tasks = 8; + string name = 10; + } + repeated AppListItem apps = 1; } message AppLookupObjectRequest { @@ -342,18 +351,6 @@ message AppSetObjectsRequest { string single_object_id = 6; } -message AppStats { - string app_id = 1; - string description = 3; - AppState state = 4; - double created_at = 5; - double stopped_at = 6; - int32 n_running_tasks = 8; - string object_entity = 9; - string name = 10; - double deployed_at = 11; -} - message AppStopRequest { string app_id = 1 [ (modal.options.audit_target_attr) = true ]; AppStopSource source = 2; diff --git a/test/app_test.py b/test/app_test.py index 7bd3a9453..b2a0418ca 100644 --- a/test/app_test.py +++ b/test/app_test.py @@ -9,7 +9,6 @@ from modal import App, Dict, Image, Mount, Secret, Stub, Volume, enable_output, web_endpoint from modal._output import OutputManager -from modal.app import list_apps # type: ignore from modal.exception import DeprecationError, ExecutionError, InvalidError, NotFoundError from modal.partial_function import _parse_custom_domains from modal.runner import deploy_app, deploy_stub @@ -319,16 +318,6 @@ def test_app(client): square_modal.remote(42) -def test_list_apps(client): - apps_0 = [app.name for app in list_apps(client=client)] - app = App() - deploy_app(app, "foobar", client=client) - apps_1 = [app.name for app in list_apps(client=client)] - - assert len(apps_1) == len(apps_0) + 1 - assert set(apps_1) - set(apps_0) == set(["foobar"]) - - def test_non_string_app_name(): with pytest.raises(InvalidError, match="Must be string"): App(Image.debian_slim()) # type: ignore diff --git a/test/conftest.py b/test/conftest.py index b94890c1c..010bff146 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -374,7 +374,7 @@ async def AppList(self, stream): apps = [] for app_name, app_id in self.deployed_apps.items(): apps.append( - api_pb2.AppStats( + api_pb2.AppListResponse.AppListItem( name=app_name, description=app_name, app_id=app_id,