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.
- anyscale/_private/anyscale_client/README.md +1 -1
- anyscale/_private/anyscale_client/anyscale_client.py +178 -46
- anyscale/_private/anyscale_client/common.py +61 -2
- anyscale/_private/anyscale_client/fake_anyscale_client.py +145 -8
- anyscale/_private/docgen/__main__.py +34 -23
- anyscale/_private/docgen/generator.py +15 -18
- anyscale/_private/docgen/models.md +4 -2
- anyscale/_private/workload/workload_sdk.py +103 -8
- anyscale/client/README.md +3 -0
- anyscale/client/openapi_client/__init__.py +1 -0
- anyscale/client/openapi_client/api/default_api.py +249 -0
- anyscale/client/openapi_client/models/__init__.py +1 -0
- anyscale/client/openapi_client/models/baseimagesenum.py +83 -1
- anyscale/client/openapi_client/models/cloud_resource.py +59 -3
- anyscale/client/openapi_client/models/cloud_resource_gcp.py +59 -3
- anyscale/client/openapi_client/models/clouddeployment_response.py +121 -0
- anyscale/client/openapi_client/models/create_cloud_resource.py +59 -3
- anyscale/client/openapi_client/models/create_cloud_resource_gcp.py +59 -3
- anyscale/client/openapi_client/models/object_storage.py +2 -2
- anyscale/client/openapi_client/models/ray_runtime_env_config.py +57 -1
- anyscale/client/openapi_client/models/supportedbaseimagesenum.py +80 -1
- anyscale/cloud/models.py +1 -1
- anyscale/commands/cloud_commands.py +73 -70
- anyscale/commands/command_examples.py +28 -40
- anyscale/commands/project_commands.py +377 -106
- anyscale/controllers/cloud_controller.py +81 -86
- anyscale/job/_private/job_sdk.py +38 -20
- anyscale/project/__init__.py +101 -1
- anyscale/project/_private/project_sdk.py +90 -2
- anyscale/project/commands.py +188 -1
- anyscale/project/models.py +198 -2
- anyscale/sdk/anyscale_client/models/baseimagesenum.py +83 -1
- anyscale/sdk/anyscale_client/models/ray_runtime_env_config.py +57 -1
- anyscale/sdk/anyscale_client/models/supportedbaseimagesenum.py +80 -1
- anyscale/service/_private/service_sdk.py +2 -1
- anyscale/shared_anyscale_utils/latest_ray_version.py +1 -1
- anyscale/util.py +3 -0
- anyscale/utils/runtime_env.py +3 -1
- anyscale/version.py +1 -1
- {anyscale-0.26.51.dist-info → anyscale-0.26.52.dist-info}/METADATA +1 -1
- {anyscale-0.26.51.dist-info → anyscale-0.26.52.dist-info}/RECORD +46 -45
- {anyscale-0.26.51.dist-info → anyscale-0.26.52.dist-info}/WHEEL +0 -0
- {anyscale-0.26.51.dist-info → anyscale-0.26.52.dist-info}/entry_points.txt +0 -0
- {anyscale-0.26.51.dist-info → anyscale-0.26.52.dist-info}/licenses/LICENSE +0 -0
- {anyscale-0.26.51.dist-info → anyscale-0.26.52.dist-info}/licenses/NOTICE +0 -0
- {anyscale-0.26.51.dist-info → anyscale-0.26.52.dist-info}/top_level.txt +0 -0
@@ -1,22 +1,99 @@
|
|
1
|
-
from
|
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.
|
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
|
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="
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
"--
|
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
|
-
"--
|
44
|
-
"-
|
45
|
-
help="[Deprecated] List projects created by any user.",
|
117
|
+
"--json",
|
118
|
+
"-j",
|
46
119
|
is_flag=True,
|
47
|
-
default=
|
48
|
-
|
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
|
-
"--
|
53
|
-
|
54
|
-
|
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
|
-
|
57
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
300
|
+
cloud: str,
|
301
|
+
*,
|
302
|
+
description: Optional[str] = None,
|
303
|
+
initial_cluster_config: Optional[str] = None,
|
65
304
|
) -> None:
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
73
|
-
|
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
|
-
)
|