anyscale 0.26.28__py3-none-any.whl → 0.26.30__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.
Files changed (54) hide show
  1. anyscale/__init__.py +10 -0
  2. anyscale/_private/anyscale_client/anyscale_client.py +69 -0
  3. anyscale/_private/anyscale_client/common.py +38 -0
  4. anyscale/_private/anyscale_client/fake_anyscale_client.py +11 -0
  5. anyscale/_private/docgen/__main__.py +4 -18
  6. anyscale/_private/docgen/api.md +0 -125
  7. anyscale/_private/docgen/models.md +0 -111
  8. anyscale/client/README.md +0 -6
  9. anyscale/client/openapi_client/__init__.py +0 -4
  10. anyscale/client/openapi_client/api/default_api.py +0 -228
  11. anyscale/client/openapi_client/models/__init__.py +0 -4
  12. anyscale/client/openapi_client/models/workload_info.py +59 -3
  13. anyscale/commands/command_examples.py +10 -0
  14. anyscale/commands/job_queue_commands.py +295 -104
  15. anyscale/commands/list_util.py +14 -1
  16. anyscale/commands/machine_pool_commands.py +25 -11
  17. anyscale/commands/service_commands.py +10 -14
  18. anyscale/commands/workspace_commands_v2.py +462 -25
  19. anyscale/controllers/job_controller.py +5 -210
  20. anyscale/job_queue/__init__.py +89 -0
  21. anyscale/job_queue/_private/job_queue_sdk.py +158 -0
  22. anyscale/job_queue/commands.py +130 -0
  23. anyscale/job_queue/models.py +284 -0
  24. anyscale/scripts.py +1 -1
  25. anyscale/sdk/anyscale_client/__init__.py +0 -11
  26. anyscale/sdk/anyscale_client/api/default_api.py +140 -1433
  27. anyscale/sdk/anyscale_client/models/__init__.py +0 -11
  28. anyscale/service/__init__.py +4 -1
  29. anyscale/service/_private/service_sdk.py +5 -0
  30. anyscale/service/commands.py +4 -2
  31. anyscale/utils/ssh_websocket_proxy.py +178 -0
  32. anyscale/version.py +1 -1
  33. {anyscale-0.26.28.dist-info → anyscale-0.26.30.dist-info}/METADATA +3 -1
  34. {anyscale-0.26.28.dist-info → anyscale-0.26.30.dist-info}/RECORD +39 -49
  35. anyscale/client/openapi_client/models/serve_deployment_fast_api_docs_status.py +0 -123
  36. anyscale/client/openapi_client/models/servedeploymentfastapidocsstatus_response.py +0 -121
  37. anyscale/client/openapi_client/models/web_terminal.py +0 -121
  38. anyscale/client/openapi_client/models/webterminal_response.py +0 -121
  39. anyscale/sdk/anyscale_client/models/cluster_environment_build_log_response.py +0 -123
  40. anyscale/sdk/anyscale_client/models/clusterenvironmentbuildlogresponse_response.py +0 -121
  41. anyscale/sdk/anyscale_client/models/create_cloud.py +0 -518
  42. anyscale/sdk/anyscale_client/models/object_storage_config.py +0 -122
  43. anyscale/sdk/anyscale_client/models/object_storage_config_s3.py +0 -256
  44. anyscale/sdk/anyscale_client/models/objectstorageconfig_response.py +0 -121
  45. anyscale/sdk/anyscale_client/models/session_operation.py +0 -266
  46. anyscale/sdk/anyscale_client/models/session_operation_type.py +0 -101
  47. anyscale/sdk/anyscale_client/models/sessionoperation_response.py +0 -121
  48. anyscale/sdk/anyscale_client/models/update_cloud.py +0 -150
  49. anyscale/sdk/anyscale_client/models/update_project.py +0 -150
  50. {anyscale-0.26.28.dist-info → anyscale-0.26.30.dist-info}/LICENSE +0 -0
  51. {anyscale-0.26.28.dist-info → anyscale-0.26.30.dist-info}/NOTICE +0 -0
  52. {anyscale-0.26.28.dist-info → anyscale-0.26.30.dist-info}/WHEEL +0 -0
  53. {anyscale-0.26.28.dist-info → anyscale-0.26.30.dist-info}/entry_points.txt +0 -0
  54. {anyscale-0.26.28.dist-info → anyscale-0.26.30.dist-info}/top_level.txt +0 -0
