anyscale 0.26.21__py3-none-any.whl → 0.26.22__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- anyscale/_private/anyscale_client/anyscale_client.py +103 -43
- anyscale/_private/anyscale_client/common.py +37 -7
- anyscale/_private/anyscale_client/fake_anyscale_client.py +98 -27
- anyscale/_private/models/model_base.py +95 -0
- anyscale/_private/workload/workload_sdk.py +3 -1
- anyscale/aggregated_instance_usage/models.py +4 -4
- anyscale/client/README.md +1 -0
- anyscale/client/openapi_client/api/default_api.py +122 -1
- anyscale/commands/command_examples.py +4 -0
- anyscale/commands/list_util.py +107 -0
- anyscale/commands/service_commands.py +267 -31
- anyscale/commands/util.py +5 -4
- anyscale/controllers/service_controller.py +7 -86
- anyscale/service/__init__.py +53 -3
- anyscale/service/_private/service_sdk.py +177 -41
- anyscale/service/commands.py +78 -1
- anyscale/service/models.py +65 -0
- anyscale/util.py +35 -1
- anyscale/version.py +1 -1
- {anyscale-0.26.21.dist-info → anyscale-0.26.22.dist-info}/METADATA +1 -1
- {anyscale-0.26.21.dist-info → anyscale-0.26.22.dist-info}/RECORD +26 -25
- {anyscale-0.26.21.dist-info → anyscale-0.26.22.dist-info}/LICENSE +0 -0
- {anyscale-0.26.21.dist-info → anyscale-0.26.22.dist-info}/NOTICE +0 -0
- {anyscale-0.26.21.dist-info → anyscale-0.26.22.dist-info}/WHEEL +0 -0
- {anyscale-0.26.21.dist-info → anyscale-0.26.22.dist-info}/entry_points.txt +0 -0
- {anyscale-0.26.21.dist-info → anyscale-0.26.22.dist-info}/top_level.txt +0 -0
@@ -15,9 +15,9 @@ import anyscale
|
|
15
15
|
from anyscale.aggregated_instance_usage.models import DownloadCSVFilters
|
16
16
|
|
17
17
|
download_csv_filters = DownloadCSVFilters(
|
18
|
-
# Start date (inclusive) for the usage CSV.
|
18
|
+
# Start date (UTC inclusive) for the usage CSV.
|
19
19
|
start_date="2024-10-01",
|
20
|
-
# End date (inclusive) for the usage CSV.
|
20
|
+
# End date (UTC inclusive) for the usage CSV.
|
21
21
|
end_date="2024-10-31",
|
22
22
|
# Optional cloud name to filter by.
|
23
23
|
cloud="cloud_name",
|
@@ -39,14 +39,14 @@ download_csv_filters = DownloadCSVFilters(
|
|
39
39
|
raise ValueError("Incorrect date format, should be YYYY-MM-DD")
|
40
40
|
|
41
41
|
start_date: str = field(
|
42
|
-
metadata={"docstring": "Start date (inclusive) for the usage CSV."}
|
42
|
+
metadata={"docstring": "Start date (UTC inclusive) for the usage CSV."}
|
43
43
|
)
|
44
44
|
|
45
45
|
def _validate_start_date(self, start_date: str):
|
46
46
|
self._validate_date(start_date)
|
47
47
|
|
48
48
|
end_date: str = field(
|
49
|
-
metadata={"docstring": "End date (inclusive) for the usage CSV."}
|
49
|
+
metadata={"docstring": "End date (UTC inclusive) for the usage CSV."}
|
50
50
|
)
|
51
51
|
|
52
52
|
def _validate_end_date(self, end_date: str):
|
anyscale/client/README.md
CHANGED
@@ -294,6 +294,7 @@ Class | Method | HTTP request | Description
|
|
294
294
|
*DefaultApi* | [**list_project_collaborators_api_v2_projects_project_id_collaborators_users_get**](docs/DefaultApi.md#list_project_collaborators_api_v2_projects_project_id_collaborators_users_get) | **GET** /api/v2/projects/{project_id}/collaborators/users | List Project Collaborators
|
295
295
|
*DefaultApi* | [**list_projects_api_v2_projects_get**](docs/DefaultApi.md#list_projects_api_v2_projects_get) | **GET** /api/v2/projects/ | List Projects
|
296
296
|
*DefaultApi* | [**list_ray_sessions_api_v2_tasks_dashboard_list_ray_sessions_get**](docs/DefaultApi.md#list_ray_sessions_api_v2_tasks_dashboard_list_ray_sessions_get) | **GET** /api/v2/tasks_dashboard/list_ray_sessions | List Ray Sessions
|
297
|
+
*DefaultApi* | [**list_recommended_workspace_templates_api_v2_experimental_workspaces_templates_recommended_get**](docs/DefaultApi.md#list_recommended_workspace_templates_api_v2_experimental_workspaces_templates_recommended_get) | **GET** /api/v2/experimental_workspaces/templates/recommended | List Recommended Workspace Templates
|
297
298
|
*DefaultApi* | [**list_services_api_v2_services_v2_get**](docs/DefaultApi.md#list_services_api_v2_services_v2_get) | **GET** /api/v2/services-v2/ | List Services
|
298
299
|
*DefaultApi* | [**list_sessions_api_v2_sessions_get**](docs/DefaultApi.md#list_sessions_api_v2_sessions_get) | **GET** /api/v2/sessions/ | List Sessions
|
299
300
|
*DefaultApi* | [**list_workspace_templates_api_v2_experimental_workspaces_templates_get**](docs/DefaultApi.md#list_workspace_templates_api_v2_experimental_workspaces_templates_get) | **GET** /api/v2/experimental_workspaces/templates | List Workspace Templates
|
@@ -20980,6 +20980,7 @@ class DefaultApi(object):
|
|
20980
20980
|
|
20981
20981
|
:param async_req bool: execute request asynchronously
|
20982
20982
|
:param str cluster_id: (required)
|
20983
|
+
:param bool skip_job_details: Skip decorating job details, which can be an expensive operation.
|
20983
20984
|
:param _preload_content: if False, the urllib3.HTTPResponse object will
|
20984
20985
|
be returned without reading/decoding response
|
20985
20986
|
data. Default is True.
|
@@ -21004,6 +21005,7 @@ class DefaultApi(object):
|
|
21004
21005
|
|
21005
21006
|
:param async_req bool: execute request asynchronously
|
21006
21007
|
:param str cluster_id: (required)
|
21008
|
+
:param bool skip_job_details: Skip decorating job details, which can be an expensive operation.
|
21007
21009
|
:param _return_http_data_only: response data without head status code
|
21008
21010
|
and headers
|
21009
21011
|
:param _preload_content: if False, the urllib3.HTTPResponse object will
|
@@ -21021,7 +21023,8 @@ class DefaultApi(object):
|
|
21021
21023
|
local_var_params = locals()
|
21022
21024
|
|
21023
21025
|
all_params = [
|
21024
|
-
'cluster_id'
|
21026
|
+
'cluster_id',
|
21027
|
+
'skip_job_details'
|
21025
21028
|
]
|
21026
21029
|
all_params.extend(
|
21027
21030
|
[
|
@@ -21052,6 +21055,8 @@ class DefaultApi(object):
|
|
21052
21055
|
query_params = []
|
21053
21056
|
if 'cluster_id' in local_var_params and local_var_params['cluster_id'] is not None: # noqa: E501
|
21054
21057
|
query_params.append(('cluster_id', local_var_params['cluster_id'])) # noqa: E501
|
21058
|
+
if 'skip_job_details' in local_var_params and local_var_params['skip_job_details'] is not None: # noqa: E501
|
21059
|
+
query_params.append(('skip_job_details', local_var_params['skip_job_details'])) # noqa: E501
|
21055
21060
|
|
21056
21061
|
header_params = {}
|
21057
21062
|
|
@@ -26320,6 +26325,122 @@ class DefaultApi(object):
|
|
26320
26325
|
_request_timeout=local_var_params.get('_request_timeout'),
|
26321
26326
|
collection_formats=collection_formats)
|
26322
26327
|
|
26328
|
+
def list_recommended_workspace_templates_api_v2_experimental_workspaces_templates_recommended_get(self, **kwargs): # noqa: E501
|
26329
|
+
"""List Recommended Workspace Templates # noqa: E501
|
26330
|
+
|
26331
|
+
Lists all workspace templates ranked by user's organization marketing questions # noqa: E501
|
26332
|
+
This method makes a synchronous HTTP request by default. To make an
|
26333
|
+
asynchronous HTTP request, please pass async_req=True
|
26334
|
+
>>> thread = api.list_recommended_workspace_templates_api_v2_experimental_workspaces_templates_recommended_get(async_req=True)
|
26335
|
+
>>> result = thread.get()
|
26336
|
+
|
26337
|
+
:param async_req bool: execute request asynchronously
|
26338
|
+
:param int count: Maximum number of templates to return
|
26339
|
+
:param list[str] oa_group_names: Search for templates that belong to the provided group name
|
26340
|
+
:param _preload_content: if False, the urllib3.HTTPResponse object will
|
26341
|
+
be returned without reading/decoding response
|
26342
|
+
data. Default is True.
|
26343
|
+
:param _request_timeout: timeout setting for this request. If one
|
26344
|
+
number provided, it will be total request
|
26345
|
+
timeout. It can also be a pair (tuple) of
|
26346
|
+
(connection, read) timeouts.
|
26347
|
+
:return: WorkspacetemplateListResponse
|
26348
|
+
If the method is called asynchronously,
|
26349
|
+
returns the request thread.
|
26350
|
+
"""
|
26351
|
+
kwargs['_return_http_data_only'] = True
|
26352
|
+
return self.list_recommended_workspace_templates_api_v2_experimental_workspaces_templates_recommended_get_with_http_info(**kwargs) # noqa: E501
|
26353
|
+
|
26354
|
+
def list_recommended_workspace_templates_api_v2_experimental_workspaces_templates_recommended_get_with_http_info(self, **kwargs): # noqa: E501
|
26355
|
+
"""List Recommended Workspace Templates # noqa: E501
|
26356
|
+
|
26357
|
+
Lists all workspace templates ranked by user's organization marketing questions # noqa: E501
|
26358
|
+
This method makes a synchronous HTTP request by default. To make an
|
26359
|
+
asynchronous HTTP request, please pass async_req=True
|
26360
|
+
>>> thread = api.list_recommended_workspace_templates_api_v2_experimental_workspaces_templates_recommended_get_with_http_info(async_req=True)
|
26361
|
+
>>> result = thread.get()
|
26362
|
+
|
26363
|
+
:param async_req bool: execute request asynchronously
|
26364
|
+
:param int count: Maximum number of templates to return
|
26365
|
+
:param list[str] oa_group_names: Search for templates that belong to the provided group name
|
26366
|
+
:param _return_http_data_only: response data without head status code
|
26367
|
+
and headers
|
26368
|
+
:param _preload_content: if False, the urllib3.HTTPResponse object will
|
26369
|
+
be returned without reading/decoding response
|
26370
|
+
data. Default is True.
|
26371
|
+
:param _request_timeout: timeout setting for this request. If one
|
26372
|
+
number provided, it will be total request
|
26373
|
+
timeout. It can also be a pair (tuple) of
|
26374
|
+
(connection, read) timeouts.
|
26375
|
+
:return: tuple(WorkspacetemplateListResponse, status_code(int), headers(HTTPHeaderDict))
|
26376
|
+
If the method is called asynchronously,
|
26377
|
+
returns the request thread.
|
26378
|
+
"""
|
26379
|
+
|
26380
|
+
local_var_params = locals()
|
26381
|
+
|
26382
|
+
all_params = [
|
26383
|
+
'count',
|
26384
|
+
'oa_group_names'
|
26385
|
+
]
|
26386
|
+
all_params.extend(
|
26387
|
+
[
|
26388
|
+
'async_req',
|
26389
|
+
'_return_http_data_only',
|
26390
|
+
'_preload_content',
|
26391
|
+
'_request_timeout'
|
26392
|
+
]
|
26393
|
+
)
|
26394
|
+
|
26395
|
+
for key, val in six.iteritems(local_var_params['kwargs']):
|
26396
|
+
if key not in all_params:
|
26397
|
+
raise ApiTypeError(
|
26398
|
+
"Got an unexpected keyword argument '%s'"
|
26399
|
+
" to method list_recommended_workspace_templates_api_v2_experimental_workspaces_templates_recommended_get" % key
|
26400
|
+
)
|
26401
|
+
local_var_params[key] = val
|
26402
|
+
del local_var_params['kwargs']
|
26403
|
+
|
26404
|
+
collection_formats = {}
|
26405
|
+
|
26406
|
+
path_params = {}
|
26407
|
+
|
26408
|
+
query_params = []
|
26409
|
+
if 'count' in local_var_params and local_var_params['count'] is not None: # noqa: E501
|
26410
|
+
query_params.append(('count', local_var_params['count'])) # noqa: E501
|
26411
|
+
if 'oa_group_names' in local_var_params and local_var_params['oa_group_names'] is not None: # noqa: E501
|
26412
|
+
query_params.append(('oa_group_names', local_var_params['oa_group_names'])) # noqa: E501
|
26413
|
+
collection_formats['oa_group_names'] = 'multi' # noqa: E501
|
26414
|
+
|
26415
|
+
header_params = {}
|
26416
|
+
|
26417
|
+
form_params = []
|
26418
|
+
local_var_files = {}
|
26419
|
+
|
26420
|
+
body_params = None
|
26421
|
+
# HTTP header `Accept`
|
26422
|
+
header_params['Accept'] = self.api_client.select_header_accept(
|
26423
|
+
['application/json']) # noqa: E501
|
26424
|
+
|
26425
|
+
# Authentication setting
|
26426
|
+
auth_settings = [] # noqa: E501
|
26427
|
+
|
26428
|
+
return self.api_client.call_api(
|
26429
|
+
'/api/v2/experimental_workspaces/templates/recommended', 'GET',
|
26430
|
+
path_params,
|
26431
|
+
query_params,
|
26432
|
+
header_params,
|
26433
|
+
body=body_params,
|
26434
|
+
post_params=form_params,
|
26435
|
+
files=local_var_files,
|
26436
|
+
response_type='WorkspacetemplateListResponse', # noqa: E501
|
26437
|
+
auth_settings=auth_settings,
|
26438
|
+
async_req=local_var_params.get('async_req'),
|
26439
|
+
_return_http_data_only=local_var_params.get('_return_http_data_only'), # noqa: E501
|
26440
|
+
_preload_content=local_var_params.get('_preload_content', True),
|
26441
|
+
_request_timeout=local_var_params.get('_request_timeout'),
|
26442
|
+
collection_formats=collection_formats)
|
26443
|
+
|
26323
26444
|
def list_services_api_v2_services_v2_get(self, **kwargs): # noqa: E501
|
26324
26445
|
"""List Services # noqa: E501
|
26325
26446
|
|
@@ -0,0 +1,107 @@
|
|
1
|
+
import itertools
|
2
|
+
from json import dumps as json_dumps
|
3
|
+
from typing import Any, Callable, Dict, Iterator, List, Optional
|
4
|
+
|
5
|
+
from rich.console import Console
|
6
|
+
from rich.table import Table
|
7
|
+
|
8
|
+
from anyscale.util import AnyscaleJSONEncoder
|
9
|
+
|
10
|
+
|
11
|
+
def _paginate(iterator: Iterator[Any], page_size: Optional[int]) -> Iterator[List[Any]]:
|
12
|
+
if page_size is None:
|
13
|
+
yield list(iterator)
|
14
|
+
else:
|
15
|
+
while True:
|
16
|
+
page = list(itertools.islice(iterator, page_size))
|
17
|
+
if not page:
|
18
|
+
return
|
19
|
+
yield page
|
20
|
+
|
21
|
+
|
22
|
+
def display_list( # noqa: PLR0913
|
23
|
+
iterator: Iterator[Any],
|
24
|
+
item_formatter: Callable[[Any], Dict[str, Any]],
|
25
|
+
table_creator: Callable[[bool], Table],
|
26
|
+
json_output: bool,
|
27
|
+
page_size: int,
|
28
|
+
interactive: bool,
|
29
|
+
max_items: Optional[int],
|
30
|
+
console: Console,
|
31
|
+
) -> int:
|
32
|
+
"""Displays a list of items from an iterator, handling pagination and output format.
|
33
|
+
|
34
|
+
Args:
|
35
|
+
iterator: The iterator yielding items to display.
|
36
|
+
item_formatter: A callable that takes an item and returns a dictionary
|
37
|
+
representing the row data (for table) or the JSON object.
|
38
|
+
table_creator: A callable that takes a boolean (is_first_page) and
|
39
|
+
returns a rich.Table instance. Used only if json_output is False.
|
40
|
+
json_output: If True, output items as a JSON list. Otherwise, display
|
41
|
+
them in a table created by table_creator.
|
42
|
+
page_size: The number of items to display per page in interactive mode.
|
43
|
+
interactive: If True, enables interactive pagination. If False, displays
|
44
|
+
up to max_items (or all items if max_items is None) without prompting.
|
45
|
+
max_items: The maximum total number of items to display when interactive
|
46
|
+
is False. If None, all items are displayed.
|
47
|
+
console: The rich.Console object to use for output.
|
48
|
+
|
49
|
+
Returns:
|
50
|
+
The total number of items displayed.
|
51
|
+
"""
|
52
|
+
total_count = 0
|
53
|
+
pages = _paginate(iterator, page_size if interactive else max_items)
|
54
|
+
|
55
|
+
# fetch first page under spinner
|
56
|
+
with console.status("Retrieving items…", spinner="dots"):
|
57
|
+
try:
|
58
|
+
first_page = next(pages)
|
59
|
+
except StopIteration:
|
60
|
+
first_page = []
|
61
|
+
|
62
|
+
def _render(page: List[Any], is_first: bool, page_num: int):
|
63
|
+
nonlocal total_count
|
64
|
+
total_count += len(page)
|
65
|
+
if interactive:
|
66
|
+
console.print(f"[dim]Page {page_num}[/dim]")
|
67
|
+
rows = [item_formatter(item) for item in page]
|
68
|
+
if json_output:
|
69
|
+
json_str = json_dumps(rows, indent=2, cls=AnyscaleJSONEncoder)
|
70
|
+
console.print_json(json=json_str)
|
71
|
+
else:
|
72
|
+
tbl = table_creator(is_first)
|
73
|
+
for row in rows:
|
74
|
+
tbl.add_row(*row.values())
|
75
|
+
console.print(tbl)
|
76
|
+
|
77
|
+
# render first page
|
78
|
+
if first_page:
|
79
|
+
_render(first_page, True, page_num=1)
|
80
|
+
|
81
|
+
# non-interactive: stop after first page
|
82
|
+
if not interactive:
|
83
|
+
return total_count
|
84
|
+
|
85
|
+
# interactive: prompt after full first page
|
86
|
+
if len(first_page) == page_size:
|
87
|
+
console.print()
|
88
|
+
console.print(
|
89
|
+
"[dim]Press [bold]Enter[/bold] to continue, [bold]q[/bold] to quit…[/]"
|
90
|
+
)
|
91
|
+
if input("> ").strip().lower() == "q":
|
92
|
+
return total_count
|
93
|
+
|
94
|
+
# render remaining pages
|
95
|
+
page_num = 2
|
96
|
+
for page in pages:
|
97
|
+
_render(page, False, page_num)
|
98
|
+
if len(page) == page_size:
|
99
|
+
console.print()
|
100
|
+
console.print(
|
101
|
+
"[dim]Press [bold]Enter[/bold] to continue, [bold]q[/bold] to quit…[/]"
|
102
|
+
)
|
103
|
+
if input("> ").strip().lower() == "q":
|
104
|
+
break
|
105
|
+
page_num += 1
|
106
|
+
|
107
|
+
return total_count
|
@@ -2,15 +2,18 @@ from io import StringIO
|
|
2
2
|
from json import dumps as json_dumps
|
3
3
|
import pathlib
|
4
4
|
import sys
|
5
|
-
from typing import Any, Dict, Optional, Tuple
|
5
|
+
from typing import Any, Dict, List, Optional, Tuple
|
6
6
|
|
7
7
|
import click
|
8
|
+
from rich.console import Console
|
9
|
+
from rich.table import Table
|
8
10
|
from typing_extensions import Literal
|
9
11
|
import yaml
|
10
12
|
|
11
13
|
from anyscale._private.models.image_uri import ImageURI
|
12
14
|
from anyscale.cli_logger import BlockLogger
|
13
15
|
from anyscale.commands import command_examples
|
16
|
+
from anyscale.commands.list_util import display_list
|
14
17
|
from anyscale.commands.util import (
|
15
18
|
AnyscaleCommand,
|
16
19
|
convert_kv_strings_to_dict,
|
@@ -19,8 +22,21 @@ from anyscale.commands.util import (
|
|
19
22
|
)
|
20
23
|
from anyscale.controllers.service_controller import ServiceController
|
21
24
|
import anyscale.service
|
22
|
-
from anyscale.service import
|
23
|
-
|
25
|
+
from anyscale.service.models import (
|
26
|
+
ServiceConfig,
|
27
|
+
ServiceLogMode,
|
28
|
+
ServiceSortField,
|
29
|
+
ServiceState,
|
30
|
+
ServiceStatus,
|
31
|
+
ServiceVersionStatus,
|
32
|
+
SortOrder,
|
33
|
+
)
|
34
|
+
from anyscale.util import (
|
35
|
+
AnyscaleJSONEncoder,
|
36
|
+
get_endpoint,
|
37
|
+
validate_non_negative_arg,
|
38
|
+
validate_service_state_filter,
|
39
|
+
)
|
24
40
|
|
25
41
|
|
26
42
|
log = BlockLogger() # CLI Logger
|
@@ -389,12 +405,14 @@ def status(
|
|
389
405
|
status_dict.get("primary_version", {}).pop("config", None)
|
390
406
|
status_dict.get("canary_version", {}).pop("config", None)
|
391
407
|
|
408
|
+
console = Console()
|
392
409
|
if json:
|
393
|
-
|
410
|
+
json_str = json_dumps(status_dict, indent=2, cls=AnyscaleJSONEncoder)
|
411
|
+
console.print_json(json=json_str)
|
394
412
|
else:
|
395
413
|
stream = StringIO()
|
396
414
|
yaml.dump(status_dict, stream, sort_keys=False)
|
397
|
-
print(stream.getvalue(), end="")
|
415
|
+
console.print(stream.getvalue(), end="")
|
398
416
|
|
399
417
|
|
400
418
|
@service_cli.command(
|
@@ -624,51 +642,270 @@ def rollout( # noqa: PLR0913
|
|
624
642
|
)
|
625
643
|
|
626
644
|
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
645
|
+
MAX_PAGE_SIZE = 50
|
646
|
+
NON_INTERACTIVE_DEFAULT_MAX_ITEMS = 10
|
647
|
+
|
648
|
+
|
649
|
+
def validate_page_size(ctx, param, value):
|
650
|
+
value = validate_non_negative_arg(ctx, param, value)
|
651
|
+
if value is not None and value > MAX_PAGE_SIZE:
|
652
|
+
raise click.BadParameter(f"must be less than or equal to {MAX_PAGE_SIZE}.")
|
653
|
+
return value
|
654
|
+
|
655
|
+
|
656
|
+
def validate_max_items(ctx, param, value):
|
657
|
+
if value is None:
|
658
|
+
return None
|
659
|
+
return validate_non_negative_arg(ctx, param, value)
|
660
|
+
|
661
|
+
|
662
|
+
def _parse_sort_option(sort: Optional[str],) -> Tuple[Optional[str], SortOrder]:
|
663
|
+
"""
|
664
|
+
Given a raw sort string (e.g. "-created_at"), return
|
665
|
+
(canonical_field_name, SortOrder).
|
666
|
+
"""
|
667
|
+
if not sort:
|
668
|
+
return None, SortOrder.ASC
|
669
|
+
|
670
|
+
# build case-insensitive map of allowed fields
|
671
|
+
allowed = {f.value.lower(): f.value for f in ServiceSortField.__members__.values()}
|
672
|
+
|
673
|
+
# detect leading '-' for descending
|
674
|
+
if sort.startswith("-"):
|
675
|
+
raw = sort[1:]
|
676
|
+
order = SortOrder.DESC
|
677
|
+
else:
|
678
|
+
raw = sort
|
679
|
+
order = SortOrder.ASC
|
680
|
+
|
681
|
+
key = raw.lower()
|
682
|
+
if key not in allowed:
|
683
|
+
allowed_names = ", ".join(sorted(allowed.values()))
|
684
|
+
raise click.BadParameter(
|
685
|
+
f"Invalid sort field '{raw}'. Allowed fields: {allowed_names}"
|
686
|
+
)
|
687
|
+
|
688
|
+
return allowed[key], order
|
689
|
+
|
690
|
+
|
691
|
+
def _create_service_list_table(show_header: bool) -> Table:
|
692
|
+
table = Table(show_header=show_header, expand=True)
|
693
|
+
# NAME and ID: larger ratios, can wrap but never truncate
|
694
|
+
table.add_column(
|
695
|
+
"NAME", no_wrap=False, overflow="fold", ratio=3, min_width=15,
|
696
|
+
)
|
697
|
+
table.add_column(
|
698
|
+
"ID", no_wrap=False, overflow="fold", ratio=2, min_width=12,
|
699
|
+
)
|
700
|
+
# all other columns will wrap as needed
|
701
|
+
for heading in (
|
702
|
+
"CURRENT STATE",
|
703
|
+
"CREATOR",
|
704
|
+
"PROJECT",
|
705
|
+
"LAST DEPLOYED AT",
|
706
|
+
):
|
707
|
+
table.add_column(
|
708
|
+
heading, no_wrap=False, overflow="fold", ratio=1, min_width=8,
|
709
|
+
)
|
710
|
+
|
711
|
+
return table
|
712
|
+
|
713
|
+
|
714
|
+
def _format_service_output_data(svc: ServiceStatus) -> Dict[str, str]:
|
715
|
+
last_deployed_at = ""
|
716
|
+
if isinstance(svc.primary_version, ServiceVersionStatus):
|
717
|
+
last_deployed_at = svc.primary_version.created_at.strftime("%Y-%m-%d %H:%M:%S")
|
718
|
+
|
719
|
+
return {
|
720
|
+
"name": svc.name,
|
721
|
+
"id": svc.id,
|
722
|
+
"current_state": str(svc.state),
|
723
|
+
"creator": str(svc.creator or ""),
|
724
|
+
"project": str(svc.project or ""),
|
725
|
+
"last_deployed_at": last_deployed_at,
|
726
|
+
}
|
727
|
+
|
728
|
+
|
729
|
+
@service_cli.command(
|
730
|
+
name="list", help="List services.", cls=AnyscaleCommand,
|
631
731
|
)
|
732
|
+
@click.option("--service-id", "--id", help="ID of the service to display.")
|
733
|
+
@click.option("--name", "-n", help="Name of the service to display.")
|
632
734
|
@click.option(
|
633
|
-
"--
|
735
|
+
"--cloud",
|
736
|
+
type=str,
|
737
|
+
help="The Anyscale Cloud of this workload; defaults to your org/workspace cloud.",
|
634
738
|
)
|
635
739
|
@click.option(
|
636
|
-
"--project
|
740
|
+
"--project",
|
741
|
+
type=str,
|
742
|
+
help="Named project to use; defaults to your org/workspace project.",
|
637
743
|
)
|
638
744
|
@click.option(
|
639
745
|
"--created-by-me",
|
746
|
+
is_flag=True,
|
747
|
+
default=False,
|
640
748
|
help="List services created by me only.",
|
749
|
+
)
|
750
|
+
@click.option(
|
751
|
+
"--state",
|
752
|
+
"-s",
|
753
|
+
"state_filter",
|
754
|
+
multiple=True,
|
755
|
+
callback=validate_service_state_filter,
|
756
|
+
help=(
|
757
|
+
"Filter by service state (repeatable). "
|
758
|
+
f"Allowed: {', '.join(s.value for s in ServiceState)}"
|
759
|
+
),
|
760
|
+
)
|
761
|
+
@click.option(
|
762
|
+
"--include-archived",
|
641
763
|
is_flag=True,
|
642
764
|
default=False,
|
765
|
+
help="Include archived services.",
|
643
766
|
)
|
644
767
|
@click.option(
|
645
768
|
"--max-items",
|
646
|
-
required=False,
|
647
|
-
default=10,
|
648
769
|
type=int,
|
649
|
-
|
650
|
-
|
770
|
+
callback=validate_max_items,
|
771
|
+
help="Max total items (only with --no-interactive).",
|
651
772
|
)
|
652
|
-
|
653
|
-
|
773
|
+
@click.option(
|
774
|
+
"--page-size",
|
775
|
+
type=int,
|
776
|
+
default=10,
|
777
|
+
show_default=True,
|
778
|
+
callback=validate_page_size,
|
779
|
+
help=f"Items per page (max {MAX_PAGE_SIZE}).",
|
780
|
+
)
|
781
|
+
@click.option(
|
782
|
+
"--interactive/--no-interactive",
|
783
|
+
default=True,
|
784
|
+
show_default=True,
|
785
|
+
help="Use interactive paging.",
|
786
|
+
)
|
787
|
+
@click.option(
|
788
|
+
"--sort",
|
789
|
+
help=(
|
790
|
+
"Sort by FIELD (prefix with '-' for desc). "
|
791
|
+
f"Allowed: {', '.join(f.value for f in ServiceSortField.__members__.values())}"
|
792
|
+
),
|
793
|
+
)
|
794
|
+
@click.option(
|
795
|
+
"-v",
|
796
|
+
"--verbose",
|
797
|
+
is_flag=True,
|
798
|
+
default=False,
|
799
|
+
help="Include full config in JSON output.",
|
800
|
+
)
|
801
|
+
@click.option(
|
802
|
+
"-j",
|
803
|
+
"--json",
|
804
|
+
"json_output",
|
805
|
+
is_flag=True,
|
806
|
+
default=False,
|
807
|
+
help="Emit structured JSON to stdout.",
|
808
|
+
)
|
809
|
+
def list( # noqa: PLR0913, A001
|
654
810
|
service_id: Optional[str],
|
655
|
-
|
811
|
+
name: Optional[str],
|
656
812
|
created_by_me: bool,
|
657
|
-
|
813
|
+
cloud: Optional[str],
|
814
|
+
project: Optional[str],
|
815
|
+
state_filter: List[str],
|
816
|
+
include_archived: bool,
|
817
|
+
max_items: Optional[int],
|
818
|
+
page_size: int,
|
819
|
+
sort: Optional[str],
|
820
|
+
json_output: bool,
|
821
|
+
interactive: bool,
|
822
|
+
verbose: bool,
|
658
823
|
):
|
659
|
-
"""List services based on the provided filters.
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
824
|
+
"""List services based on the provided filters."""
|
825
|
+
if max_items is not None and interactive:
|
826
|
+
raise click.UsageError("--max-items only allowed with --no-interactive")
|
827
|
+
|
828
|
+
# parse sort
|
829
|
+
sort_field, sort_order = _parse_sort_option(sort)
|
830
|
+
|
831
|
+
# normalize max_items
|
832
|
+
effective_max = max_items
|
833
|
+
if not interactive and effective_max is None:
|
834
|
+
stderr = Console(stderr=True)
|
835
|
+
stderr.print(
|
836
|
+
f"Defaulting to {NON_INTERACTIVE_DEFAULT_MAX_ITEMS} items in batch mode; "
|
837
|
+
"use --max-items to override."
|
838
|
+
)
|
839
|
+
effective_max = NON_INTERACTIVE_DEFAULT_MAX_ITEMS
|
840
|
+
|
841
|
+
console = Console()
|
842
|
+
stderr = Console(stderr=True)
|
843
|
+
|
844
|
+
# diagnostics
|
845
|
+
stderr.print("[bold]Listing services with:[/]")
|
846
|
+
stderr.print(f"• name = {name or '<any>'}")
|
847
|
+
stderr.print(f"• states = {', '.join(state_filter) or '<all>'}")
|
848
|
+
stderr.print(f"• created_by_me = {created_by_me}")
|
849
|
+
stderr.print(f"• include_archived= {include_archived}")
|
850
|
+
stderr.print(f"• sort = {sort or '<none>'}")
|
851
|
+
stderr.print(f"• mode = {'interactive' if interactive else 'batch'}")
|
852
|
+
stderr.print(f"• per-page limit = {page_size}")
|
853
|
+
stderr.print(f"• max-items total = {effective_max or 'all'}")
|
854
|
+
stderr.print(f"\nView your Services in the UI at {get_endpoint('/services')}\n")
|
855
|
+
|
856
|
+
creator_id = (
|
857
|
+
ServiceController().get_authenticated_user_id() if created_by_me else None
|
670
858
|
)
|
671
859
|
|
860
|
+
# choose formatter
|
861
|
+
if json_output:
|
862
|
+
|
863
|
+
def json_formatter(svc: ServiceStatus) -> Dict[str, Any]:
|
864
|
+
data = svc.to_dict()
|
865
|
+
if not verbose:
|
866
|
+
data.get("primary_version", {}).pop("config", None)
|
867
|
+
data.get("canary_version", {}).pop("config", None)
|
868
|
+
return data
|
869
|
+
|
870
|
+
formatter = json_formatter
|
871
|
+
else:
|
872
|
+
formatter = _format_service_output_data
|
873
|
+
|
874
|
+
total = 0
|
875
|
+
try:
|
876
|
+
iterator = anyscale.service.list(
|
877
|
+
service_id=service_id,
|
878
|
+
name=name,
|
879
|
+
state_filter=state_filter,
|
880
|
+
creator_id=creator_id,
|
881
|
+
cloud=cloud,
|
882
|
+
project=project,
|
883
|
+
include_archived=include_archived,
|
884
|
+
max_items=None if interactive else effective_max,
|
885
|
+
page_size=page_size,
|
886
|
+
sort_field=sort_field,
|
887
|
+
sort_order=sort_order,
|
888
|
+
)
|
889
|
+
total = display_list(
|
890
|
+
iterator=iter(iterator),
|
891
|
+
item_formatter=formatter,
|
892
|
+
table_creator=_create_service_list_table,
|
893
|
+
json_output=json_output,
|
894
|
+
page_size=page_size,
|
895
|
+
interactive=interactive,
|
896
|
+
max_items=effective_max,
|
897
|
+
console=console,
|
898
|
+
)
|
899
|
+
|
900
|
+
if not json_output:
|
901
|
+
if total > 0:
|
902
|
+
stderr.print(f"\nFetched {total} services.")
|
903
|
+
else:
|
904
|
+
stderr.print("\nNo services found.")
|
905
|
+
except Exception as e: # noqa: BLE001
|
906
|
+
log.error(f"Failed to list services: {e}")
|
907
|
+
sys.exit(1)
|
908
|
+
|
672
909
|
|
673
910
|
# TODO(mowen): Add cloud support for this when we refactor to new SDK method
|
674
911
|
@service_cli.command(name="rollback", help="Roll back a service.")
|
@@ -708,7 +945,6 @@ def rollback(
|
|
708
945
|
service_controller.rollback(service_id, max_surge_percent)
|
709
946
|
|
710
947
|
|
711
|
-
# TODO(mowen): Add cloud support for this when we refactor to new SDK method
|
712
948
|
@service_cli.command(name="terminate", help="Terminate a service.")
|
713
949
|
@click.option(
|
714
950
|
"--service-id", "--id", required=False, help="ID of service.",
|