anyscale 0.26.69__py3-none-any.whl → 0.26.71__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 +126 -3
- anyscale/_private/anyscale_client/common.py +51 -2
- anyscale/_private/anyscale_client/fake_anyscale_client.py +103 -11
- anyscale/client/README.md +43 -4
- anyscale/client/openapi_client/__init__.py +30 -4
- anyscale/client/openapi_client/api/default_api.py +1769 -27
- anyscale/client/openapi_client/models/__init__.py +30 -4
- anyscale/client/openapi_client/models/api_key_info.py +29 -3
- anyscale/client/openapi_client/models/apply_autoscaling_config_update_model.py +350 -0
- anyscale/client/openapi_client/models/apply_multi_version_update_weights_update_model.py +152 -0
- anyscale/client/openapi_client/models/apply_production_service_multi_version_v2_model.py +207 -0
- anyscale/client/openapi_client/models/apply_production_service_v2_model.py +31 -3
- anyscale/client/openapi_client/models/apply_version_weight_update_model.py +181 -0
- anyscale/client/openapi_client/models/backend_server_api_product_models_catalog_client_models_table_metadata.py +546 -0
- anyscale/client/openapi_client/models/backend_server_api_product_models_data_catalogs_table_metadata.py +178 -0
- anyscale/client/openapi_client/models/baseimagesenum.py +139 -1
- anyscale/client/openapi_client/models/catalog_metadata.py +150 -0
- anyscale/client/openapi_client/models/cloud_data_bucket_file_type.py +2 -1
- anyscale/client/openapi_client/models/{oauthconnectionresponse_response.py → clouddeployment_response.py} +11 -11
- anyscale/client/openapi_client/models/column_info.py +265 -0
- anyscale/client/openapi_client/models/compute_node_type.py +29 -1
- anyscale/client/openapi_client/models/connection_metadata.py +206 -0
- anyscale/client/openapi_client/models/create_experimental_workspace.py +29 -1
- anyscale/client/openapi_client/models/create_workspace_from_template.py +29 -1
- anyscale/client/openapi_client/models/create_workspace_template_version.py +59 -3
- anyscale/client/openapi_client/models/data_catalog.py +45 -31
- anyscale/client/openapi_client/models/data_catalog_connection.py +74 -58
- anyscale/client/openapi_client/models/{ha_job_event_level.py → data_catalog_object_type.py} +7 -8
- anyscale/client/openapi_client/models/data_catalog_schema.py +324 -0
- anyscale/client/openapi_client/models/data_catalog_table.py +437 -0
- anyscale/client/openapi_client/models/data_catalog_volume.py +437 -0
- anyscale/client/openapi_client/models/datacatalogschema_list_response.py +147 -0
- anyscale/client/openapi_client/models/datacatalogtable_list_response.py +147 -0
- anyscale/client/openapi_client/models/datacatalogvolume_list_response.py +147 -0
- anyscale/client/openapi_client/models/decorated_list_service_api_model.py +58 -1
- anyscale/client/openapi_client/models/decorated_production_service_v2_api_model.py +60 -3
- anyscale/client/openapi_client/models/decorated_serve_deployment.py +27 -1
- anyscale/client/openapi_client/models/decorated_service_event_api_model.py +3 -3
- anyscale/client/openapi_client/models/decoratedproductionservicev2_versionapimodel_response.py +121 -0
- anyscale/client/openapi_client/models/describe_machine_pool_machines_filters.py +33 -5
- anyscale/client/openapi_client/models/describe_machine_pool_requests_filters.py +33 -5
- anyscale/client/openapi_client/models/describe_machine_pool_workloads_filters.py +33 -5
- anyscale/client/openapi_client/models/{service_event_level.py → entity_type.py} +9 -9
- anyscale/client/openapi_client/models/event_level.py +2 -1
- anyscale/client/openapi_client/models/job_event_fields.py +206 -0
- anyscale/client/openapi_client/models/machine_type_partition_filter.py +152 -0
- anyscale/client/openapi_client/models/partition_info.py +30 -1
- anyscale/client/openapi_client/models/physical_resources.py +178 -0
- anyscale/client/openapi_client/models/production_job_event.py +3 -3
- anyscale/client/openapi_client/models/rollout_strategy.py +2 -1
- anyscale/client/openapi_client/models/schema_metadata.py +150 -0
- anyscale/client/openapi_client/models/service_event_fields.py +318 -0
- anyscale/client/openapi_client/models/sso_config.py +18 -18
- anyscale/client/openapi_client/models/supportedbaseimagesenum.py +139 -1
- anyscale/client/openapi_client/models/table_data_preview.py +209 -0
- anyscale/client/openapi_client/models/task_summary_config.py +29 -3
- anyscale/client/openapi_client/models/task_table_config.py +29 -3
- anyscale/client/openapi_client/models/unified_event.py +377 -0
- anyscale/client/openapi_client/models/unified_origin_filter.py +113 -0
- anyscale/client/openapi_client/models/unifiedevent_list_response.py +147 -0
- anyscale/client/openapi_client/models/volume_metadata.py +150 -0
- anyscale/client/openapi_client/models/worker_node_type.py +29 -1
- anyscale/client/openapi_client/models/workspace_event_fields.py +122 -0
- anyscale/client/openapi_client/models/workspace_template_version.py +58 -1
- anyscale/client/openapi_client/models/workspace_template_version_data_object.py +58 -1
- anyscale/cloud/models.py +2 -2
- anyscale/commands/cloud_commands.py +133 -2
- anyscale/commands/job_commands.py +121 -1
- anyscale/commands/job_queue_commands.py +99 -2
- anyscale/commands/service_commands.py +267 -67
- anyscale/commands/setup_k8s.py +546 -31
- anyscale/commands/util.py +104 -1
- anyscale/commands/workspace_commands.py +123 -5
- anyscale/commands/workspace_commands_v2.py +17 -1
- anyscale/compute_config/_private/compute_config_sdk.py +25 -12
- anyscale/compute_config/models.py +15 -0
- anyscale/controllers/cloud_controller.py +15 -2
- anyscale/controllers/job_controller.py +12 -0
- anyscale/controllers/kubernetes_verifier.py +80 -66
- anyscale/controllers/workspace_controller.py +67 -5
- anyscale/job/_private/job_sdk.py +50 -2
- anyscale/job/commands.py +3 -0
- anyscale/job/models.py +16 -0
- anyscale/job_queue/__init__.py +37 -1
- anyscale/job_queue/_private/job_queue_sdk.py +28 -1
- anyscale/job_queue/commands.py +61 -1
- anyscale/sdk/anyscale_client/__init__.py +1 -0
- anyscale/sdk/anyscale_client/api/default_api.py +12 -2
- anyscale/sdk/anyscale_client/models/__init__.py +1 -0
- anyscale/sdk/anyscale_client/models/apply_production_service_v2_model.py +31 -3
- anyscale/sdk/anyscale_client/models/apply_service_model.py +31 -3
- anyscale/sdk/anyscale_client/models/baseimagesenum.py +139 -1
- anyscale/sdk/anyscale_client/models/compute_node_type.py +29 -1
- anyscale/sdk/anyscale_client/models/physical_resources.py +178 -0
- anyscale/sdk/anyscale_client/models/rollout_strategy.py +2 -1
- anyscale/sdk/anyscale_client/models/supportedbaseimagesenum.py +139 -1
- anyscale/sdk/anyscale_client/models/worker_node_type.py +29 -1
- anyscale/service/__init__.py +51 -3
- anyscale/service/_private/service_sdk.py +481 -58
- anyscale/service/commands.py +90 -4
- anyscale/service/models.py +56 -0
- anyscale/shared_anyscale_utils/latest_ray_version.py +1 -1
- anyscale/version.py +1 -1
- anyscale/workspace/_private/workspace_sdk.py +1 -0
- anyscale/workspace/models.py +19 -0
- {anyscale-0.26.69.dist-info → anyscale-0.26.71.dist-info}/METADATA +1 -1
- {anyscale-0.26.69.dist-info → anyscale-0.26.71.dist-info}/RECORD +112 -85
- anyscale/client/openapi_client/models/o_auth_connection_response.py +0 -229
- {anyscale-0.26.69.dist-info → anyscale-0.26.71.dist-info}/WHEEL +0 -0
- {anyscale-0.26.69.dist-info → anyscale-0.26.71.dist-info}/entry_points.txt +0 -0
- {anyscale-0.26.69.dist-info → anyscale-0.26.71.dist-info}/licenses/LICENSE +0 -0
- {anyscale-0.26.69.dist-info → anyscale-0.26.71.dist-info}/licenses/NOTICE +0 -0
- {anyscale-0.26.69.dist-info → anyscale-0.26.71.dist-info}/top_level.txt +0 -0
|
@@ -23,7 +23,10 @@ from anyscale.client.openapi_client.models import (
|
|
|
23
23
|
from anyscale.client.openapi_client.models.compute_stack import ComputeStack
|
|
24
24
|
from anyscale.cloud.models import CreateCloudCollaborator, CreateCloudCollaborators
|
|
25
25
|
from anyscale.commands import command_examples
|
|
26
|
-
from anyscale.commands.setup_k8s import
|
|
26
|
+
from anyscale.commands.setup_k8s import (
|
|
27
|
+
setup_kubernetes_cloud,
|
|
28
|
+
setup_kubernetes_cloud_resource,
|
|
29
|
+
)
|
|
27
30
|
from anyscale.commands.util import AnyscaleCommand, OptionPromptNull
|
|
28
31
|
from anyscale.controllers.cloud_controller import CloudController
|
|
29
32
|
from anyscale.util import (
|
|
@@ -381,6 +384,134 @@ def cloud_resource_create(
|
|
|
381
384
|
print(e)
|
|
382
385
|
|
|
383
386
|
|
|
387
|
+
@cloud_resource_group.command(
|
|
388
|
+
name="setup",
|
|
389
|
+
help="Set up cloud resources for an existing cloud on a Kubernetes cluster.",
|
|
390
|
+
cls=AnyscaleCommand,
|
|
391
|
+
is_alpha=True,
|
|
392
|
+
)
|
|
393
|
+
@click.option(
|
|
394
|
+
"--provider",
|
|
395
|
+
help="The cloud provider type.",
|
|
396
|
+
required=True,
|
|
397
|
+
type=click.Choice(["aws", "gcp"], case_sensitive=False),
|
|
398
|
+
)
|
|
399
|
+
@click.option(
|
|
400
|
+
"--region", help="Region to set up the resources in.", required=True,
|
|
401
|
+
)
|
|
402
|
+
@click.option(
|
|
403
|
+
"--stack",
|
|
404
|
+
help="The compute stack to use (only k8s is supported for this command).",
|
|
405
|
+
required=False,
|
|
406
|
+
type=click.Choice(["k8s"], case_sensitive=False),
|
|
407
|
+
default="k8s",
|
|
408
|
+
show_default=True,
|
|
409
|
+
)
|
|
410
|
+
@click.option(
|
|
411
|
+
"--cloud",
|
|
412
|
+
help="The name of the existing cloud to add resources to. Either this or --cloud-id is required.",
|
|
413
|
+
type=str,
|
|
414
|
+
required=False,
|
|
415
|
+
)
|
|
416
|
+
@click.option(
|
|
417
|
+
"--cloud-id",
|
|
418
|
+
help="The ID of the existing cloud to add resources to. Either this or --cloud is required.",
|
|
419
|
+
type=str,
|
|
420
|
+
required=False,
|
|
421
|
+
)
|
|
422
|
+
@click.option(
|
|
423
|
+
"--cluster-name", help="Kubernetes cluster name.", required=True, type=str,
|
|
424
|
+
)
|
|
425
|
+
@click.option(
|
|
426
|
+
"--namespace",
|
|
427
|
+
help="Kubernetes namespace for Anyscale operator.",
|
|
428
|
+
required=False,
|
|
429
|
+
type=str,
|
|
430
|
+
default="anyscale-operator",
|
|
431
|
+
)
|
|
432
|
+
@click.option(
|
|
433
|
+
"--project-id",
|
|
434
|
+
help="Globally Unique project ID for GCP clouds (e.g., my-project-abc123)",
|
|
435
|
+
required=False,
|
|
436
|
+
type=str,
|
|
437
|
+
)
|
|
438
|
+
@click.option(
|
|
439
|
+
"--functional-verify",
|
|
440
|
+
help="Verify the cloud is functional. This will check that the cloud can launch workspace/service.",
|
|
441
|
+
required=False,
|
|
442
|
+
is_flag=False,
|
|
443
|
+
flag_value="workspace",
|
|
444
|
+
)
|
|
445
|
+
@click.option(
|
|
446
|
+
"--yes", "-y", is_flag=True, default=False, help="Skip asking for confirmation."
|
|
447
|
+
)
|
|
448
|
+
@click.option(
|
|
449
|
+
"--values-file",
|
|
450
|
+
help="Path to save the generated Helm values file (default: auto-generated with timestamp).",
|
|
451
|
+
required=False,
|
|
452
|
+
type=str,
|
|
453
|
+
)
|
|
454
|
+
@click.option(
|
|
455
|
+
"--debug", is_flag=True, default=False, help="Enable debug logging.",
|
|
456
|
+
)
|
|
457
|
+
@click.option(
|
|
458
|
+
"--operator-chart",
|
|
459
|
+
help="Path to operator chart (skips helm repo add/update).",
|
|
460
|
+
required=False,
|
|
461
|
+
type=str,
|
|
462
|
+
hidden=True,
|
|
463
|
+
)
|
|
464
|
+
@click.option(
|
|
465
|
+
"--resource-name",
|
|
466
|
+
help="Name for the cloud resource (optional, will be auto-generated if not provided)",
|
|
467
|
+
required=False,
|
|
468
|
+
type=str,
|
|
469
|
+
default=None,
|
|
470
|
+
)
|
|
471
|
+
def cloud_resource_setup( # noqa: PLR0913
|
|
472
|
+
provider: str,
|
|
473
|
+
region: str,
|
|
474
|
+
stack: str,
|
|
475
|
+
cloud: Optional[str],
|
|
476
|
+
cloud_id: Optional[str],
|
|
477
|
+
cluster_name: str,
|
|
478
|
+
namespace: str,
|
|
479
|
+
project_id: Optional[str],
|
|
480
|
+
functional_verify: Optional[str],
|
|
481
|
+
yes: bool,
|
|
482
|
+
values_file: Optional[str],
|
|
483
|
+
debug: bool,
|
|
484
|
+
operator_chart: Optional[str],
|
|
485
|
+
resource_name: Optional[str],
|
|
486
|
+
) -> None:
|
|
487
|
+
"""
|
|
488
|
+
Set up cloud resources for an existing Anyscale cloud on a Kubernetes cluster.
|
|
489
|
+
|
|
490
|
+
This command sets up infrastructure (S3/GCS buckets, IAM roles, etc.) and installs
|
|
491
|
+
the Anyscale operator on your Kubernetes cluster, then creates a cloud resource in
|
|
492
|
+
an existing cloud instead of registering a new cloud.
|
|
493
|
+
"""
|
|
494
|
+
# Validate stack
|
|
495
|
+
if stack != "k8s":
|
|
496
|
+
raise click.ClickException("Only --stack=k8s is supported for this command.")
|
|
497
|
+
|
|
498
|
+
setup_kubernetes_cloud_resource(
|
|
499
|
+
provider=provider,
|
|
500
|
+
region=region,
|
|
501
|
+
cloud_name=cloud,
|
|
502
|
+
cloud_id=cloud_id,
|
|
503
|
+
cluster_name=cluster_name,
|
|
504
|
+
namespace=namespace,
|
|
505
|
+
project_id=project_id,
|
|
506
|
+
functional_verify=bool(functional_verify),
|
|
507
|
+
yes=yes,
|
|
508
|
+
values_file=values_file,
|
|
509
|
+
debug=debug,
|
|
510
|
+
operator_chart=operator_chart,
|
|
511
|
+
resource_name=resource_name,
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
|
|
384
515
|
@cloud_resource_group.command(
|
|
385
516
|
name="delete",
|
|
386
517
|
help="Remove a cloud resource from an existing cloud.",
|
|
@@ -451,7 +582,7 @@ def cloud_resource_delete(cloud: str, resource: str, yes: bool,) -> None:
|
|
|
451
582
|
@click.option(
|
|
452
583
|
"--resources-file",
|
|
453
584
|
"-f",
|
|
454
|
-
help="EXPERIMENTAL: Path to a YAML file defining a list of cloud resources. Schema: https://docs.anyscale.com/reference/cloud/#cloudresource.",
|
|
585
|
+
help="EXPERIMENTAL: Path to a YAML file defining a single cloud resource or a list of cloud resources. Schema: https://docs.anyscale.com/reference/cloud/#cloudresource.",
|
|
455
586
|
required=False,
|
|
456
587
|
)
|
|
457
588
|
@click.option(
|
|
@@ -5,17 +5,31 @@ from subprocess import list2cmdline
|
|
|
5
5
|
from typing import List, Optional, Tuple
|
|
6
6
|
|
|
7
7
|
import click
|
|
8
|
+
from rich.console import Console
|
|
8
9
|
import yaml
|
|
9
10
|
|
|
10
11
|
import anyscale
|
|
11
12
|
from anyscale._private.models.image_uri import ImageURI
|
|
13
|
+
from anyscale.authenticate import get_auth_api_client
|
|
12
14
|
from anyscale.cli_logger import BlockLogger
|
|
15
|
+
from anyscale.client.openapi_client.models.delete_resource_tags_request import (
|
|
16
|
+
DeleteResourceTagsRequest,
|
|
17
|
+
)
|
|
13
18
|
from anyscale.client.openapi_client.models.ha_job_states import HaJobStates
|
|
19
|
+
from anyscale.client.openapi_client.models.resource_tag_resource_type import (
|
|
20
|
+
ResourceTagResourceType,
|
|
21
|
+
)
|
|
22
|
+
from anyscale.client.openapi_client.models.upsert_resource_tags_request import (
|
|
23
|
+
UpsertResourceTagsRequest,
|
|
24
|
+
)
|
|
14
25
|
from anyscale.commands import command_examples
|
|
15
26
|
from anyscale.commands.util import (
|
|
16
27
|
AnyscaleCommand,
|
|
28
|
+
build_kv_table,
|
|
17
29
|
convert_kv_strings_to_dict,
|
|
18
30
|
override_env_vars,
|
|
31
|
+
parse_repeatable_tags_to_dict,
|
|
32
|
+
parse_tags_kv_to_str_map,
|
|
19
33
|
)
|
|
20
34
|
from anyscale.controllers.job_controller import JobController
|
|
21
35
|
from anyscale.job.models import JobConfig, JobLogMode, JobState, JobStatus
|
|
@@ -129,6 +143,12 @@ def job_cli() -> None:
|
|
|
129
143
|
type=str,
|
|
130
144
|
help="Python modules to be available for import in the Ray workers. Each entry must be a path to a local directory.",
|
|
131
145
|
)
|
|
146
|
+
@click.option(
|
|
147
|
+
"--tag",
|
|
148
|
+
"tags",
|
|
149
|
+
multiple=True,
|
|
150
|
+
help="Tag in key=value (or key:value) format. Repeat to add multiple.",
|
|
151
|
+
)
|
|
132
152
|
@click.option(
|
|
133
153
|
"--cloud",
|
|
134
154
|
required=False,
|
|
@@ -182,6 +202,7 @@ def submit( # noqa: PLR0912 PLR0913 C901
|
|
|
182
202
|
exclude: Tuple[str],
|
|
183
203
|
requirements: Optional[str],
|
|
184
204
|
py_module: Tuple[str],
|
|
205
|
+
tags: Tuple[str],
|
|
185
206
|
cloud: Optional[str],
|
|
186
207
|
project: Optional[str],
|
|
187
208
|
max_retries: Optional[int],
|
|
@@ -336,6 +357,11 @@ and override the entrypoint with `python main.py`.
|
|
|
336
357
|
if timeout_s is not None:
|
|
337
358
|
config = config.options(timeout_s=timeout_s)
|
|
338
359
|
|
|
360
|
+
if tags:
|
|
361
|
+
tag_map = parse_tags_kv_to_str_map(tags)
|
|
362
|
+
if tag_map:
|
|
363
|
+
config = config.options(tags=tag_map)
|
|
364
|
+
|
|
339
365
|
log.info(f"Submitting job with config {config}.")
|
|
340
366
|
job_id = anyscale.job.submit(config)
|
|
341
367
|
|
|
@@ -347,7 +373,7 @@ and override the entrypoint with `python main.py`.
|
|
|
347
373
|
log.info("Use `--wait` to wait for the job to run and stream logs.")
|
|
348
374
|
|
|
349
375
|
if wait:
|
|
350
|
-
anyscale.job.wait(id=job_id)
|
|
376
|
+
anyscale.job.wait(id=job_id, follow=True)
|
|
351
377
|
|
|
352
378
|
|
|
353
379
|
# TODO(mowen): Add cloud support for this when we refactor to new SDK method
|
|
@@ -379,6 +405,17 @@ and override the entrypoint with `python main.py`.
|
|
|
379
405
|
"If not provided, defaults to listing only unarchived jobs."
|
|
380
406
|
),
|
|
381
407
|
)
|
|
408
|
+
@click.option(
|
|
409
|
+
"--tag",
|
|
410
|
+
"tags",
|
|
411
|
+
multiple=True,
|
|
412
|
+
help=(
|
|
413
|
+
"This option can be repeated to filter by multiple tags. "
|
|
414
|
+
"Tags with the same key are ORed, whereas tags with different keys are ANDed. "
|
|
415
|
+
"Example: --tag team:mlops --tag team:infra --tag env:prod. "
|
|
416
|
+
"Filters with team: (mlops OR infra) AND env:prod."
|
|
417
|
+
),
|
|
418
|
+
)
|
|
382
419
|
@click.option(
|
|
383
420
|
"--max-items",
|
|
384
421
|
required=False,
|
|
@@ -404,6 +441,7 @@ def list( # noqa: A001 PLR0913
|
|
|
404
441
|
include_archived: bool,
|
|
405
442
|
max_items: int,
|
|
406
443
|
states: List[HaJobStates],
|
|
444
|
+
tags: List[str],
|
|
407
445
|
) -> None:
|
|
408
446
|
job_controller = JobController()
|
|
409
447
|
job_controller.list(
|
|
@@ -414,6 +452,7 @@ def list( # noqa: A001 PLR0913
|
|
|
414
452
|
include_archived=include_archived,
|
|
415
453
|
max_items=max_items,
|
|
416
454
|
states=states,
|
|
455
|
+
tags=parse_repeatable_tags_to_dict(tags) if tags else None,
|
|
417
456
|
)
|
|
418
457
|
|
|
419
458
|
|
|
@@ -755,3 +794,84 @@ status will be returned.
|
|
|
755
794
|
stream = StringIO()
|
|
756
795
|
yaml.dump(status_dict, stream, sort_keys=False)
|
|
757
796
|
print(stream.getvalue(), end="")
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
@job_cli.group("tags", help="Manage tags for jobs.")
|
|
800
|
+
def job_tags_cli() -> None:
|
|
801
|
+
pass
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
@job_tags_cli.command(name="add", help="Add or update tags on a job.")
|
|
805
|
+
@click.option("--id", "job_id", required=False, help="Unique ID of the job.")
|
|
806
|
+
@click.option("--name", "-n", required=False, help="Name of the job.")
|
|
807
|
+
@click.option(
|
|
808
|
+
"--tag",
|
|
809
|
+
"tags",
|
|
810
|
+
multiple=True,
|
|
811
|
+
help="Tag in key=value (or key:value) format. Repeat to add multiple.",
|
|
812
|
+
)
|
|
813
|
+
def job_tags_add(job_id: Optional[str], name: Optional[str], tags: Tuple[str]) -> None:
|
|
814
|
+
if not job_id and not name:
|
|
815
|
+
raise click.ClickException("Provide either --id or --name.")
|
|
816
|
+
tag_map = parse_tags_kv_to_str_map(tags)
|
|
817
|
+
if not tag_map:
|
|
818
|
+
raise click.ClickException("Provide at least one --tag key=value.")
|
|
819
|
+
req = UpsertResourceTagsRequest(
|
|
820
|
+
resource_type=ResourceTagResourceType.JOB,
|
|
821
|
+
resource_id=job_id or JobController().resolve_job_id(job_id, name),
|
|
822
|
+
tags=tag_map,
|
|
823
|
+
)
|
|
824
|
+
JobController().api_client.upsert_resource_tags_api_v2_tags_resource_put(req)
|
|
825
|
+
stderr = Console(stderr=True)
|
|
826
|
+
ident = job_id or name or "<unknown>"
|
|
827
|
+
stderr.print(f"Tags updated for job '{ident}'.")
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
@job_tags_cli.command(name="remove", help="Remove tags by key from a job.")
|
|
831
|
+
@click.option("--id", "job_id", required=False, help="Unique ID of the job.")
|
|
832
|
+
@click.option("--name", "-n", required=False, help="Name of the job.")
|
|
833
|
+
@click.option("--key", "keys", multiple=True, help="Tag key to remove. Repeatable.")
|
|
834
|
+
def job_tags_remove(
|
|
835
|
+
job_id: Optional[str], name: Optional[str], keys: Tuple[str]
|
|
836
|
+
) -> None:
|
|
837
|
+
if not job_id and not name:
|
|
838
|
+
raise click.ClickException("Provide either --id or --name.")
|
|
839
|
+
key_list = [k for k in keys if k and k.strip()]
|
|
840
|
+
if not key_list:
|
|
841
|
+
raise click.ClickException("Provide at least one --key to remove.")
|
|
842
|
+
req = DeleteResourceTagsRequest(
|
|
843
|
+
resource_type=ResourceTagResourceType.JOB,
|
|
844
|
+
resource_id=job_id or JobController().resolve_job_id(job_id, name),
|
|
845
|
+
keys=key_list,
|
|
846
|
+
)
|
|
847
|
+
JobController().api_client.delete_resource_tags_api_v2_tags_resource_delete(req)
|
|
848
|
+
stderr = Console(stderr=True)
|
|
849
|
+
ident = job_id or name or "<unknown>"
|
|
850
|
+
stderr.print(f"Removed tag keys {key_list} from job '{ident}'.")
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
@job_tags_cli.command(name="list", help="List tags for a job.")
|
|
854
|
+
@click.option("--id", "job_id", required=False, help="Unique ID of the job.")
|
|
855
|
+
@click.option("--name", "-n", required=False, help="Name of the job.")
|
|
856
|
+
@click.option("--json", "json_output", is_flag=True, default=False)
|
|
857
|
+
def job_tags_list(
|
|
858
|
+
job_id: Optional[str], name: Optional[str], json_output: bool
|
|
859
|
+
) -> None:
|
|
860
|
+
if not job_id and not name:
|
|
861
|
+
raise click.ClickException("Provide either --id or --name.")
|
|
862
|
+
if not job_id:
|
|
863
|
+
job_id = JobController().resolve_job_id(job_id, name)
|
|
864
|
+
auth = get_auth_api_client()
|
|
865
|
+
resp = auth.api_client.get_tags_for_resource_api_v2_tags_resource_get(
|
|
866
|
+
ResourceTagResourceType.JOB, job_id
|
|
867
|
+
)
|
|
868
|
+
tags = getattr(resp.result, "tags", [])
|
|
869
|
+
if json_output:
|
|
870
|
+
Console().print_json(json=json_dumps([t.to_dict() for t in tags], indent=2))
|
|
871
|
+
else:
|
|
872
|
+
stderr = Console(stderr=True)
|
|
873
|
+
if not tags:
|
|
874
|
+
stderr.print("No tags found.")
|
|
875
|
+
return
|
|
876
|
+
pairs = [(t.key, t.value) for t in tags]
|
|
877
|
+
stderr.print(build_kv_table(pairs, title="Tags"))
|
|
@@ -5,16 +5,20 @@ from enum import Enum
|
|
|
5
5
|
from functools import partial
|
|
6
6
|
from json import dumps as json_dumps
|
|
7
7
|
import sys
|
|
8
|
-
from typing import Dict, get_type_hints, List, Optional
|
|
8
|
+
from typing import Dict, get_type_hints, List, Optional, Tuple
|
|
9
9
|
|
|
10
10
|
import click
|
|
11
11
|
from rich.console import Console
|
|
12
12
|
from rich.table import Table
|
|
13
13
|
|
|
14
|
+
from anyscale.authenticate import get_auth_api_client
|
|
14
15
|
from anyscale.client.openapi_client.models.job_queue_sort_directive import (
|
|
15
16
|
JobQueueSortDirective,
|
|
16
17
|
)
|
|
17
18
|
from anyscale.client.openapi_client.models.job_queue_sort_field import JobQueueSortField
|
|
19
|
+
from anyscale.client.openapi_client.models.resource_tag_resource_type import (
|
|
20
|
+
ResourceTagResourceType,
|
|
21
|
+
)
|
|
18
22
|
from anyscale.client.openapi_client.models.session_state import SessionState
|
|
19
23
|
from anyscale.client.openapi_client.models.sort_order import SortOrder
|
|
20
24
|
from anyscale.commands import command_examples
|
|
@@ -24,7 +28,12 @@ from anyscale.commands.list_util import (
|
|
|
24
28
|
NON_INTERACTIVE_DEFAULT_MAX_ITEMS,
|
|
25
29
|
validate_page_size,
|
|
26
30
|
)
|
|
27
|
-
from anyscale.commands.util import
|
|
31
|
+
from anyscale.commands.util import (
|
|
32
|
+
AnyscaleCommand,
|
|
33
|
+
build_kv_table,
|
|
34
|
+
parse_repeatable_tags_to_dict,
|
|
35
|
+
parse_tags_kv_to_str_map,
|
|
36
|
+
)
|
|
28
37
|
import anyscale.job_queue
|
|
29
38
|
from anyscale.job_queue.models import JobQueueStatus, JobQueueStatusKeys
|
|
30
39
|
from anyscale.util import get_endpoint, get_user_info, validate_non_negative_arg
|
|
@@ -94,6 +103,17 @@ VIEW_COLUMNS: Dict[ViewOption, List[JobQueueStatusKeys]] = {
|
|
|
94
103
|
type=click.Choice(SessionState.allowable_values, case_sensitive=False),
|
|
95
104
|
help="Filter by cluster status.",
|
|
96
105
|
)
|
|
106
|
+
@click.option(
|
|
107
|
+
"--tag",
|
|
108
|
+
"tags",
|
|
109
|
+
multiple=True,
|
|
110
|
+
help=(
|
|
111
|
+
"This option can be repeated to filter by multiple tags. "
|
|
112
|
+
"Tags with the same key are ORed, whereas tags with different keys are ANDed. "
|
|
113
|
+
"Example: --tag team:mlops --tag team:infra --tag env:prod. "
|
|
114
|
+
"Filters with team: (mlops OR infra) AND env:prod."
|
|
115
|
+
),
|
|
116
|
+
)
|
|
97
117
|
@click.option(
|
|
98
118
|
"--view",
|
|
99
119
|
type=click.Choice([opt.value for opt in ViewOption], case_sensitive=False),
|
|
@@ -133,6 +153,7 @@ def list_job_queues( # noqa: PLR0913
|
|
|
133
153
|
cloud: Optional[str],
|
|
134
154
|
project: Optional[str],
|
|
135
155
|
cluster_status: Optional[str],
|
|
156
|
+
tags: List[str],
|
|
136
157
|
include_all_users: bool,
|
|
137
158
|
view: ViewOption,
|
|
138
159
|
page_size: int,
|
|
@@ -172,6 +193,7 @@ def list_job_queues( # noqa: PLR0913
|
|
|
172
193
|
creator_id=None if include_all_users else (user.id if user else None),
|
|
173
194
|
cloud=cloud,
|
|
174
195
|
project=project,
|
|
196
|
+
tags_filter=parse_repeatable_tags_to_dict(tags) if tags else None,
|
|
175
197
|
page_size=page_size,
|
|
176
198
|
max_items=None if not no_interactive else effective_max,
|
|
177
199
|
sorting_directives=sort_dirs,
|
|
@@ -240,6 +262,81 @@ def update_job_queue(
|
|
|
240
262
|
sys.exit(1)
|
|
241
263
|
|
|
242
264
|
|
|
265
|
+
@job_queue_cli.group("tags", help="Manage tags for job queues.")
|
|
266
|
+
def job_queue_tags_cli() -> None:
|
|
267
|
+
pass
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@job_queue_tags_cli.command(name="add", help="Add or update tags on a job queue.")
|
|
271
|
+
@click.option("--id", "job_queue_id", help="ID of a job queue.")
|
|
272
|
+
@click.option("--name", type=str, help="Name of a job queue.")
|
|
273
|
+
@click.option(
|
|
274
|
+
"--tag",
|
|
275
|
+
"tags",
|
|
276
|
+
multiple=True,
|
|
277
|
+
help="Tag in key=value (or key:value) format. Repeat to add multiple.",
|
|
278
|
+
)
|
|
279
|
+
def job_queue_tags_add(
|
|
280
|
+
job_queue_id: Optional[str], name: Optional[str], tags: Tuple[str],
|
|
281
|
+
) -> None:
|
|
282
|
+
if not job_queue_id and not name:
|
|
283
|
+
raise click.ClickException("Provide either --id or --name.")
|
|
284
|
+
tag_map = parse_tags_kv_to_str_map(tags)
|
|
285
|
+
if not tag_map:
|
|
286
|
+
raise click.ClickException("Provide at least one --tag key=value.")
|
|
287
|
+
anyscale.job_queue.add_tags(job_queue_id=job_queue_id, name=name, tags=tag_map)
|
|
288
|
+
stderr = Console(stderr=True)
|
|
289
|
+
ident = job_queue_id or name or "<unknown>"
|
|
290
|
+
stderr.print(f"Tags updated for job queue '{ident}'.")
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@job_queue_tags_cli.command(name="remove", help="Remove tags by key from a job queue.")
|
|
294
|
+
@click.option("--id", "job_queue_id", help="ID of a job queue.")
|
|
295
|
+
@click.option("--name", type=str, help="Name of a job queue.")
|
|
296
|
+
@click.option("--key", "keys", multiple=True, help="Tag key to remove. Repeatable.")
|
|
297
|
+
def job_queue_tags_remove(
|
|
298
|
+
job_queue_id: Optional[str], name: Optional[str], keys: Tuple[str],
|
|
299
|
+
) -> None:
|
|
300
|
+
if not job_queue_id and not name:
|
|
301
|
+
raise click.ClickException("Provide either --id or --name.")
|
|
302
|
+
key_list = [k for k in keys if k and k.strip()]
|
|
303
|
+
if not key_list:
|
|
304
|
+
raise click.ClickException("Provide at least one --key to remove.")
|
|
305
|
+
anyscale.job_queue.remove_tags(job_queue_id=job_queue_id, name=name, keys=key_list)
|
|
306
|
+
stderr = Console(stderr=True)
|
|
307
|
+
ident = job_queue_id or name or "<unknown>"
|
|
308
|
+
stderr.print(f"Removed tag keys {key_list} from job queue '{ident}'.")
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@job_queue_tags_cli.command(name="list", help="List tags for a job queue.")
|
|
312
|
+
@click.option("--id", "job_queue_id", help="ID of a job queue.")
|
|
313
|
+
@click.option("--name", type=str, help="Name of a job queue.")
|
|
314
|
+
@click.option("--json", "json_output", is_flag=True, default=False)
|
|
315
|
+
def job_queue_tags_list(
|
|
316
|
+
job_queue_id: Optional[str], name: Optional[str], json_output: bool,
|
|
317
|
+
) -> None:
|
|
318
|
+
if not job_queue_id and not name:
|
|
319
|
+
raise click.ClickException("Provide either --id or --name.")
|
|
320
|
+
if not job_queue_id:
|
|
321
|
+
# Resolve ID via status (public SDK), which fetches by ID only; so instead list by name
|
|
322
|
+
jq = anyscale.job_queue.status(job_queue_id=anyscale.job_queue.list(name=name, max_items=1).__next__().id) # type: ignore
|
|
323
|
+
job_queue_id = jq.id
|
|
324
|
+
auth = get_auth_api_client()
|
|
325
|
+
resp = auth.api_client.get_tags_for_resource_api_v2_tags_resource_get(
|
|
326
|
+
ResourceTagResourceType.JOB_QUEUE, job_queue_id
|
|
327
|
+
)
|
|
328
|
+
tags = getattr(resp.result, "tags", [])
|
|
329
|
+
if json_output:
|
|
330
|
+
Console().print_json(json=json_dumps([t.to_dict() for t in tags], indent=2))
|
|
331
|
+
else:
|
|
332
|
+
stderr = Console(stderr=True)
|
|
333
|
+
if not tags:
|
|
334
|
+
stderr.print("No tags found.")
|
|
335
|
+
return
|
|
336
|
+
pairs = [(t.key, t.value) for t in tags]
|
|
337
|
+
stderr.print(build_kv_table(pairs, title="Tags"))
|
|
338
|
+
|
|
339
|
+
|
|
243
340
|
@job_queue_cli.command(
|
|
244
341
|
name="status",
|
|
245
342
|
help="Show job queue details.",
|