@@ -1,172 +1,363 @@
1
- from typing import List
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from enum import Enum
5
+ from functools import partial
6
+ from json import dumps as json_dumps
7
+ import sys
8
+ from typing import Dict, get_type_hints, List, Optional
2
9
 
3
10
  import click
11
+ from rich.console import Console
12
+ from rich.table import Table
4
13
 
5
14
  from anyscale.client.openapi_client.models.job_queue_sort_directive import (
6
15
  JobQueueSortDirective,
7
16
  )
8
17
  from anyscale.client.openapi_client.models.job_queue_sort_field import JobQueueSortField
18
+ from anyscale.client.openapi_client.models.session_state import SessionState
9
19
  from anyscale.client.openapi_client.models.sort_order import SortOrder
10
20
  from anyscale.commands import command_examples
21
+ from anyscale.commands.list_util import (
22
+ display_list,
23
+ MAX_PAGE_SIZE,
24
+ NON_INTERACTIVE_DEFAULT_MAX_ITEMS,
25
+ validate_page_size,
26
+ )
11
27
  from anyscale.commands.util import AnyscaleCommand
12
- from anyscale.controllers.job_controller import JobController, JobQueueView
13
- from anyscale.util import validate_non_negative_arg
28
+ import anyscale.job_queue
29
+ from anyscale.job_queue.models import JobQueueStatus, JobQueueStatusKeys
30
+ from anyscale.util import get_endpoint, get_user_info, validate_non_negative_arg
14
31
 
15
32
 
16
- @click.group(
17
- "job-queues", help="Interact with production job queues running on Anyscale."
18
- )
33
+ @click.group("job-queue", help="Manage Anyscale Job Queues.")
19
34
  def job_queue_cli() -> None:
20
35
  pass
21
36
 
22
37
 
23
- def parse_sort_fields(
24
- param: str, sort_fields: List[str],
25
- ) -> List[JobQueueSortDirective]:
26
- sort_directives = []
27
-
28
- for field_str in sort_fields:
29
- descending = field_str.startswith("-")
30
- raw_field = field_str.lstrip("-").upper()
31
-
32
- if raw_field not in JobQueueSortField.allowable_values:
33
- raise click.UsageError(
34
- f"{param} must be one of {', '.join([v.lower() for v in JobQueueSortField.allowable_values])}"
35
- )
38
+ class ViewOption(Enum):
39
+ DEFAULT = "default"
40
+ STATS = "stats"
41
+ ALL = "all"
36
42
 
37
- sort_directives.append(
38
- JobQueueSortDirective(
39
- sort_field=raw_field,
40
- sort_order=SortOrder.DESC if descending else SortOrder.ASC,
41
- )
42
- )
43
43
 
44
- return sort_directives
44
+ VIEW_COLUMNS: Dict[ViewOption, List[JobQueueStatusKeys]] = {
45
+ ViewOption.DEFAULT: [
46
+ "name",
47
+ "id",
48
+ "state",
49
+ "creator_email",
50
+ "project_id",
51
+ "created_at",
52
+ ],
53
+ ViewOption.STATS: [
54
+ "id",
55
+ "name",
56
+ "total_jobs",
57
+ "active_jobs",
58
+ "successful_jobs",
59
+ "failed_jobs",
60
+ ],
61
+ ViewOption.ALL: [
62
+ "name",
63
+ "id",
64
+ "state",
65
+ "creator_email",
66
+ "project_id",
67
+ "created_at",
68
+ "max_concurrency",
69
+ "idle_timeout_s",
70
+ "cloud_id",
71
+ "user_provided_id",
72
+ "execution_mode",
73
+ "total_jobs",
74
+ "active_jobs",
75
+ "successful_jobs",
76
+ "failed_jobs",
77
+ ],
78
+ }
45
79
 
