anyscale 0.26.51__py3-none-any.whl → 0.26.52__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 (46) hide show
  1. anyscale/_private/anyscale_client/README.md +1 -1
  2. anyscale/_private/anyscale_client/anyscale_client.py +178 -46
  3. anyscale/_private/anyscale_client/common.py +61 -2
  4. anyscale/_private/anyscale_client/fake_anyscale_client.py +145 -8
  5. anyscale/_private/docgen/__main__.py +34 -23
  6. anyscale/_private/docgen/generator.py +15 -18
  7. anyscale/_private/docgen/models.md +4 -2
  8. anyscale/_private/workload/workload_sdk.py +103 -8
  9. anyscale/client/README.md +3 -0
  10. anyscale/client/openapi_client/__init__.py +1 -0
  11. anyscale/client/openapi_client/api/default_api.py +249 -0
  12. anyscale/client/openapi_client/models/__init__.py +1 -0
  13. anyscale/client/openapi_client/models/baseimagesenum.py +83 -1
  14. anyscale/client/openapi_client/models/cloud_resource.py +59 -3
  15. anyscale/client/openapi_client/models/cloud_resource_gcp.py +59 -3
  16. anyscale/client/openapi_client/models/clouddeployment_response.py +121 -0
  17. anyscale/client/openapi_client/models/create_cloud_resource.py +59 -3
  18. anyscale/client/openapi_client/models/create_cloud_resource_gcp.py +59 -3
  19. anyscale/client/openapi_client/models/object_storage.py +2 -2
  20. anyscale/client/openapi_client/models/ray_runtime_env_config.py +57 -1
  21. anyscale/client/openapi_client/models/supportedbaseimagesenum.py +80 -1
  22. anyscale/cloud/models.py +1 -1
  23. anyscale/commands/cloud_commands.py +73 -70
  24. anyscale/commands/command_examples.py +28 -40
  25. anyscale/commands/project_commands.py +377 -106
  26. anyscale/controllers/cloud_controller.py +81 -86
  27. anyscale/job/_private/job_sdk.py +38 -20
  28. anyscale/project/__init__.py +101 -1
  29. anyscale/project/_private/project_sdk.py +90 -2
  30. anyscale/project/commands.py +188 -1
  31. anyscale/project/models.py +198 -2
  32. anyscale/sdk/anyscale_client/models/baseimagesenum.py +83 -1
  33. anyscale/sdk/anyscale_client/models/ray_runtime_env_config.py +57 -1
  34. anyscale/sdk/anyscale_client/models/supportedbaseimagesenum.py +80 -1
  35. anyscale/service/_private/service_sdk.py +2 -1
  36. anyscale/shared_anyscale_utils/latest_ray_version.py +1 -1
  37. anyscale/util.py +3 -0
  38. anyscale/utils/runtime_env.py +3 -1
  39. anyscale/version.py +1 -1
  40. {anyscale-0.26.51.dist-info → anyscale-0.26.52.dist-info}/METADATA +1 -1
  41. {anyscale-0.26.51.dist-info → anyscale-0.26.52.dist-info}/RECORD +46 -45
  42. {anyscale-0.26.51.dist-info → anyscale-0.26.52.dist-info}/WHEEL +0 -0
  43. {anyscale-0.26.51.dist-info → anyscale-0.26.52.dist-info}/entry_points.txt +0 -0
  44. {anyscale-0.26.51.dist-info → anyscale-0.26.52.dist-info}/licenses/LICENSE +0 -0
  45. {anyscale-0.26.51.dist-info → anyscale-0.26.52.dist-info}/licenses/NOTICE +0 -0
  46. {anyscale-0.26.51.dist-info → anyscale-0.26.52.dist-info}/top_level.txt +0 -0
@@ -1,22 +1,99 @@
1
- from typing import Optional
1
+ from io import StringIO
2
+ from json import dumps as json_dumps
3
+ import sys
4
+ from typing import Dict, Optional, Tuple
2
5
 
3
6
  import click
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+ import yaml
4
10
 
5
11
  import anyscale
6
12
  from anyscale.cli_logger import BlockLogger
7
13
  from anyscale.commands import command_examples
8
- from anyscale.commands.util import AnyscaleCommand, LegacyAnyscaleCommand, NotRequiredIf
14
+ from anyscale.commands.list_util import display_list
15
+ from anyscale.commands.util import AnyscaleCommand, NotRequiredIf
9
16
  from anyscale.controllers.project_controller import ProjectController
