anyscale 0.26.70__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 (78) hide show
  1. anyscale/_private/anyscale_client/anyscale_client.py +63 -6
  2. anyscale/_private/anyscale_client/common.py +33 -3
  3. anyscale/_private/anyscale_client/fake_anyscale_client.py +27 -2
  4. anyscale/client/README.md +29 -0
  5. anyscale/client/openapi_client/__init__.py +19 -0
  6. anyscale/client/openapi_client/api/default_api.py +1307 -4
  7. anyscale/client/openapi_client/models/__init__.py +19 -0
  8. anyscale/client/openapi_client/models/apply_multi_version_update_weights_update_model.py +152 -0
  9. anyscale/client/openapi_client/models/apply_version_weight_update_model.py +181 -0
  10. anyscale/client/openapi_client/models/backend_server_api_product_models_catalog_client_models_table_metadata.py +546 -0
  11. anyscale/client/openapi_client/models/backend_server_api_product_models_data_catalogs_table_metadata.py +178 -0
  12. anyscale/client/openapi_client/models/baseimagesenum.py +70 -1
  13. anyscale/client/openapi_client/models/catalog_metadata.py +150 -0
  14. anyscale/client/openapi_client/models/column_info.py +265 -0
  15. anyscale/client/openapi_client/models/compute_node_type.py +29 -1
  16. anyscale/client/openapi_client/models/connection_metadata.py +206 -0
  17. anyscale/client/openapi_client/models/create_workspace_template_version.py +31 -3
  18. anyscale/client/openapi_client/models/data_catalog.py +45 -31
  19. anyscale/client/openapi_client/models/data_catalog_connection.py +74 -58
  20. anyscale/client/openapi_client/models/data_catalog_object_type.py +100 -0
  21. anyscale/client/openapi_client/models/data_catalog_schema.py +324 -0
  22. anyscale/client/openapi_client/models/data_catalog_table.py +437 -0
  23. anyscale/client/openapi_client/models/data_catalog_volume.py +437 -0
  24. anyscale/client/openapi_client/models/datacatalogschema_list_response.py +147 -0
  25. anyscale/client/openapi_client/models/datacatalogtable_list_response.py +147 -0
  26. anyscale/client/openapi_client/models/datacatalogvolume_list_response.py +147 -0
  27. anyscale/client/openapi_client/models/decorated_serve_deployment.py +27 -1
  28. anyscale/client/openapi_client/models/decoratedproductionservicev2_versionapimodel_response.py +121 -0
  29. anyscale/client/openapi_client/models/describe_machine_pool_machines_filters.py +2 -2
  30. anyscale/client/openapi_client/models/describe_machine_pool_requests_filters.py +33 -5
  31. anyscale/client/openapi_client/models/describe_machine_pool_workloads_filters.py +2 -2
  32. anyscale/client/openapi_client/models/physical_resources.py +178 -0
  33. anyscale/client/openapi_client/models/schema_metadata.py +150 -0
  34. anyscale/client/openapi_client/models/sso_config.py +18 -18
  35. anyscale/client/openapi_client/models/supportedbaseimagesenum.py +70 -1
  36. anyscale/client/openapi_client/models/table_data_preview.py +209 -0
  37. anyscale/client/openapi_client/models/volume_metadata.py +150 -0
  38. anyscale/client/openapi_client/models/worker_node_type.py +29 -1
  39. anyscale/client/openapi_client/models/workspace_template_version.py +29 -1
  40. anyscale/client/openapi_client/models/workspace_template_version_data_object.py +29 -1
  41. anyscale/commands/job_commands.py +120 -0
  42. anyscale/commands/job_queue_commands.py +99 -2
  43. anyscale/commands/service_commands.py +139 -2
  44. anyscale/commands/util.py +104 -1
  45. anyscale/commands/workspace_commands.py +123 -5
  46. anyscale/commands/workspace_commands_v2.py +17 -1
  47. anyscale/compute_config/_private/compute_config_sdk.py +25 -12
  48. anyscale/compute_config/models.py +15 -0
  49. anyscale/controllers/job_controller.py +12 -0
  50. anyscale/controllers/workspace_controller.py +67 -5
  51. anyscale/job/_private/job_sdk.py +3 -1
  52. anyscale/job/models.py +16 -0
  53. anyscale/job_queue/__init__.py +37 -1
  54. anyscale/job_queue/_private/job_queue_sdk.py +28 -1
  55. anyscale/job_queue/commands.py +61 -1
  56. anyscale/sdk/anyscale_client/__init__.py +1 -0
  57. anyscale/sdk/anyscale_client/api/default_api.py +12 -2
  58. anyscale/sdk/anyscale_client/models/__init__.py +1 -0
  59. anyscale/sdk/anyscale_client/models/baseimagesenum.py +70 -1
  60. anyscale/sdk/anyscale_client/models/compute_node_type.py +29 -1
  61. anyscale/sdk/anyscale_client/models/physical_resources.py +178 -0
  62. anyscale/sdk/anyscale_client/models/supportedbaseimagesenum.py +70 -1
  63. anyscale/sdk/anyscale_client/models/worker_node_type.py +29 -1
  64. anyscale/service/__init__.py +40 -0
  65. anyscale/service/_private/service_sdk.py +121 -24
  66. anyscale/service/commands.py +75 -1
  67. anyscale/service/models.py +46 -2
  68. anyscale/shared_anyscale_utils/latest_ray_version.py +1 -1
  69. anyscale/version.py +1 -1
  70. anyscale/workspace/_private/workspace_sdk.py +1 -0
  71. anyscale/workspace/models.py +19 -0
  72. {anyscale-0.26.70.dist-info → anyscale-0.26.71.dist-info}/METADATA +1 -1
  73. {anyscale-0.26.70.dist-info → anyscale-0.26.71.dist-info}/RECORD +78 -58
  74. {anyscale-0.26.70.dist-info → anyscale-0.26.71.dist-info}/WHEEL +0 -0
  75. {anyscale-0.26.70.dist-info → anyscale-0.26.71.dist-info}/entry_points.txt +0 -0
  76. {anyscale-0.26.70.dist-info → anyscale-0.26.71.dist-info}/licenses/LICENSE +0 -0
  77. {anyscale-0.26.70.dist-info → anyscale-0.26.71.dist-info}/licenses/NOTICE +0 -0
  78. {anyscale-0.26.70.dist-info → anyscale-0.26.71.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,150 @@
1
+ # coding: utf-8
2
+
3
+ """
4
+ Managed Ray API
5
+
6
+ No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) # noqa: E501
7
+
8
+ The version of the OpenAPI document: 0.1.0
9
+ Generated by: https://openapi-generator.tech
10
+ """
11
+
12
+
13
+ import pprint
14
+ import re # noqa: F401
15
+
16
+ import six
17
+
18
+ from openapi_client.configuration import Configuration
19
+
20
+
21
+ class VolumeMetadata(object):
22
+ """NOTE: This class is auto generated by OpenAPI Generator.
23
+ Ref: https://openapi-generator.tech
24
+
25
+ Do not edit the class manually.
26
+ """
27
+
28
+ """
29
+ Attributes:
30
+ openapi_types (dict): The key is attribute name
31
+ and the value is attribute type.
32
+ attribute_map (dict): The key is attribute name
33
+ and the value is json key in definition.
34
+ """
35
+ openapi_types = {
36
+ 'owner': 'str',
37
+ 'full_name': 'str'
38
+ }
39
+
40
+ attribute_map = {
41
+ 'owner': 'owner',
42
+ 'full_name': 'full_name'
43
+ }
44
+
45
+ def __init__(self, owner=None, full_name=None, local_vars_configuration=None): # noqa: E501
46
+ """VolumeMetadata - a model defined in OpenAPI""" # noqa: E501
47
+ if local_vars_configuration is None:
48
+ local_vars_configuration = Configuration()
49
+ self.local_vars_configuration = local_vars_configuration
50
+
51
+ self._owner = None
52
+ self._full_name = None
53
+ self.discriminator = None
54
+
55
+ if owner is not None:
56
+ self.owner = owner
57
+ if full_name is not None:
58
+ self.full_name = full_name
59
+
60
+ @property
61
+ def owner(self):
62
+ """Gets the owner of this VolumeMetadata. # noqa: E501
63
+
64
+ Owner of the entity # noqa: E501
65
+
66
+ :return: The owner of this VolumeMetadata. # noqa: E501
67
+ :rtype: str
68
+ """
69
+ return self._owner
70
+
71
+ @owner.setter
72
+ def owner(self, owner):
73
+ """Sets the owner of this VolumeMetadata.
74
+
75
+ Owner of the entity # noqa: E501
76
+
77
+ :param owner: The owner of this VolumeMetadata. # noqa: E501
78
+ :type: str
79
+ """
80
+
81
+ self._owner = owner
82
+
83
+ @property
84
+ def full_name(self):
85
+ """Gets the full_name of this VolumeMetadata. # noqa: E501
86
+
87
+ Fully qualified name of the entity # noqa: E501
88
+
89
+ :return: The full_name of this VolumeMetadata. # noqa: E501
90
+ :rtype: str
91
+ """
92
+ return self._full_name
93
+
94
+ @full_name.setter
95
+ def full_name(self, full_name):
96
+ """Sets the full_name of this VolumeMetadata.
97
+
98
+ Fully qualified name of the entity # noqa: E501
99
+
100
+ :param full_name: The full_name of this VolumeMetadata. # noqa: E501
101
+ :type: str
102
+ """
103
+
104
+ self._full_name = full_name
105
+
106
+ def to_dict(self):
107
+ """Returns the model properties as a dict"""
108
+ result = {}
109
+
110
+ for attr, _ in six.iteritems(self.openapi_types):
111
+ value = getattr(self, attr)
112
+ if isinstance(value, list):
113
+ result[attr] = list(map(
114
+ lambda x: x.to_dict() if hasattr(x, "to_dict") else x,
115
+ value
116
+ ))
117
+ elif hasattr(value, "to_dict"):
118
+ result[attr] = value.to_dict()
119
+ elif isinstance(value, dict):
120
+ result[attr] = dict(map(
121
+ lambda item: (item[0], item[1].to_dict())
122
+ if hasattr(item[1], "to_dict") else item,
123
+ value.items()
124
+ ))
125
+ else:
126
+ result[attr] = value
127
+
128
+ return result
129
+
130
+ def to_str(self):
131
+ """Returns the string representation of the model"""
132
+ return pprint.pformat(self.to_dict())
133
+
134
+ def __repr__(self):
135
+ """For `print` and `pprint`"""
136
+ return self.to_str()
137
+
138
+ def __eq__(self, other):
139
+ """Returns true if both objects are equal"""
140
+ if not isinstance(other, VolumeMetadata):
141
+ return False
142
+
143
+ return self.to_dict() == other.to_dict()
144
+
145
+ def __ne__(self, other):
146
+ """Returns true if both objects are not equal"""
147
+ if not isinstance(other, VolumeMetadata):
148
+ return True
149
+
150
+ return self.to_dict() != other.to_dict()
@@ -36,6 +36,7 @@ class WorkerNodeType(object):
36
36
  'name': 'str',
37
37
  'instance_type': 'str',
38
38
  'resources': 'Resources',
39
+ 'physical_resources': 'PhysicalResources',
39
40
  'labels': 'dict(str, str)',
40
41
  'aws_advanced_configurations_json': 'object',
41
42
  'gcp_advanced_configurations_json': 'object',
@@ -51,6 +52,7 @@ class WorkerNodeType(object):
51
52
  'name': 'name',
52
53
  'instance_type': 'instance_type',
53
54
  'resources': 'resources',
55
+ 'physical_resources': 'physical_resources',
54
56
  'labels': 'labels',
55
57
  'aws_advanced_configurations_json': 'aws_advanced_configurations_json',
56
58
  'gcp_advanced_configurations_json': 'gcp_advanced_configurations_json',
@@ -62,7 +64,7 @@ class WorkerNodeType(object):
62
64
  'fallback_to_ondemand': 'fallback_to_ondemand'
63
65
  }