46
80
 
47
81
  @job_queue_cli.command(
48
82
  name="list",
49
- short_help="List job queues.",
83
+ help="List job queues.",
50
84
  cls=AnyscaleCommand,
51
85
  example=command_examples.JOB_QUEUE_LIST,
52
86
  )
87
+ @click.option("--id", "job_queue_id", help="ID of a job queue.")
88
+ @click.option("--name", type=str, help="Filter by name.")
89
+ @click.option("--cloud", type=str, help="Filter by cloud.")
90
+ @click.option("--project", type=str, help="Filter by project.")
91
+ @click.option("--include-all-users/--only-mine", default=False)
53
92
  @click.option(
54
- "--include-all-users",
55
- is_flag=True,
56
- default=False,
57
- help="Include job queues not created by current user.",
93
+ "--cluster-status",
94
+ type=click.Choice(SessionState.allowable_values, case_sensitive=False),
95
+ help="Filter by cluster status.",
58
96
  )
59
97
  @click.option(
60
98
  "--view",
61
- type=click.Choice([v.name.lower() for v in JobQueueView], case_sensitive=False),
62
- default=JobQueueView.DEFAULT.name,
63
- help="Select which view to display.",
64
- callback=lambda _, __, value: JobQueueView[value.upper()],
99
+ type=click.Choice([opt.value for opt in ViewOption], case_sensitive=False),
100
+ default=ViewOption.DEFAULT.value,
101
+ help="Columns view.",
102
+ callback=lambda _ctx, _param, value: ViewOption(value),
65
103
  )
66
104
  @click.option(
67
- "--page",
68
- default=100,
105
+ "--page-size",
106
+ default=10,
69
107
  type=int,
70
- help="Page size (default 100).",
71
- callback=validate_non_negative_arg,
108
+ callback=validate_page_size,
109
+ help=f"Items per page (max {MAX_PAGE_SIZE}).",
72
110
  )
73
111
  @click.option(
74
112
  "--max-items",
75
- required=False,
76
113
  type=int,
77
- help="Max items to show in list (only valid in interactive mode).",
78
114
  callback=lambda ctx, param, value: validate_non_negative_arg(ctx, param, value)
79
115
  if value
80
116
  else None,
117
+ help="Non-interactive max items.",
81
118
  )
82
119
  @click.option(
83
120
  "--sort",
84
- "sorting_directives",
121
+ "sort_dirs",
85
122
  multiple=True,
86
- default=[JobQueueSortField.CREATED_AT],
87
- help=f"""
88
- Sort by column(s). Prefix column with - to sort in descending order.
89
- Supported columns: {', '.join([v.lower() for v in JobQueueSortField.allowable_values])}.
90
- """,
91
- callback=lambda _, __, value: parse_sort_fields("sort", list(value)),
123
+ default=["-created_at"],
124
+ callback=lambda _ctx, _param, values: _parse_sort_fields("sort", list(values)),
92
125
  )
126
+ @click.option("--no-interactive/--interactive", default=False)
93
127
  @click.option(
94
- "--interactive/--no-interactive",
95
- default=True,
96
- help="--no-interactive disables the default interactive mode.",
128
+ "--json", "json_output", is_flag=True, default=False, help="JSON output.",
97
129
  )
