anyscale 0.26.29__py3-none-any.whl → 0.26.31__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 (39) hide show
  1. anyscale/__init__.py +10 -0
  2. anyscale/_private/anyscale_client/anyscale_client.py +76 -60
  3. anyscale/_private/anyscale_client/common.py +39 -1
  4. anyscale/_private/anyscale_client/fake_anyscale_client.py +11 -0
  5. anyscale/_private/docgen/__main__.py +4 -0
  6. anyscale/_private/docgen/models.md +2 -2
  7. anyscale/client/README.md +2 -0
  8. anyscale/client/openapi_client/__init__.py +1 -0
  9. anyscale/client/openapi_client/api/default_api.py +118 -0
  10. anyscale/client/openapi_client/models/__init__.py +1 -0
  11. anyscale/client/openapi_client/models/baseimagesenum.py +68 -1
  12. anyscale/client/openapi_client/models/get_or_create_build_from_image_uri_request.py +207 -0
  13. anyscale/client/openapi_client/models/supportedbaseimagesenum.py +68 -1
  14. anyscale/cluster_compute.py +3 -8
  15. anyscale/commands/command_examples.py +10 -0
  16. anyscale/commands/job_queue_commands.py +295 -104
  17. anyscale/commands/list_util.py +14 -1
  18. anyscale/commands/machine_pool_commands.py +14 -2
  19. anyscale/commands/service_commands.py +6 -12
  20. anyscale/commands/workspace_commands_v2.py +462 -25
  21. anyscale/controllers/compute_config_controller.py +3 -19
  22. anyscale/controllers/job_controller.py +5 -210
  23. anyscale/job_queue/__init__.py +89 -0
  24. anyscale/job_queue/_private/job_queue_sdk.py +158 -0
  25. anyscale/job_queue/commands.py +130 -0
  26. anyscale/job_queue/models.py +284 -0
  27. anyscale/scripts.py +1 -1
  28. anyscale/sdk/anyscale_client/models/baseimagesenum.py +68 -1
  29. anyscale/sdk/anyscale_client/models/supportedbaseimagesenum.py +68 -1
  30. anyscale/shared_anyscale_utils/latest_ray_version.py +1 -1
  31. anyscale/utils/ssh_websocket_proxy.py +178 -0
  32. anyscale/version.py +1 -1
  33. {anyscale-0.26.29.dist-info → anyscale-0.26.31.dist-info}/METADATA +3 -1
  34. {anyscale-0.26.29.dist-info → anyscale-0.26.31.dist-info}/RECORD +39 -33
  35. {anyscale-0.26.29.dist-info → anyscale-0.26.31.dist-info}/LICENSE +0 -0
  36. {anyscale-0.26.29.dist-info → anyscale-0.26.31.dist-info}/NOTICE +0 -0
  37. {anyscale-0.26.29.dist-info → anyscale-0.26.31.dist-info}/WHEEL +0 -0
  38. {anyscale-0.26.29.dist-info → anyscale-0.26.31.dist-info}/entry_points.txt +0 -0
  39. {anyscale-0.26.29.dist-info → anyscale-0.26.31.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]]:
@@ -260,7 +260,13 @@ def list_machine_pools(format_: str) -> None:
260
260
  raise click.ClickException(f"Invalid output format '{format}'.")
261
261
 
262
262
 
263
- @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
+ )
264
270
  @click.option("--name", type=str, required=True, help="Provide a machine pool name.")
265
271
  @click.option("--cloud", type=str, required=True, help="Provide a cloud name.")
266
272
  def attach_machine_pool_to_cloud(name: str, cloud: str) -> None:
@@ -271,7 +277,13 @@ def attach_machine_pool_to_cloud(name: str, cloud: str) -> None:
271
277
  print(f"Attached machine pool '{name}' to cloud '{cloud}'.")
272
278
 
273
279
 
274
- @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
+ )
275
287
  @click.option("--name", type=str, required=True, help="Provide a machine pool name.")
276
288
  @click.option("--cloud", type=str, required=True, help="Provide a cloud name.")
277
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,
@@ -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