64
66
 
65
- def __init__(self, name=None, instance_type=None, resources=None, labels=None, aws_advanced_configurations_json=None, gcp_advanced_configurations_json=None, advanced_configurations_json=None, flags=None, min_workers=None, max_workers=None, use_spot=False, fallback_to_ondemand=False, local_vars_configuration=None): # noqa: E501
67
+ def __init__(self, name=None, instance_type=None, resources=None, physical_resources=None, labels=None, aws_advanced_configurations_json=None, gcp_advanced_configurations_json=None, advanced_configurations_json=None, flags=None, min_workers=None, max_workers=None, use_spot=False, fallback_to_ondemand=False, local_vars_configuration=None): # noqa: E501
66
68
  """WorkerNodeType - a model defined in OpenAPI""" # noqa: E501
67
69
  if local_vars_configuration is None:
68
70
  local_vars_configuration = Configuration()
@@ -71,6 +73,7 @@ class WorkerNodeType(object):
71
73
  self._name = None
72
74
  self._instance_type = None
73
75
  self._resources = None
76
+ self._physical_resources = None
74
77
  self._labels = None
75
78
  self._aws_advanced_configurations_json = None
76
79
  self._gcp_advanced_configurations_json = None
@@ -86,6 +89,8 @@ class WorkerNodeType(object):
86
89
  self.instance_type = instance_type