98
- def list_job_queues(
130
+ def list_job_queues( # noqa: PLR0913
131
+ job_queue_id: Optional[str],
132
+ name: Optional[str],
133
+ cloud: Optional[str],
134
+ project: Optional[str],
135
+ cluster_status: Optional[str],
99
136
  include_all_users: bool,
100
- view: JobQueueView,
101
- page: int,
102
- max_items: int,
103
- sorting_directives: List[JobQueueSortDirective],
104
- interactive: bool,
105
- ):
106
- if max_items is not None and interactive:
107
- raise click.UsageError("--max-items can only be used in non interactive mode.")
108
- job_controller = JobController()
109
- job_controller.list_job_queues(
110
- max_items=max_items,
111
- page_size=page,
137
+ view: ViewOption,
138
+ page_size: int,
139
+ max_items: Optional[int],
140
+ sort_dirs: List[JobQueueSortDirective],
141
+ no_interactive: bool,
142
+ json_output: bool,
143
+ ) -> None:
144
+ """List and page job queues according to filters and view."""
145
+ if max_items and not no_interactive:
146
+ raise click.UsageError("--max-items only in non-interactive mode")
147
+
148
+ effective_max = max_items or NON_INTERACTIVE_DEFAULT_MAX_ITEMS
149
+ console = Console()
150
+ stderr = Console(stderr=True)
151
+
152
+ _print_list_diagnostics(
153
+ stderr=stderr,
154
+ job_queue_id=job_queue_id,
155
+ name=name,
112
156
  include_all_users=include_all_users,
157
+ cloud=cloud,
158
+ project=project,
159
+ cluster_status=cluster_status,
113
160
  view=view,
114
- sorting_directives=sorting_directives,
115
- interactive=interactive,
161
+ sort_dirs=sort_dirs,
162
+ no_interactive=no_interactive,
163
+ page_size=page_size,
164
+ effective_max=effective_max,
116
165
  )
117
166
 
167
+ try:
168
+ user = get_user_info()
169
+ iterator = anyscale.job_queue.list(
170
+ job_queue_id=job_queue_id,
171
+ name=name,
172
+ creator_id=None if include_all_users else (user.id if user else None),
173
+ cloud=cloud,
174
+ project=project,
175
+ page_size=page_size,
176
+ max_items=None if not no_interactive else effective_max,
177
+ sorting_directives=sort_dirs,
178
+ )
179
+ cols = VIEW_COLUMNS[view]
180
+ table_fn = partial(_create_table, view)
181
+
182
+ def row_fn(jq: JobQueueStatus) -> Dict[str, str]:
183
+ data = _format_data(jq)
184
+ return {c: data[c] for c in cols}
185
+
186
+ total = display_list(
187
+ iterator=iter(iterator),
188
+ item_formatter=row_fn,
189
+ table_creator=table_fn,
190
+ json_output=json_output,
191
+ page_size=page_size,
192
+ interactive=not no_interactive,
193
+ max_items=effective_max,
194
+ console=console,
195
+ )
196
+ if not json_output:
197
+ stderr.print(f"Fetched {total} queues" if total else "No queues found.")
198
+
199
+ except Exception as e: # noqa: BLE001
200
+ stderr.print(f"Error: {e}", style="red")
201
+ sys.exit(1)
202
+
118
203
 
119
204
  @job_queue_cli.command(
120
205
  name="update",
121
- short_help="Update job queue.",
206
+ help="Update job queue settings.",
122
207
  cls=AnyscaleCommand,
123
208
  example=command_examples.JOB_QUEUE_UPDATE,
124
209
  )
210
+ @click.option("--id", "job_queue_id", required=True)
211
+ @click.option("--max-concurrency", type=int)
212
+ @click.option("--idle-timeout-s", type=int)
125
213
  @click.option(
126
- "--id", "job_queue_id", required=False, default=None, help="ID of the job queue."
127
- )
128
- @click.option(
129
- "--name",
130
- "job_queue_name",
131
- required=False,
132
- default=None,
133
- help="Name of the job queue.",
134
- )
135
- @click.option(
136
- "--max-concurrency",
137
- required=False,
138
- default=None,
139
- help="Maximum concurrency of the job queue",
140
- )
141
- @click.option(
142
- "--idle-timeout-s",
143
- required=False,
144
- default=None,
145
- help="Idle timeout of the job queue",
214
+ "--json", "json_output", is_flag=True, default=False, help="JSON output.",
146
215
  )
147
216
  def update_job_queue(
148
- job_queue_id: str, job_queue_name: str, max_concurrency: int, idle_timeout_s: int
149
- ):
150
- if job_queue_id is None and job_queue_name is None:
151
- raise click.ClickException("ID or name of job queue is required")
152
- job_controller = JobController()
153
- job_controller.update_job_queue(
154
- job_queue_id=job_queue_id,
155
- job_queue_name=job_queue_name,
156
- max_concurrency=max_concurrency,
157
- idle_timeout_s=idle_timeout_s,
158
- )
217
+ job_queue_id: str,
218
+ max_concurrency: Optional[int],
219
+ idle_timeout_s: Optional[int],
220
+ json_output: bool,
221
+ ) -> None:
222
+ """Update the max_concurrency or idle_timeout_s of a job queue."""
223
+ if max_concurrency is None and idle_timeout_s is None:
224
+ raise click.ClickException("Specify --max-concurrency or --idle-timeout-s")
225
+ stderr = Console(stderr=True)
226
+ stderr.print(f"Updating job queue '{job_queue_id}'...")
227
+ try:
228
+ jq = anyscale.job_queue.update(
229
+ job_queue_id=job_queue_id,
230
+ job_queue_name=None,
231
+ max_concurrency=max_concurrency,
232
+ idle_timeout_s=idle_timeout_s,
233
+ )
234
+ if json_output:
235
+ Console().print_json(json_dumps(_format_data(jq), indent=2))
236
+ else:
237
+ _display_single(jq, stderr, ViewOption.ALL)
238
+ except Exception as e: # noqa: BLE001
239
+ stderr.print(f"Update failed: {e}", style="red")
240
+ sys.exit(1)
159
241
 
