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.
Files changed (113) hide show
  1. anyscale/_private/anyscale_client/anyscale_client.py +126 -3
  2. anyscale/_private/anyscale_client/common.py +51 -2
  3. anyscale/_private/anyscale_client/fake_anyscale_client.py +103 -11
  4. anyscale/client/README.md +43 -4
  5. anyscale/client/openapi_client/__init__.py +30 -4
  6. anyscale/client/openapi_client/api/default_api.py +1769 -27
  7. anyscale/client/openapi_client/models/__init__.py +30 -4
  8. anyscale/client/openapi_client/models/api_key_info.py +29 -3
  9. anyscale/client/openapi_client/models/apply_autoscaling_config_update_model.py +350 -0
  10. anyscale/client/openapi_client/models/apply_multi_version_update_weights_update_model.py +152 -0
  11. anyscale/client/openapi_client/models/apply_production_service_multi_version_v2_model.py +207 -0
  12. anyscale/client/openapi_client/models/apply_production_service_v2_model.py +31 -3
  13. anyscale/client/openapi_client/models/apply_version_weight_update_model.py +181 -0
  14. anyscale/client/openapi_client/models/backend_server_api_product_models_catalog_client_models_table_metadata.py +546 -0
  15. anyscale/client/openapi_client/models/backend_server_api_product_models_data_catalogs_table_metadata.py +178 -0
  16. anyscale/client/openapi_client/models/baseimagesenum.py +139 -1
  17. anyscale/client/openapi_client/models/catalog_metadata.py +150 -0
  18. anyscale/client/openapi_client/models/cloud_data_bucket_file_type.py +2 -1
  19. anyscale/client/openapi_client/models/{oauthconnectionresponse_response.py → clouddeployment_response.py} +11 -11
  20. anyscale/client/openapi_client/models/column_info.py +265 -0
  21. anyscale/client/openapi_client/models/compute_node_type.py +29 -1
  22. anyscale/client/openapi_client/models/connection_metadata.py +206 -0
  23. anyscale/client/openapi_client/models/create_experimental_workspace.py +29 -1
  24. anyscale/client/openapi_client/models/create_workspace_from_template.py +29 -1
  25. anyscale/client/openapi_client/models/create_workspace_template_version.py +59 -3
  26. anyscale/client/openapi_client/models/data_catalog.py +45 -31
  27. anyscale/client/openapi_client/models/data_catalog_connection.py +74 -58
  28. anyscale/client/openapi_client/models/{ha_job_event_level.py → data_catalog_object_type.py} +7 -8
  29. anyscale/client/openapi_client/models/data_catalog_schema.py +324 -0
  30. anyscale/client/openapi_client/models/data_catalog_table.py +437 -0
  31. anyscale/client/openapi_client/models/data_catalog_volume.py +437 -0
  32. anyscale/client/openapi_client/models/datacatalogschema_list_response.py +147 -0
  33. anyscale/client/openapi_client/models/datacatalogtable_list_response.py +147 -0
  34. anyscale/client/openapi_client/models/datacatalogvolume_list_response.py +147 -0
  35. anyscale/client/openapi_client/models/decorated_list_service_api_model.py +58 -1
  36. anyscale/client/openapi_client/models/decorated_production_service_v2_api_model.py +60 -3
  37. anyscale/client/openapi_client/models/decorated_serve_deployment.py +27 -1
  38. anyscale/client/openapi_client/models/decorated_service_event_api_model.py +3 -3
  39. anyscale/client/openapi_client/models/decoratedproductionservicev2_versionapimodel_response.py +121 -0
  40. anyscale/client/openapi_client/models/describe_machine_pool_machines_filters.py +33 -5
  41. anyscale/client/openapi_client/models/describe_machine_pool_requests_filters.py +33 -5
  42. anyscale/client/openapi_client/models/describe_machine_pool_workloads_filters.py +33 -5
  43. anyscale/client/openapi_client/models/{service_event_level.py → entity_type.py} +9 -9
  44. anyscale/client/openapi_client/models/event_level.py +2 -1
  45. anyscale/client/openapi_client/models/job_event_fields.py +206 -0
  46. anyscale/client/openapi_client/models/machine_type_partition_filter.py +152 -0
  47. anyscale/client/openapi_client/models/partition_info.py +30 -1
  48. anyscale/client/openapi_client/models/physical_resources.py +178 -0
  49. anyscale/client/openapi_client/models/production_job_event.py +3 -3
  50. anyscale/client/openapi_client/models/rollout_strategy.py +2 -1
  51. anyscale/client/openapi_client/models/schema_metadata.py +150 -0
  52. anyscale/client/openapi_client/models/service_event_fields.py +318 -0
  53. anyscale/client/openapi_client/models/sso_config.py +18 -18
  54. anyscale/client/openapi_client/models/supportedbaseimagesenum.py +139 -1
  55. anyscale/client/openapi_client/models/table_data_preview.py +209 -0
  56. anyscale/client/openapi_client/models/task_summary_config.py +29 -3
  57. anyscale/client/openapi_client/models/task_table_config.py +29 -3
  58. anyscale/client/openapi_client/models/unified_event.py +377 -0
  59. anyscale/client/openapi_client/models/unified_origin_filter.py +113 -0
  60. anyscale/client/openapi_client/models/unifiedevent_list_response.py +147 -0
  61. anyscale/client/openapi_client/models/volume_metadata.py +150 -0
  62. anyscale/client/openapi_client/models/worker_node_type.py +29 -1
  63. anyscale/client/openapi_client/models/workspace_event_fields.py +122 -0
  64. anyscale/client/openapi_client/models/workspace_template_version.py +58 -1
  65. anyscale/client/openapi_client/models/workspace_template_version_data_object.py +58 -1
  66. anyscale/cloud/models.py +2 -2
  67. anyscale/commands/cloud_commands.py +133 -2
  68. anyscale/commands/job_commands.py +121 -1
  69. anyscale/commands/job_queue_commands.py +99 -2
  70. anyscale/commands/service_commands.py +267 -67
  71. anyscale/commands/setup_k8s.py +546 -31
  72. anyscale/commands/util.py +104 -1
  73. anyscale/commands/workspace_commands.py +123 -5
  74. anyscale/commands/workspace_commands_v2.py +17 -1
  75. anyscale/compute_config/_private/compute_config_sdk.py +25 -12
  76. anyscale/compute_config/models.py +15 -0
  77. anyscale/controllers/cloud_controller.py +15 -2
  78. anyscale/controllers/job_controller.py +12 -0
  79. anyscale/controllers/kubernetes_verifier.py +80 -66
  80. anyscale/controllers/workspace_controller.py +67 -5
  81. anyscale/job/_private/job_sdk.py +50 -2
  82. anyscale/job/commands.py +3 -0
  83. anyscale/job/models.py +16 -0
  84. anyscale/job_queue/__init__.py +37 -1
  85. anyscale/job_queue/_private/job_queue_sdk.py +28 -1
  86. anyscale/job_queue/commands.py +61 -1
  87. anyscale/sdk/anyscale_client/__init__.py +1 -0
  88. anyscale/sdk/anyscale_client/api/default_api.py +12 -2
  89. anyscale/sdk/anyscale_client/models/__init__.py +1 -0
  90. anyscale/sdk/anyscale_client/models/apply_production_service_v2_model.py +31 -3
  91. anyscale/sdk/anyscale_client/models/apply_service_model.py +31 -3
  92. anyscale/sdk/anyscale_client/models/baseimagesenum.py +139 -1
  93. anyscale/sdk/anyscale_client/models/compute_node_type.py +29 -1
  94. anyscale/sdk/anyscale_client/models/physical_resources.py +178 -0
  95. anyscale/sdk/anyscale_client/models/rollout_strategy.py +2 -1
  96. anyscale/sdk/anyscale_client/models/supportedbaseimagesenum.py +139 -1
  97. anyscale/sdk/anyscale_client/models/worker_node_type.py +29 -1
  98. anyscale/service/__init__.py +51 -3
  99. anyscale/service/_private/service_sdk.py +481 -58
  100. anyscale/service/commands.py +90 -4
  101. anyscale/service/models.py +56 -0
  102. anyscale/shared_anyscale_utils/latest_ray_version.py +1 -1
  103. anyscale/version.py +1 -1
  104. anyscale/workspace/_private/workspace_sdk.py +1 -0
  105. anyscale/workspace/models.py +19 -0
  106. {anyscale-0.26.69.dist-info → anyscale-0.26.71.dist-info}/METADATA +1 -1
  107. {anyscale-0.26.69.dist-info → anyscale-0.26.71.dist-info}/RECORD +112 -85
  108. anyscale/client/openapi_client/models/o_auth_connection_response.py +0 -229
  109. {anyscale-0.26.69.dist-info → anyscale-0.26.71.dist-info}/WHEEL +0 -0
  110. {anyscale-0.26.69.dist-info → anyscale-0.26.71.dist-info}/entry_points.txt +0 -0
  111. {anyscale-0.26.69.dist-info → anyscale-0.26.71.dist-info}/licenses/LICENSE +0 -0
  112. {anyscale-0.26.69.dist-info → anyscale-0.26.71.dist-info}/licenses/NOTICE +0 -0
  113. {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 setup_kubernetes_cloud
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 AnyscaleCommand
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.",