87
90
  if resources is not None:
88
91
  self.resources = resources
92
+ if physical_resources is not None:
93
+ self.physical_resources = physical_resources
89
94
  if labels is not None:
90
95
  self.labels = labels
91
96
  if aws_advanced_configurations_json is not None:
@@ -178,6 +183,29 @@ class WorkerNodeType(object):
178
183
 
179
184
  self._resources = resources
180
185
 
186
+ @property
187
+ def physical_resources(self):
188
+ """Gets the physical_resources of this WorkerNodeType. # noqa: E501
189
+
190
+ Physical resources for compute node type which specifies the actual CPU, memory, and GPU resources that should be allocated for this node type. # noqa: E501
191
+
192
+ :return: The physical_resources of this WorkerNodeType. # noqa: E501
193
+ :rtype: PhysicalResources
194
+ """
195
+ return self._physical_resources
196
+
197
+ @physical_resources.setter
198
+ def physical_resources(self, physical_resources):
199
+ """Sets the physical_resources of this WorkerNodeType.
200
+
201
+ Physical resources for compute node type which specifies the actual CPU, memory, and GPU resources that should be allocated for this node type. # noqa: E501
202
+
203
+ :param physical_resources: The physical_resources of this WorkerNodeType. # noqa: E501
204
+ :type: PhysicalResources
205
+ """
206
+
207
+ self._physical_resources = physical_resources
208
+
181
209
  @property