160
242
 
161
243
  @job_queue_cli.command(
162
- name="info",
163
- short_help="Info of a job queue.",
244
+ name="status",
245
+ help="Show job queue details.",
164
246
  cls=AnyscaleCommand,
165
247
  example=command_examples.JOB_QUEUE_INFO,
166
248
  )
249
+ @click.option("--id", "job_queue_id", required=True)
167
250
  @click.option(
168
- "--id", "job_queue_id", required=True, default=None, help="ID of the job."
251
+ "--view",
252
+ type=click.Choice([opt.value for opt in ViewOption], case_sensitive=False),
253
+ default=ViewOption.DEFAULT.value,
254
+ help="Columns view.",
255
+ callback=lambda _ctx, _param, value: ViewOption(value),
169
256
  )
170
- def get_job_queue(job_queue_id: str):
171
- job_controller = JobController()
172
- job_controller.get_job_queue(job_queue_id=job_queue_id)
257
+ @click.option(
258
+ "--json", "json_output", is_flag=True, default=False, help="JSON output.",
259
+ )
260
+ def status(job_queue_id: str, view: ViewOption, json_output: bool,) -> None:
261
+ """Fetch and display a single job queue's details."""
262
+ stderr = Console(stderr=True)
263
+ stderr.print(f"Fetching job queue '{job_queue_id}'...")
264
+ try:
265
+ jq = anyscale.job_queue.status(job_queue_id=job_queue_id)
266
+ if json_output:
267
+ Console().print_json(json_dumps(_format_data(jq), indent=2))
268
+ else:
269
+ _display_single(jq, stderr, view)
270
+ except Exception as e: # noqa: BLE001
271
+ stderr.print(f"Failed: {e}", style="red")
272
+ sys.exit(1)
273
+
274
+
275
+ def _parse_sort_fields(
276
+ param: str, sort_fields: List[str],
277
+ ) -> List[JobQueueSortDirective]:
278
+ """Convert a list of string fields into JobQueueSortDirective objects."""
279
+ directives: List[JobQueueSortDirective] = []
280
+ opts = ", ".join(v.lower() for v in JobQueueSortField.allowable_values)
281
+ for field_str in sort_fields:
282
+ desc = field_str.startswith("-")
283
+ raw = field_str.lstrip("-").upper()
284
+ if raw not in JobQueueSortField.allowable_values:
285
+ raise click.UsageError(f"{param} must be one of {opts}")
286
+ directives.append(
287
+ JobQueueSortDirective(
288
+ sort_field=raw, sort_order=SortOrder.DESC if desc else SortOrder.ASC,
289
+ )
290
+ )
291
+ return directives
292
+
293
+
294
+ def _create_table(view: ViewOption, show_header: bool) -> Table:
295
+ """Create a Rich Table with columns based on the selected view."""
296
+ table = Table(show_header=show_header, expand=True)
297
+ for key in VIEW_COLUMNS[view]:
298
+ table.add_column(key.replace("_", " ").upper(), overflow="fold")
299
+ return table
300
+
301
+
302
+ def _format_data(jq: JobQueueStatus) -> Dict[str, str]:
303
+ """Format a JobQueueStatus object into a dictionary of strings for display."""
304
+ data = {}
305
+ for key in get_type_hints(JobQueueStatus):
306
+ value = getattr(jq, key, None)
307
+ if isinstance(value, datetime):
308
+ data[key] = value.strftime("%Y-%m-%d %H:%M:%S") if value else ""
309
+ elif value is None:
310
+ data[key] = ""
311
+ else:
312
+ data[key] = str(value)
313
+ return data
314
+
315
+
316
+ def _display_single(jq: JobQueueStatus, stderr: Console, view: ViewOption,) -> None:
317
+ """Display a single job queue's details in a table using the selected view.
318
+
319
+ Args:
320
+ jq: The JobQueueStatus object to display.
321
+ stderr: The Rich Console object to print to.
322
+ view: The ViewOption determining which columns to display.
323
+ """
324
+ table = _create_table(view, show_header=True)
325
+ data = _format_data(jq)
326
+ table.add_row(*(data[col] for col in VIEW_COLUMNS[view]))
327
+ stderr.print(table)
328
+
329
+
330
+ def _print_list_diagnostics( # noqa: PLR0913
331
+ stderr: Console,
332
+ job_queue_id: Optional[str],
333
+ name: Optional[str],
334
+ include_all_users: bool,
335
+ cloud: Optional[str],
336
+ project: Optional[str],
337
+ cluster_status: Optional[str],
338
+ view: ViewOption,
339
+ sort_dirs: List[JobQueueSortDirective],
340
+ no_interactive: bool,
341
+ page_size: int,
342
+ effective_max: int,
343
+ ) -> None:
344
+ """Prints diagnostic information for the list_job_queues command."""
345
+ stderr.print("[bold]Listing with:[/]")
346
+ stderr.print(f"id: {job_queue_id or '<any>'}")
347
+ stderr.print(f"name: {name or '<any>'}")
348
+ stderr.print(f"creator: {'all' if include_all_users else 'mine'}")
349
+ stderr.print(f"cloud: {cloud or '<any>'}")
350
+ stderr.print(f"project: {project or '<any>'}")
351
+ stderr.print(f"cluster: {cluster_status or '<any>'}")
352
+ stderr.print(f"view: {view.value}")
353
+
354
+ formatted_sort_dirs = [
355
+ f"{'-' if d.sort_order == SortOrder.DESC else ''}{(d.sort_field or '').lower()}"
356
+ for d in sort_dirs
357
+ ]
358
+ stderr.print(f"sort: {formatted_sort_dirs}")
359
+
360
+ stderr.print(f"mode: {'batch' if no_interactive else 'interactive'}")
361
+ stderr.print(f"page-size: {page_size}")
362
+ stderr.print(f"max-items: {effective_max}")
363
+ stderr.print(f"UI: {get_endpoint('/job-queues')}\n")
@@ -2,10 +2,23 @@ import itertools
2
2
  from json import dumps as json_dumps