10
17
  from anyscale.project.models import (
11
18
  CreateProjectCollaborator,
12
19
  CreateProjectCollaborators,
20
+ Project,
21
+ ProjectMinimal,
22
+ ProjectSortField,
23
+ ProjectSortOrder,
13
24
  )
14
25
  from anyscale.project_utils import validate_project_name
15
- from anyscale.util import validate_non_negative_arg
26
+ from anyscale.util import (
27
+ AnyscaleJSONEncoder,
28
+ get_endpoint,
29
+ )
16
30
 
17
31
 
18
32
  log = BlockLogger()
19
33
 
34
+ MAX_PAGE_SIZE = 50
35
+ NON_INTERACTIVE_DEFAULT_MAX_ITEMS = 10
36
+
37
+
38
+ def _create_project_list_table(show_header: bool) -> Table:
39
+ table = Table(show_header=show_header, expand=True)
40
+ # NAME and ID: larger ratios, can wrap but never truncate
41
+ table.add_column(
42
+ "NAME", no_wrap=False, overflow="fold", ratio=3, min_width=15,
43
+ )
44
+ table.add_column(
45
+ "ID", no_wrap=False, overflow="fold", ratio=2, min_width=12,
46
+ )
47
+ # all other columns will wrap as needed
48
+ for heading in (
49
+ "DESCRIPTION",
50
+ "CREATED AT",
51
+ "CREATOR",
52
+ "PARENT CLOUD ID",
53
+ ):
54
+ table.add_column(
55
+ heading, no_wrap=False, overflow="fold", ratio=1, min_width=8,
56
+ )
57
+ return table
58
+
59
+
60
+ def _format_project_output_data(project: Project) -> Dict[str, str]:
61
+ return {
62
+ "name": project.name,
63
+ "id": project.id,
64
+ "description": project.description,
65
+ "created_at": project.created_at,
66
+ "creator": str(project.creator_id or ""),
67
+ "parent_cloud_id": str(project.parent_cloud_id or ""),
68
+ }
69
+
70
+
71
+ def _parse_sort_option(
72
+ sort: Optional[str],
73
+ ) -> Tuple[Optional[ProjectSortField], ProjectSortOrder]:
74
+ if not sort:
75
+ return None, ProjectSortOrder.ASC
76
+
77
+ # build case-insensitive map of allowed fields
78
+ allowed = {f.value.lower(): f.value for f in ProjectSortField.__members__.values()}
79
+
80
+ # detect leading '-' for descending
81
+ if sort.startswith("-"):
82
+ raw = sort[1:]
83
+ order = ProjectSortOrder.DESC
84
+ else:
85
+ raw = sort
86
+ order = ProjectSortOrder.ASC
87
+
88
+ key = raw.lower()
89
+ if key not in allowed:
90
+ allowed_names = ", ".join(sorted(allowed.values()))
91
+ raise click.BadParameter(
92
+ f"Invalid sort field '{raw}'. Allowed fields: {allowed_names}"
93
+ )
94
+
95
+ return ProjectSortField(allowed[key]), order
96
+
20
97
 