182
210
  def labels(self):
183
211
  """Gets the labels of this WorkerNodeType. # noqa: E501
@@ -37,6 +37,7 @@ class WorkspaceTemplateVersion(object):
37
37
  'image_uri': 'str',
38
38
  'compute_configs': 'dict(str, str)',
39
39
  'artifacts': 'WorkspaceSystemArtifacts',
40
+ 'idle_termination_minutes': 'int',
40
41
  'id': 'str',
41
42
  'version': 'int',
42
43
  'creator_id': 'str',
@@ -49,6 +50,7 @@ class WorkspaceTemplateVersion(object):
49
50
  'image_uri': 'image_uri',
50
51
  'compute_configs': 'compute_configs',
51
52
  'artifacts': 'artifacts',
53
+ 'idle_termination_minutes': 'idle_termination_minutes',
52
54
  'id': 'id',
53
55
  'version': 'version',
54
56
  'creator_id': 'creator_id',
@@ -56,7 +58,7 @@ class WorkspaceTemplateVersion(object):
56
58
  'created_at': 'created_at'
57
59
  }
58
60
 
59
- def __init__(self, template_id=None, image_uri=None, compute_configs=None, artifacts=None, id=None, version=None, creator_id=None, creator_email=None, created_at=None, local_vars_configuration=None): # noqa: E501
61
+ def __init__(self, template_id=None, image_uri=None, compute_configs=None, artifacts=None, idle_termination_minutes=None, id=None, version=None, creator_id=None, creator_email=None, created_at=None, local_vars_configuration=None): # noqa: E501
60
62
  """WorkspaceTemplateVersion - a model defined in OpenAPI""" # noqa: E501
61
63
  if local_vars_configuration is None:
62
64
  local_vars_configuration = Configuration()
@@ -66,6 +68,7 @@ class WorkspaceTemplateVersion(object):
66
68
  self._image_uri = None
67
69
  self._compute_configs = None
68
70
  self._artifacts = None
71
+ self._idle_termination_minutes = None
69
72
  self._id = None
70
73
  self._version = None
71
74
  self._creator_id = None
@@ -80,6 +83,8 @@ class WorkspaceTemplateVersion(object):
80
83
  self.compute_configs = compute_configs
81
84
  if artifacts is not None:
82
85
  self.artifacts = artifacts
86
+ if idle_termination_minutes is not None:
87
+ self.idle_termination_minutes = idle_termination_minutes
83
88
  self.id = id
84
89
  self.version = version
85
90
  self.creator_id = creator_id
@@ -180,6 +185,29 @@ class WorkspaceTemplateVersion(object):
180
185
 
181
186
  self._artifacts = artifacts
182
187
 
188
+ @property
189
+ def idle_termination_minutes(self):
190
+ """Gets the idle_termination_minutes of this WorkspaceTemplateVersion. # noqa: E501
191
+
192
+ Idle termination minutes for this version # noqa: E501
193
+
194
+ :return: The idle_termination_minutes of this WorkspaceTemplateVersion. # noqa: E501
195
+ :rtype: int
196
+ """
197
+ return self._idle_termination_minutes
198
+
199
+ @idle_termination_minutes.setter
200
+ def idle_termination_minutes(self, idle_termination_minutes):
201
+ """Sets the idle_termination_minutes of this WorkspaceTemplateVersion.
202
+
203
+ Idle termination minutes for this version # noqa: E501
204
+
205
+ :param idle_termination_minutes: The idle_termination_minutes of this WorkspaceTemplateVersion. # noqa: E501
206
+ :type: int
207
+ """
208
+
209
+ self._idle_termination_minutes = idle_termination_minutes
210
+
183
211
  @property
184
212
  def id(self):
185
213
  """Gets the id of this WorkspaceTemplateVersion. # noqa: E501