3
3
  from typing import Any, Callable, Dict, Iterator, List, Optional
4
4
 
5
+ import click
5
6
  from rich.console import Console
6
7
  from rich.table import Table
7
8
 
8
- from anyscale.util import AnyscaleJSONEncoder
9
+ from anyscale.util import AnyscaleJSONEncoder, validate_non_negative_arg
10
+
11
+
12
+ MAX_PAGE_SIZE = 50
13
+ NON_INTERACTIVE_DEFAULT_MAX_ITEMS = 10
14
+
15
+
16
+ def validate_page_size(ctx, param, value):
17
+ """Click callback to validate page size argument."""
18
+ value = validate_non_negative_arg(ctx, param, value)
19
+ if value is not None and value > MAX_PAGE_SIZE:
20
+ raise click.BadParameter(f"must be less than or equal to {MAX_PAGE_SIZE}.")
21
+ return value
9
22
 
10
23
 
11
24
  def _paginate(iterator: Iterator[Any], page_size: Optional[int]) -> Iterator[List[Any]]:
@@ -113,8 +113,9 @@ def describe(name: str, format_: str) -> None:
113
113
  "PARTITION",
114
114
  "STATE",
115
115
  "WORKLOAD DETAILS",
116
- "WORKLOAD SCORE",
117
116
  "WORKLOAD START TIME",