21
98
  @click.group(
22
99
  "project",
@@ -28,49 +105,316 @@ def project_cli() -> None:
28
105
 
29
106
 
30
107
  @project_cli.command(
31
- name="list",
32
- short_help="List projects for which you have access.",
33
- help="List projects for which you have access. By default, only projects created by you are listed.",
34
- cls=LegacyAnyscaleCommand,
35
- is_limited_support=True,
36
- legacy_prefix="anyscale project",
108
+ name="get",
109
+ help="Get details of a project.",
110
+ cls=AnyscaleCommand,
111
+ example=command_examples.PROJECT_GET_EXAMPLE,
37
112
  )
38
113
  @click.option(
39
- "--name", "-n", help="List information for a particular project.", type=str
114
+ "--id", "-i", type=str, required=True, help="ID of the project.",
40
115
  )
41
- @click.option("--json", help="Format output as JSON.", is_flag=True)
42
116
  @click.option(
43
- "--any-creator",
44
- "-a",
45
- help="[Deprecated] List projects created by any user.",
117
+ "--json",
118
+ "-j",
46
119
  is_flag=True,
47
- default=None,
48
- hidden=True,
120
+ default=False,
121
+ help="Output the details in a structured JSON format.",
122
+ )
123
+ def get(id: str, json: bool = False): # noqa: A002
124
+ try:
125
+ project: Project = anyscale.project.get(project_id=id)
126
+ except ValueError as e:
127
+ log.error(f"Error getting project details: {e}")
128
+ sys.exit(1)
129
+
130
+ console = Console()
131
+ if json:
132
+ json_str = json_dumps(project.to_dict(), indent=2, cls=AnyscaleJSONEncoder)
133
+ console.print_json(json=json_str)
134
+ else:
135
+ stream = StringIO()
136
+ yaml.dump(project.to_dict(), stream, sort_keys=False)
137
+ console.print(stream.getvalue(), end="")
138
+
139
+
140
+ @project_cli.command(
141
+ name="list",
142
+ help="List all projects with optional filters.",
143
+ cls=AnyscaleCommand,
144
+ example=command_examples.PROJECT_LIST_EXAMPLE,
49
145
  )
50
- @click.option("--created-by-me", help="List projects created by me only.", is_flag=True)
51
146
  @click.option(
52
- "--max-items",
53
- required=False,
54
- default=20,
147
+ "--name", "-n", type=str, help="A string to filter projects by name.",
148
+ )
149
+ @click.option(
150
+ "--creator", "-u", type=str, help="The ID of a creator to filter projects.",
151
+ )
152
+ @click.option(
153
+ "--cloud", "-c", type=str, help="The ID of a parent cloud to filter projects.",
154
+ )
155
+ @click.option(
156
+ "--include-defaults/--exclude-defaults",
157
+ default=True,
158
+ show_default=True,
159
+ help="Whether to include default projects.",
160
+ )
161
+ @click.option(
162
+ "--max-items", type=int, help="The maximum number of projects to return.",
163
+ )
164
+ @click.option(
165
+ "--page-size",
55
166
  type=int,
56
- help="Max items to show in list.",
57
- callback=validate_non_negative_arg,
167
+ default=10,
168
+ show_default=True,
169
+ help="The number of projects to return per page.",
170
+ )
171
+ @click.option(
172
+ "--sort",
173
+ help=(
174
+ "Sort by FIELD (prefix with '-' for desc). "
175
+ f"Allowed: {', '.join(ProjectSortField.__members__.values())}"
176
+ ),
177
+ )
178
+ @click.option(
179
+ "--interactive/--no-interactive",
180
+ default=True,
181
+ show_default=True,
182
+ help="Use interactive paging.",
183
+ )
184
+ @click.option(
185
+ "--json",
186
+ "-j",
187
+ is_flag=True,
188
+ default=False,
189
+ help="Output the list in a structured JSON format.",
58
190
  )
59
191
  def list( # noqa: A001
192
+ *,
193
+ name: Optional[str] = None,
194
+ creator: Optional[str] = None,
195
+ cloud: Optional[str] = None,
196
+ include_defaults: bool = True,
197
+ max_items: Optional[int] = None,
198
+ page_size: Optional[int] = None,
199
+ sort: Optional[str] = None,
200
+ interactive: bool = True,
201
+ json: bool = False,
202
+ ):
203
+
204
+ if max_items is not None and interactive:
205
+ raise click.UsageError("--max-items only allowed with --no-interactive")
206
+
207
+ sort_field, sort_order = _parse_sort_option(sort)
208
+
209
+ # normalize max_items
210
+ effective_max = max_items
211
+ if not interactive and effective_max is None:
212
+ stderr = Console(stderr=True)
213
+ stderr.print(
214
+ f"Defaulting to {NON_INTERACTIVE_DEFAULT_MAX_ITEMS} items in batch mode; "
215
+ "use --max-items to override."
216
+ )
217
+ effective_max = NON_INTERACTIVE_DEFAULT_MAX_ITEMS
218
+
219
+ console = Console()
220
+ stderr = Console(stderr=True)
221
+
222
+ # diagnostics
223
+ stderr.print("[bold]Listing projects with:[/]")
224
+ stderr.print(f"• name_contains = {name or '<any>'}")
225
+ stderr.print(f"• creator_id = {creator or '<any>'}")
226
+ stderr.print(f"• parent_cloud_id = {cloud or '<any>'}")
227
+ stderr.print(f"• include defaults = {include_defaults}")
228
+ stderr.print(f"• sort-field = {sort_field or '<none>'}")
229
+ stderr.print(f"• sort-order = {sort_order or '<none>'}")
230
+ stderr.print(f"• mode = {'interactive' if interactive else 'batch'}")
231
+ stderr.print(f"• per-page limit = {page_size}")
232
+ stderr.print(f"• max-items total = {effective_max or 'all'}")
233
+ stderr.print(f"\nView your Projects in the UI at {get_endpoint('/projects')}\n")
234
+
235
+ # choose formatter
236
+ if json:
237
+
238
+ def formatter(project):
239
+ return ProjectMinimal.from_dict(project.to_dict()).to_dict()
240
+
241
+ else:
242
+ formatter = _format_project_output_data
243
+
244
+ total = 0
245
+ try:
246
+ iterator = anyscale.project.list(
247
+ name_contains=name,
248
+ creator_id=creator,
249
+ parent_cloud_id=cloud,
250
+ include_defaults=include_defaults,
251
+ max_items=effective_max,
252
+ page_size=page_size,
253
+ sort_field=sort_field,
254
+ sort_order=sort_order,
255
+ )
256
+ total = display_list(
257
+ iterator=iter(iterator),
258
+ item_formatter=formatter,
259
+ table_creator=_create_project_list_table,
260
+ json_output=json,
261
+ page_size=page_size or MAX_PAGE_SIZE,
262
+ interactive=interactive,
263
+ max_items=effective_max,
264
+ console=console,
265
+ )
266
+
267
+ if not json:
268
+ if total > 0:
269
+ stderr.print(f"\nFetched {total} projects.")
270
+ else:
271
+ stderr.print("\nNo projects found.")
272
+ except Exception as e: # noqa: BLE001
273
+ log.error(f"Failed to list projects: {e}")
274
+ sys.exit(1)
275
+
276
+
277
+ @project_cli.command(
278
+ name="create",
279
+ help="Create a new project.",
280
+ cls=AnyscaleCommand,
281
+ example=command_examples.PROJECT_CREATE_EXAMPLE,
282
+ )
283
+ @click.option(
284
+ "--name", "-n", type=str, required=True, help="Name of the project.",
285
+ )
286
+ @click.option(
287
+ "--cloud", "-c", type=str, required=True, help="Parent cloud ID for the project.",
288
+ )
289
+ @click.option(
290
+ "--description", "-d", type=str, help="Description of the project.",
291
+ )
292
+ @click.option(
293
+ "--initial-cluster-config",
294
+ "-f",
295
+ type=str,
296
+ help="Initial cluster config for the project.",
297
+ )
298
+ def create(
60
299
  name: str,
61
- json: bool,
62
- created_by_me: bool,
63
- any_creator: Optional[bool],
64
- max_items: int,
300
+ cloud: str,
301
+ *,
302
+ description: Optional[str] = None,
303
+ initial_cluster_config: Optional[str] = None,
65
304
  ) -> None:
66
- if any_creator is not None:
67
- log.warning(
68
- "`--any-creator` and `-a` flags have been deprecated. "
69
- "`anyscale project list` now shows projects created by any user by default. "
70
- "If you would like to show projects created by you only, you can pass the --created-by-me flag"
305
+ try:
306
+ project_id: str = anyscale.project.create(
307
+ name,
308
+ cloud,
309
+ description=description or "",
310
+ initial_cluster_config=initial_cluster_config,
71
311
  )
72
- project_controller = ProjectController()
73
- project_controller.list(name, json, created_by_me, max_items)
312
+ except ValueError as e:
313
+ log.error(f"Error creating project: {e}")
314
+ sys.exit(1)
315
+
316
+ log.info(f"Created project '{name}' with ID: {project_id}")
317
+
318
+
319
+ @project_cli.command(
320
+ name="delete",
321
+ help="Delete a project.",
322
+ cls=AnyscaleCommand,
323
+ example=command_examples.PROJECT_DELETE_EXAMPLE,
324
+ )
325
+ @click.option(
326
+ "--id", "-i", type=str, required=True, help="ID of the project to delete.",
327
+ )
328
+ def delete(id: str): # noqa: A002
329
+ try:
330
+ anyscale.project.delete(id)
331
+ except ValueError as e:
332
+ log.error(f"Error deleting project: {e}")
333
+ sys.exit(1)
334
+
335
+ log.info(f"Deleted project '{id}'")
336
+
337
+
338
+ @project_cli.command(
339
+ name="get-default",
340
+ help="Get the default project for a cloud.",
341
+ cls=AnyscaleCommand,
342
+ example=command_examples.PROJECT_GET_DEFAULT_EXAMPLE,
343
+ )
344
+ @click.option(
345
+ "--cloud", "-c", type=str, required=True, help="Parent cloud ID for the project.",
346
+ )
347
+ @click.option(
348
+ "--json",
349
+ "-j",
350
+ is_flag=True,
351
+ default=False,
352
+ help="Output the project in a structured JSON format.",
353
+ )
354
+ def get_default(cloud: str, json: bool = False):
355
+ try:
356
+ project: Project = anyscale.project.get_default(cloud)
357
+ except ValueError as e:
358
+ log.error(f"Error getting default project for cloud '{cloud}': {e}")
359
+ sys.exit(1)
360
+
361
+ console = Console()
362
+ if json:
363
+ json_str = json_dumps(project.to_dict(), indent=2, cls=AnyscaleJSONEncoder)
364
+ console.print_json(json=json_str)
365
+ else:
366
+ stream = StringIO()
367
+ yaml.dump(project.to_dict(), stream, sort_keys=False)
368
+ console.print(stream.getvalue(), end="")
369
+
370
+
371
+ @project_cli.command(
372
+ name="add-collaborators",
373
+ help="Add collaborators to the project.",
374
+ cls=AnyscaleCommand,
375
+ example=command_examples.PROJECT_ADD_COLLABORATORS_EXAMPLE,
376
+ )
377
+ @click.option(
378
+ "--cloud",
379
+ "-c",
380
+ help="Name of the cloud that the project belongs to.",
381
+ required=True,
382
+ )
383
+ @click.option(
384
+ "--project",
385
+ "-p",
386
+ help="Name of the project to add collaborators to.",
387
+ required=True,
388
+ )
389
+ @click.option(
390
+ "--users-file",
391
+ help="Path to a YAML file containing a list of users to add to the project.",
392
+ required=True,
393
+ )
394
+ def add_collaborators(cloud: str, project: str, users_file: str) -> None:
395
+ collaborators = CreateProjectCollaborators.from_yaml(users_file)
396
+
397
+ try:
398
+ anyscale.project.add_collaborators(
399
+ cloud=cloud,
400
+ project=project,
401
+ collaborators=[
402
+ CreateProjectCollaborator(**collaborator)
403
+ for collaborator in collaborators.collaborators
404
+ ],
405
+ )
406
+ except ValueError as e:
407
+ log.error(f"Error adding collaborators to project: {e}")
408
+ return
409
+
410
+ log.info(
411
+ f"Successfully added {len(collaborators.collaborators)} collaborators to project {project}."
412
+ )
413
+
414
+
415
+ # ================================================
416
+ # LEGACY CODE
417
+ # ================================================
74
418
 
75
419
 
76
420
  def _validate_project_name(ctx, param, value) -> str: # noqa: ARG001
@@ -178,76 +522,3 @@ def init(project_id: Optional[str], name: Optional[str],) -> None:
178
522
 
179
523
  project_controller = ProjectController()
180
524
  project_controller.init(project_id, name, None, None)
181
-
182
-
183
- @project_cli.command(
184
- name="create",
185
- help="Create a new project.",
186
- cls=LegacyAnyscaleCommand,
187
- is_limited_support=True,
188
- legacy_prefix="anyscale project",
189
- )
190
- @click.option(
191
- "--name",
192
- "-n",
193
- help="Project name.",
194
- callback=_validate_project_name,
195
- prompt=True,
196
- default=_default_project_name(),
197
- )
198
- @click.option(
199
- "--parent-cloud-id",
200
- required=False,
201
- default=None,
202
- help=(
203
- "Cloud id that this project is associated with. This argument "
204
- "is only relevant if cloud isolation is enabled."
205
- ),
206
- )
207
- def create(name: str, parent_cloud_id: str) -> None:
208
- project_controller = ProjectController()
209
- project_controller.create(name, parent_cloud_id)
210
-
211
-
212
- @project_cli.command(
213
- name="add-collaborators",
214
- help="Add collaborators to the project.",
215
- cls=AnyscaleCommand,
216
- example=command_examples.PROJECT_ADD_COLLABORATORS_EXAMPLE,
217
- )
218
- @click.option(
219
- "--cloud",
220
- "-c",
221
- help="Name of the cloud that the project belongs to.",
222
- required=True,
223
- )
224
- @click.option(
225
- "--project",
226
- "-p",
227
- help="Name of the project to add collaborators to.",
228
- required=True,
229
- )
230
- @click.option(
231
- "--users-file",
232
- help="Path to a YAML file containing a list of users to add to the project.",
233
- required=True,
234
- )
235
- def add_collaborators(cloud: str, project: str, users_file: str,) -> None:
236
- collaborators = CreateProjectCollaborators.from_yaml(users_file)
237
-
238
- try:
239
- anyscale.project.add_collaborators(
240
- cloud=cloud,
241
- project=project,
242
- collaborators=[
243
- CreateProjectCollaborator(**collaborator)
244
- for collaborator in collaborators.collaborators
245
- ],
246
- )
247
- except ValueError as e:
248
- log.error(f"Error adding collaborators to project: {e}")
249
- return
250
-
251
- log.info(
252
- f"Successfully added {len(collaborators.collaborators)} collaborators to project {project}."
253
- )