@@ -37,6 +37,7 @@ class WorkspaceTemplateVersionDataObject(object):
37
37
  'image_uri': 'str',
38
38
  'compute_configs': 'dict(str, str)',
39
39
  'artifacts': 'WorkspaceSystemArtifacts',
40
+ 'idle_termination_minutes': 'int',
40
41
  'id': 'str',
41
42
  'version': 'int',
42
43
  'creator_id': 'str',
@@ -49,6 +50,7 @@ class WorkspaceTemplateVersionDataObject(object):
49
50
  'image_uri': 'image_uri',
50
51
  'compute_configs': 'compute_configs',
51
52
  'artifacts': 'artifacts',
53
+ 'idle_termination_minutes': 'idle_termination_minutes',
52
54
  'id': 'id',
53
55
  'version': 'version',
54
56
  'creator_id': 'creator_id',
@@ -56,7 +58,7 @@ class WorkspaceTemplateVersionDataObject(object):
56
58
  'created_at': 'created_at'
57
59
  }
58
60
 
59
- def __init__(self, template_id=None, image_uri=None, compute_configs=None, artifacts=None, id=None, version=None, creator_id=None, creator_email=None, created_at=None, local_vars_configuration=None): # noqa: E501
61
+ def __init__(self, template_id=None, image_uri=None, compute_configs=None, artifacts=None, idle_termination_minutes=None, id=None, version=None, creator_id=None, creator_email=None, created_at=None, local_vars_configuration=None): # noqa: E501
60
62
  """WorkspaceTemplateVersionDataObject - a model defined in OpenAPI""" # noqa: E501
61
63
  if local_vars_configuration is None:
62
64
  local_vars_configuration = Configuration()
@@ -66,6 +68,7 @@ class WorkspaceTemplateVersionDataObject(object):
66
68
  self._image_uri = None
67
69
  self._compute_configs = None
68
70
  self._artifacts = None
71
+ self._idle_termination_minutes = None
69
72
  self._id = None
70
73
  self._version = None
71
74
  self._creator_id = None
@@ -80,6 +83,8 @@ class WorkspaceTemplateVersionDataObject(object):
80
83
  self.compute_configs = compute_configs
81
84
  if artifacts is not None:
82
85
  self.artifacts = artifacts
86
+ if idle_termination_minutes is not None:
87
+ self.idle_termination_minutes = idle_termination_minutes
83
88
  self.id = id
84
89
  self.version = version
85
90
  self.creator_id = creator_id
@@ -180,6 +185,29 @@ class WorkspaceTemplateVersionDataObject(object):
180
185
 
181
186
  self._artifacts = artifacts
182
187
 
188
+ @property
189
+ def idle_termination_minutes(self):
190
+ """Gets the idle_termination_minutes of this WorkspaceTemplateVersionDataObject. # noqa: E501
191
+
192
+ Idle termination minutes for this version # noqa: E501
193
+
194
+ :return: The idle_termination_minutes of this WorkspaceTemplateVersionDataObject. # noqa: E501
195
+ :rtype: int
196
+ """
197
+ return self._idle_termination_minutes
198
+
199
+ @idle_termination_minutes.setter
200
+ def idle_termination_minutes(self, idle_termination_minutes):
201
+ """Sets the idle_termination_minutes of this WorkspaceTemplateVersionDataObject.
202
+
203
+ Idle termination minutes for this version # noqa: E501
204
+
205
+ :param idle_termination_minutes: The idle_termination_minutes of this WorkspaceTemplateVersionDataObject. # noqa: E501
206
+ :type: int
207
+ """
208
+
209
+ self._idle_termination_minutes = idle_termination_minutes
210
+
183
211
  @property
184
212
  def id(self):
185
213
  """Gets the id of this WorkspaceTemplateVersionDataObject. # noqa: E501
@@ -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
 
@@ -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.",