117
+ "WORKLOAD CREATOR",
118
+ "WORKLOAD SCORE",
118
119
  "CLOUD INSTANCE ID",
119
120
  ]
120
121
  for row in scheduler_info.machines:
@@ -124,13 +125,16 @@ def describe(name: str, format_: str) -> None:
124
125
  row.machine_type,
125
126
  row.partition,
126
127
  row.allocation_state,
127
- f"{row.workload_info.workload_type}/{row.workload_info.workload_name}/{row.workload_info.workload_cloud}"
128
+ f"{row.workload_info.workload_type}/{row.workload_info.workload_name}/{row.workload_info.workload_project}/{row.workload_info.workload_cloud}"
128
129
  if row.workload_info.workload_name
129
130
  else "",
130
- row.workload_score,
131
131
  format_time(row.workload_info.workload_start_time)
132
132
  if row.workload_info.workload_name
133
133
  else "",
134
+ row.workload_info.workload_creator
135
+ if row.workload_info.workload_name
136
+ else "",
137
+ row.workload_score,
134
138
  row.cloud_instance_id,
135
139
  ]
136
140
  )
@@ -151,7 +155,7 @@ def describe(name: str, format_: str) -> None:
151
155
  "MACHINE TYPE",
152
156
  "WORKLOAD DETAILS",
153
157
  "WORKLOAD START TIME",
154
- "WORKLOAD CLOUD",
158
+ "WORKLOAD CREATOR",
155
159
  "PARTITION SCORES",
156
160
  ]
157
161
  for row in scheduler_info.requests:
@@ -159,11 +163,9 @@ def describe(name: str, format_: str) -> None:
159
163
  [
160
164
  row.size,
161
165
  row.machine_type,
162
- f"{row.workload_info.workload_type}/{row.workload_info.workload_name}",
163
- format_time(row.workload_info.workload_start_time)
164
- if row.workload_info.workload_name
165
- else "",
166
- row.workload_info.workload_cloud,
166
+ f"{row.workload_info.workload_type}/{row.workload_info.workload_name}/{row.workload_info.workload_project}/{row.workload_info.workload_cloud}",
167
+ format_time(row.workload_info.workload_start_time),
168
+ row.workload_info.workload_creator,
167
169
  row.partition_scores,
168
170
  ]
169
171
  )
@@ -258,7 +260,13 @@ def list_machine_pools(format_: str) -> None:
258
260
  raise click.ClickException(f"Invalid output format '{format}'.")
259
261
 
260
262
 
261
- @machine_pool_cli.command(name="attach", help="Attach a machine pool to a cloud.")
263
+ @machine_pool_cli.command(
264
+ name="attach",
265
+ help="Attach a machine pool to a cloud.",
266
+ cls=AnyscaleCommand,
267
+ example=command_examples.MACHINE_POOL_ATTACH_EXAMPLE,
268
+ is_beta=True,
269
+ )
262
270
  @click.option("--name", type=str, required=True, help="Provide a machine pool name.")
263
271
  @click.option("--cloud", type=str, required=True, help="Provide a cloud name.")
264
272
  def attach_machine_pool_to_cloud(name: str, cloud: str) -> None:
@@ -269,7 +277,13 @@ def attach_machine_pool_to_cloud(name: str, cloud: str) -> None:
269
277
  print(f"Attached machine pool '{name}' to cloud '{cloud}'.")
270
278
 
271
279
 
272
- @machine_pool_cli.command(name="detach", help="Detach a machine pool from a cloud.")
280
+ @machine_pool_cli.command(
281
+ name="detach",
282
+ help="Detach a machine pool from a cloud.",
283
+ cls=AnyscaleCommand,
284
+ example=command_examples.MACHINE_POOL_DETACH_EXAMPLE,
285
+ is_beta=True,
286
+ )
273
287
  @click.option("--name", type=str, required=True, help="Provide a machine pool name.")
274
288
  @click.option("--cloud", type=str, required=True, help="Provide a cloud name.")
275
289
  def detach_machine_pool_from_cloud(name: str, cloud: str) -> None:
@@ -13,7 +13,12 @@ import yaml
13
13
  from anyscale._private.models.image_uri import ImageURI
14
14
  from anyscale.cli_logger import BlockLogger
15
15
  from anyscale.commands import command_examples
16
- from anyscale.commands.list_util import display_list
16
+ from anyscale.commands.list_util import (
17
+ display_list,
18
+ MAX_PAGE_SIZE,
19
+ NON_INTERACTIVE_DEFAULT_MAX_ITEMS,
20
+ validate_page_size,
21
+ )
17
22
  from anyscale.commands.util import (
18
23
  AnyscaleCommand,
19
24
  convert_kv_strings_to_dict,
@@ -159,7 +164,7 @@ def _read_name_from_config_file(path: str):
159
164
  required=False,
160
165
  default=None,
161
166
  type=int,
162
- help="The percentage of traffic to send to the canary version of the service (0-100). This can be used to manually shift traffic toward (or away from) the canary version. If not provided, traffic will be shifted incrementally toward the canary version until it reaches 100. Not supported when using --in-place.",
167
+ help="The percentage of traffic to send to the canary version of the service (0-100). This can be used to manually shift traffic toward (or away from) the canary version. If not provided, traffic will be shifted incrementally toward the canary version until it reaches 100. Not supported when using --in-place. This is ignored when restarting a service or creating a new service.",
163
168
  )
164
169
  @click.option(
165
170
  "--max-surge-percent",
@@ -642,17 +647,6 @@ def rollout( # noqa: PLR0913
642
647
  )
643
648
 
644
649
 
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
650
  def validate_max_items(ctx, param, value):
657
651
  if value is None:
658
652
  return None
@@ -967,6 +961,8 @@ def terminate(
967
961
 
968
962
  This applies to both v1 and v2 services.
969
963
  """
964
+ # TODO: Remove service_controller and use the sdk method. Need to update the sdk method
965
+ # so that it can resolve either service name or config file to the service id.
970
966
  service_controller = ServiceController()
971
967
  service_id = service_controller.get_service_id(
972
968
  service_id=service_id,
@@ -975,7 +971,7 @@ def terminate(
975
971
  project_id=project_id,
976
972
  )
977
973
  try:
978
- anyscale.service.terminate(service_id)
974
+ anyscale.service.terminate(id=service_id)
979
975
  log.info(f"Service {service_id} terminate initiated.")
980
976
  log.info(
981
977
  f'View the service in the UI at {get_endpoint(f"/services/{service_id}")}'