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
@@ -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
@@ -22,14 +22,20 @@ from anyscale.service.commands import (
22
22
  _ROLLBACK_EXAMPLE,
23
23
  _STATUS_ARG_DOCSTRINGS,
24
24
  _STATUS_EXAMPLE,
25
+ _TAGS_ADD_ARG_DOCSTRINGS,
26
+ _TAGS_ADD_EXAMPLE,
27
+ _TAGS_REMOVE_ARG_DOCSTRINGS,
28
+ _TAGS_REMOVE_EXAMPLE,
25
29
  _TERMINATE_ARG_DOCSTRINGS,
26
30
  _TERMINATE_EXAMPLE,
27
31
  _WAIT_ARG_DOCSTRINGS,
28
32
  _WAIT_EXAMPLE,
33
+ add_tags,
29
34
  archive,
30
35
  delete,
31
36
  deploy,
32
37
  list,
38
+ remove_tags,
33
39
  rollback,
34
40
  status,
35
41
  terminate,
@@ -181,6 +187,7 @@ class ServiceSDK:
181
187
  # Filters
182
188
  name: Optional[str] = None,
183
189
  state_filter: Optional[Union[List[ServiceState], List[str]]] = None,
190
+ tags_filter: Optional[Dict[str, List[str]]] = None,
184
191
  creator_id: Optional[str] = None,
185
192
  cloud: Optional[str] = None,
186
193
  project: Optional[str] = None,
@@ -205,6 +212,7 @@ class ServiceSDK:
205
212
  service_id=service_id,
206
213
  name=name,
207
214
  state_filter=state_filter,
215
+ tags_filter=tags_filter,
208
216
  creator_id=creator_id,
209
217
  cloud=cloud,
210
218
  project=project,
@@ -270,3 +278,35 @@ class ServiceSDK:
270
278
  mode=mode,
271
279
  max_lines=max_lines,
272
280
  )
281
+
282
+ @sdk_docs(doc_py_example=_TAGS_ADD_EXAMPLE, arg_docstrings=_TAGS_ADD_ARG_DOCSTRINGS)
283
+ def add_tags( # noqa: F811
284
+ self,
285
+ *,
286
+ id: Optional[str] = None, # noqa: A002
287
+ name: Optional[str] = None,
288
+ cloud: Optional[str] = None,
289
+ project: Optional[str] = None,
290
+ tags: Dict[str, str],
291
+ ) -> None:
292
+ """Upsert (add/update) tag key/value pairs for a service."""
293
+ return self._private_sdk.add_tags(
294
+ id=id, name=name, cloud=cloud, project=project, tags=tags
295
+ )
296
+
297
+ @sdk_docs(
298
+ doc_py_example=_TAGS_REMOVE_EXAMPLE, arg_docstrings=_TAGS_REMOVE_ARG_DOCSTRINGS
299
+ )
300
+ def remove_tags( # noqa: F811
301
+ self,
302
+ *,
303
+ id: Optional[str] = None, # noqa: A002
304
+ name: Optional[str] = None,
305
+ cloud: Optional[str] = None,
306
+ project: Optional[str] = None,
307
+ keys: List[str],
308
+ ) -> None:
309
+ """Remove tags by key from a service."""
310
+ return self._private_sdk.remove_tags(
311
+ id=id, name=name, cloud=cloud, project=project, keys=keys
312
+ )
@@ -20,6 +20,9 @@ from anyscale.client.openapi_client.models.decoratedlistserviceapimodel_list_res
20
20
  from anyscale.client.openapi_client.models.list_response_metadata import (
21
21
  ListResponseMetadata,
22
22
  )
23
+ from anyscale.client.openapi_client.models.resource_tag_resource_type import (
24
+ ResourceTagResourceType,
25
+ )
23
26
  from anyscale.compute_config.models import ComputeConfig
24
27
  from anyscale.sdk.anyscale_client.models import (
25
28
  AccessConfig,
@@ -296,6 +299,7 @@ class PrivateServiceSDK(WorkloadSDK):
296
299
  ),
297
300
  ray_gcs_external_storage_config=existing_config.ray_gcs_external_storage_config,
298
301
  tracing_config=existing_config.tracing_config,
302
+ tags=getattr(config, "tags", None),
299
303
  )
300
304
 
301
305
  def _build_apply_service_model_for_rollout( # noqa: PLR0912
@@ -431,6 +435,7 @@ class PrivateServiceSDK(WorkloadSDK):
431
435
  ray_gcs_external_storage_config=ray_gcs_external_storage_config,
432
436
  tracing_config=tracing_config,
433
437
  traffic_percent=traffic_percent,
438
+ tags=getattr(config, "tags", None),
434
439
  )
435
440
 
436
441
  def _build_apply_service_model_for_multi_version(
@@ -454,13 +459,20 @@ class PrivateServiceSDK(WorkloadSDK):
454
459
  if existing_service is not None:
455
460
  existing_versions = {
456
461
  v.version: v
457
- for v in self.client.get_service_versions(existing_service.id)
462
+ for v in self.client.get_service_versions(
463
+ existing_service.id, read_all_versions=True
464
+ )
458
465
  }
459
466
 
460
467
  service_versions: List[ApplyProductionServiceV2Model] = []
461
468
  for _, (config, version) in version_service_map.items():
462
469
  version_name = version["name"]
463
470
  traffic_percent = version["traffic_percent"]
471
+ if traffic_percent == 0:
472
+ self.logger.warning(
473
+ f"Setting traffic for version {version_name} to 0 will terminate it."
474
+ )
475
+
464
476
  # If config is None, it means the version is already deployed, and the user is likely to be only updating the traffic/capacity percent without serve config update.
465
477
  if config is None:
466
478
  if version_name not in existing_versions:
@@ -945,14 +957,18 @@ class PrivateServiceSDK(WorkloadSDK):
945
957
  sampling_ratio=model.tracing_config.sampling_ratio,
946
958
  )
947
959
 
960
+ version_name = model.version or self._get_user_facing_service_version_id(model)
961
+
948
962
  return ServiceVersionStatus(
949
963
  id=self._get_user_facing_service_version_id(model),
964
+ name=version_name,
950
965
  created_at=model.created_at,
951
966
  state=model.current_state,
952
967
  # NOTE(edoakes): there is also a "current_weight" field but it does not match the UI.
953
968
  weight=model.weight,
954
969
  config=ServiceConfig(
955
970
  name=service_name,
971
+ version_name=version_name,
956
972
  applications=model.ray_serve_config["applications"],
957
973
  image_uri=str(image_uri),
958
974
  compute_config=compute_config,
@@ -996,35 +1012,63 @@ class PrivateServiceSDK(WorkloadSDK):
996
1012
  # which means that the per-version `query_auth_token_enabled` field will lie if
997
1013
  # it's changed.
998
1014
  query_auth_token_enabled = model.auth_token is not None
1015
+ all_versions = None
1016
+ primary_version = None
1017
+ canary_version = None
1018
+ project_name = None
999
1019
 
1000
- primary_version_task = None
1001
- if model.primary_version is not None:
1002
- primary_version_task = asyncio.create_task(
1003
- self._service_version_model_to_status_async(
1004
- model.primary_version,
1005
- service_name=model.name,
1006
- project_id=model.project_id,
1007
- query_auth_token_enabled=query_auth_token_enabled,
1020
+ if model.is_multi_version:
1021
+ # For multi-version services, format all versions
1022
+ version_tasks = [
1023
+ asyncio.create_task(
1024
+ self._service_version_model_to_status_async(
1025
+ version,
1026
+ service_name=model.name,
1027
+ project_id=model.project_id,
1028
+ query_auth_token_enabled=query_auth_token_enabled,
1029
+ )
1008
1030
  )
1009
- )
1031
+ for version in model.versions
1032
+ ]
1033
+ all_versions = await asyncio.gather(*version_tasks)
1034
+
1035
+ if (
1036
+ all_versions
1037
+ and all_versions[0]
1038
+ and isinstance(all_versions[0].config, ServiceConfig)
1039
+ ):
1040
+ project_name = all_versions[0].config.project
1010
1041
 
1011
- canary_version_task = None
1012
- if model.canary_version is not None:
1013
- canary_version_task = asyncio.create_task(
1014
- self._service_version_model_to_status_async(
1015
- model.canary_version,
1016
- service_name=model.name,
1017
- project_id=model.project_id,
1018
- query_auth_token_enabled=query_auth_token_enabled,
1042
+ else:
1043
+ primary_version_task = None
1044
+ if model.primary_version is not None:
1045
+ primary_version_task = asyncio.create_task(
1046
+ self._service_version_model_to_status_async(
1047
+ model.primary_version,
1048
+ service_name=model.name,
1049
+ project_id=model.project_id,
1050
+ query_auth_token_enabled=query_auth_token_enabled,
1051
+ )
1052
+ )
1053
+
1054
+ canary_version_task = None
1055
+ if model.canary_version is not None:
1056
+ canary_version_task = asyncio.create_task(
1057
+ self._service_version_model_to_status_async(
1058
+ model.canary_version,
1059
+ service_name=model.name,
1060
+ project_id=model.project_id,
1061
+ query_auth_token_enabled=query_auth_token_enabled,
1062
+ )
1019
1063
  )
1020
- )
1021
1064
 
1022
- primary_version = await primary_version_task if primary_version_task else None
1023
- canary_version = await canary_version_task if canary_version_task else None
1065
+ primary_version = (
1066
+ await primary_version_task if primary_version_task else None
1067
+ )
1068
+ canary_version = await canary_version_task if canary_version_task else None
1024
1069
 
1025
- project_name = None
1026
- if primary_version and isinstance(primary_version.config, ServiceConfig):
1027
- project_name = primary_version.config.project
1070
+ if primary_version and isinstance(primary_version.config, ServiceConfig):
1071
+ project_name = primary_version.config.project
1028
1072
 
1029
1073
  return ServiceStatus(
1030
1074
  id=model.id,
@@ -1036,6 +1080,7 @@ class PrivateServiceSDK(WorkloadSDK):
1036
1080
  primary_version=primary_version,
1037
1081
  canary_version=canary_version,
1038
1082
  project=project_name,
1083
+ versions=all_versions,
1039
1084
  )
1040
1085
 
1041
1086
  def status(
@@ -1056,6 +1101,7 @@ class PrivateServiceSDK(WorkloadSDK):
1056
1101
  cloud: Optional[str] = None,
1057
1102
  project: Optional[str] = None,
1058
1103
  include_archived: bool = False,
1104
+ tags_filter: Optional[Dict[str, List[str]]] = None,
1059
1105
  # Paging
1060
1106
  max_items: Optional[int] = None, # Controls total items yielded by iterator
1061
1107
  page_size: Optional[int] = None, # Controls items fetched per API call
@@ -1096,6 +1142,18 @@ class PrivateServiceSDK(WorkloadSDK):
1096
1142
 
1097
1143
  normalised_states = _normalize_state_filter(state_filter)
1098
1144
 
1145
+ # Convert tags dict into backend tag_filter list[str]
1146
+ backend_tag_filter: Optional[List[str]] = None
1147
+ if tags_filter:
1148
+ flattened: List[str] = []
1149
+ for key, values in tags_filter.items():
1150
+ if not values:
1151
+ continue
1152
+ for value in values:
1153
+ if key and value:
1154
+ flattened.append(f"{key}:{value}")
1155
+ backend_tag_filter = flattened if len(flattened) > 0 else None
1156
+
1099
1157
  def _fetch_page(
1100
1158
  token: Optional[str],
1101
1159
  ) -> DecoratedlistserviceapimodelListResponse:
@@ -1106,6 +1164,7 @@ class PrivateServiceSDK(WorkloadSDK):
1106
1164
  cloud=cloud,
1107
1165
  project=project,
1108
1166
  include_archived=include_archived,
1167
+ tag_filter=backend_tag_filter,
1109
1168
  count=page_size,
1110
1169
  paging_token=token,
1111
1170
  sort_field=sort_field,
@@ -1199,6 +1258,44 @@ class PrivateServiceSDK(WorkloadSDK):
1199
1258
  model.primary_version, head, max_lines
1200
1259
  )
1201
1260
 
1261
+ def add_tags(
1262
+ self,
1263
+ *,
1264
+ id: Optional[str] = None, # noqa: A002
1265
+ name: Optional[str] = None,
1266
+ cloud: Optional[str] = None,
1267
+ project: Optional[str] = None,
1268
+ tags: Dict[str, str],
1269
+ ) -> None:
1270
+ if id is None:
1271
+ model = self._resolve_to_service_model(
1272
+ name=name, cloud=cloud, project=project
1273
+ )
1274
+ resource_id = model.id
1275
+ assert id is not None
1276
+ self.client.upsert_resource_tags(
1277
+ ResourceTagResourceType.SERVICE, resource_id, tags
1278
+ )
1279
+
1280
+ def remove_tags(
1281
+ self,
1282
+ *,
1283
+ id: Optional[str] = None, # noqa: A002
1284
+ name: Optional[str] = None,
1285
+ cloud: Optional[str] = None,
1286
+ project: Optional[str] = None,
1287
+ keys: List[str],
1288
+ ) -> None:
1289
+ if id is None:
1290
+ model = self._resolve_to_service_model(
1291
+ name=name, cloud=cloud, project=project
1292
+ )
1293
+ resource_id = model.id
1294
+ assert id is not None
1295
+ self.client.delete_resource_tags(
1296
+ ResourceTagResourceType.SERVICE, resource_id, keys
1297
+ )
1298
+
1202
1299
 
1203
1300
  def _normalize_state_filter(
1204
1301
  states: Optional[Union[List[ServiceState], List[str]]],
@@ -1,4 +1,4 @@
1
- from typing import List, Optional, Union
1
+ from typing import Dict, List, Optional, Union
2
2
 
3
3
  from anyscale._private.models.model_base import ResultIterator
4
4
  from anyscale._private.sdk import sdk_command
@@ -323,6 +323,77 @@ _CONTROLLER_LOGS_ARG_DOCSTRINGS = {
323
323
  "max_lines": "The number of log lines to be fetched. If not provided, 1000 lines will be fetched.",
324
324
  }
325
325
 
326
+ # SDK docs for Service tag operations
327
+ _TAGS_ADD_EXAMPLE = """
328
+ import anyscale
329
+
330
+ anyscale.service.add_tags(id="svc_123", tags={"team": "mlops", "env": "prod"})
331
+ """
332
+
333
+ _TAGS_ADD_ARG_DOCSTRINGS = {
334
+ "id": "ID of the service. Provide either id or name.",
335
+ "name": "Name of the service. Provide either id or name.",
336
+ "cloud": "Cloud name (used when resolving by name).",
337
+ "project": "Project name (used when resolving by name).",
338
+ "tags": "Key/value tags to upsert as a map {key: value}.",
339
+ }
340
+
341
+ _TAGS_REMOVE_EXAMPLE = """
342
+ import anyscale
343
+
344
+ anyscale.service.remove_tags(id="svc_123", keys=["team", "env"])
345
+ """
346
+
347
+ _TAGS_REMOVE_ARG_DOCSTRINGS = {
348
+ "id": "ID of the service. Provide either id or name.",
349
+ "name": "Name of the service. Provide either id or name.",
350
+ "cloud": "Cloud name (used when resolving by name).",
351
+ "project": "Project name (used when resolving by name).",
352
+ "keys": "List of tag keys to remove.",
353
+ }
354
+
355
+
356
+ @sdk_command(
357
+ _SERVICE_SDK_SINGLETON_KEY,
358
+ PrivateServiceSDK,
359
+ doc_py_example=_TAGS_ADD_EXAMPLE,
360
+ arg_docstrings=_TAGS_ADD_ARG_DOCSTRINGS,
361
+ )
362
+ def add_tags(
363
+ *,
364
+ id: Optional[str] = None, # noqa: A002
365
+ name: Optional[str] = None,
366
+ cloud: Optional[str] = None,
367
+ project: Optional[str] = None,
368
+ tags: Dict[str, str],
369
+ _private_sdk: Optional[PrivateServiceSDK] = None,
370
+ ) -> None:
371
+ """Upsert (add/update) tag key/value pairs for a service."""
372
+ return _private_sdk.add_tags( # type: ignore
373
+ id=id, name=name, cloud=cloud, project=project, tags=tags
374
+ )
375
+
376
+
377
+ @sdk_command(
378
+ _SERVICE_SDK_SINGLETON_KEY,
379
+ PrivateServiceSDK,
380
+ doc_py_example=_TAGS_REMOVE_EXAMPLE,
381
+ arg_docstrings=_TAGS_REMOVE_ARG_DOCSTRINGS,
382
+ )
383
+ def remove_tags(
384
+ *,
385
+ id: Optional[str] = None, # noqa: A002
386
+ name: Optional[str] = None,
387
+ cloud: Optional[str] = None,
388
+ project: Optional[str] = None,
389
+ keys: List[str],
390
+ _private_sdk: Optional[PrivateServiceSDK] = None,
391
+ ) -> None:
392
+ """Remove tags by key from a service."""
393
+ return _private_sdk.remove_tags( # type: ignore
394
+ id=id, name=name, cloud=cloud, project=project, keys=keys
395
+ )
396
+
326
397
 
327
398
  # This is a private command that is not exposed to the user.
328
399
  @sdk_command(
@@ -375,6 +446,7 @@ _LIST_ARG_DOCSTRINGS = {
375
446
  "cloud": "Name of the Anyscale Cloud to search in.",
376
447
  "project": "Name of the Anyscale Project to search in.",
377
448
  "include_archived": "Include archived services (default: False).",
449
+ "tags_filter": "Filter services by tags as a dict: {key: [values...]}. Values with the same key are ORed; keys are ANDed.",
378
450
  # Paging
379
451
  "max_items": "Maximum **total** number of items to yield (default: iterate all).",
380
452
  "page_size": "Number of items to fetch per API request (default: API default).",
@@ -402,6 +474,7 @@ def list( # noqa: A001
402
474
  cloud: Optional[str] = None,
403
475
  project: Optional[str] = None,
404
476
  include_archived: bool = False,
477
+ tags_filter: Optional[Dict[str, List[str]]] = None,
405
478
  # Paging
406
479
  max_items: Optional[int] = None,
407
480
  page_size: Optional[int] = None,
@@ -420,6 +493,7 @@ def list( # noqa: A001
420
493
  cloud=cloud,
421
494
  project=project,
422
495
  include_archived=include_archived,
496
+ tags_filter=tags_filter,
423
497
  max_items=max_items,
424
498
  page_size=page_size,
425
499
  sort_field=sort_field,
@@ -213,6 +213,9 @@ tracing_config: # (Optional) Configuration options for tracing.
213
213
  enabled: true
214
214
  exporter_import_path: my_module:custom_tracing_exporter
215
215
  sampling_ratio: 1.0
216
+ tags:
217
+ team: serving
218
+ cost-center: eng
216
219
  """
217
220
 
218
221
  # Override the `name` field from `WorkloadConfig` so we can document it separately for jobs and services.
@@ -239,8 +242,6 @@ tracing_config: # (Optional) Configuration options for tracing.
239
242
  # Allow None if optional; enforce non-empty string if required
240
243
  if version_name is None:
241
244
  return None
242
- if not isinstance(version_name, str) or not version_name.strip():
243
- raise ValueError("version_name must be a non-empty string")
244
245
  return version_name
245
246
 
246
247
  def _validate_applications(self, applications: List[Dict[str, Any]]):
@@ -402,6 +403,19 @@ tracing_config: # (Optional) Configuration options for tracing.
402
403
 
403
404
  return tracing_config
404
405
 
406
+ tags: Optional[Dict[str, str]] = field(
407
+ default=None, metadata={"docstring": "Tags to associate with the service."},
408
+ )
409
+
410
+ def _validate_tags(self, tags: Optional[Dict[str, str]]):
411
+ if tags is None:
412
+ return
413
+ if not isinstance(tags, dict):
414
+ raise TypeError("'tags' must be a Dict[str, str].")
415
+ for k, v in tags.items():
416
+ if not isinstance(k, str) or not isinstance(v, str):
417
+ raise TypeError("'tags' must be a Dict[str, str].")
418
+
405
419
 
406
420
  class ServiceState(ModelEnum):
407
421
  """Current state of a service."""
@@ -499,6 +513,7 @@ id: service2_uz6l8yhy2as5wrer3shzj6kh67
499
513
  state: RUNNING
500
514
  primary_version:
501
515
  id: 601bd56c4b
516
+ name: v1
502
517
  state: RUNNING
503
518
  weight: 100
504
519
  created_at: 2025-04-18 17:21:28.323174+00:00
@@ -514,6 +529,15 @@ primary_version:
514
529
  if not isinstance(id, str):
515
530
  raise TypeError("'id' must be a string.")
516
531
 
532
+ name: str = field(
533
+ metadata={"docstring": "Human-readable name of the service version."}
534
+ )
535
+
536
+ def _validate_name(self, name: str):
537
+ if name is None:
538
+ return None
539
+ return name
540
+
517
541
  state: Union[ServiceVersionState, str] = field(
518
542
  metadata={"docstring": "Current state of the service version."}
519
543
  )
@@ -700,6 +724,26 @@ primary_version:
700
724
  if project is not None and not isinstance(project, str):
701
725
  raise TypeError("project must be a string.")
702
726
 
727
+ versions: Optional[List[ServiceVersionStatus]] = field(
728
+ default=None,
729
+ repr=False,
730
+ metadata={
731
+ "docstring": "All active versions of the service. For multi-version services, this contains all versions with their traffic weights."
732
+ },
733
+ )
734
+
735
+ def _validate_versions(
736
+ self, versions: Optional[List[ServiceVersionStatus]]
737
+ ) -> Optional[List[ServiceVersionStatus]]:
738
+ if versions is None:
739
+ return None
740
+ if not isinstance(versions, list):
741
+ raise TypeError("'versions' must be a list.")
742
+ for v in versions:
743
+ if not isinstance(v, ServiceVersionStatus):
744
+ raise TypeError("'versions' must be a list of ServiceVersionStatus.")
745
+ return versions
746
+
703
747
 
704
748
  class ServiceSortField(ModelEnum):
705
749
  """Fields available for sorting services."""
@@ -1,3 +1,3 @@
1
1
  # AUTOGENERATED - modify shared_anyscale_util in root directory to make changes
2
2
  # RAY_RELEASE_UPDATE: managed by release automation.
3
- LATEST_RAY_VERSION = "2.50.1"
3
+ LATEST_RAY_VERSION = "2.51.0"
anyscale/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.26.70"
1
+ __version__ = "0.26.71"
@@ -169,6 +169,7 @@ class PrivateWorkspaceSDK(WorkloadSDK):
169
169
  cluster_environment_build_id=build_id,
170
170
  idle_timeout_minutes=config.idle_termination_minutes,
171
171
  cloud_id=cloud_id,
172
+ tags=getattr(config, "tags", None),
172
173
  skip_start=True,
173
174
  )
174
175
  )
@@ -53,6 +53,9 @@ idle_termination_minutes: 220
53
53
  env_vars:
54
54
  key: value
55
55
  requirements: /tmp/requirements.txt
56
+ tags:
57
+ team: mlops
58
+ env: prod
56
59
  """
57
60
 
58
61
  name: Optional[str] = field(
@@ -88,6 +91,19 @@ requirements: /tmp/requirements.txt
88
91
  if containerfile is not None and not isinstance(containerfile, str):
89
92
  raise TypeError("'containerfile' must be a string.")
90
93
 
94
+ tags: Optional[Dict[str, str]] = field(
95
+ default=None, metadata={"docstring": "Tags to associate with the workspace."},
96
+ )
97
+
98
+ def _validate_tags(self, tags: Optional[Dict[str, str]]):
99
+ if tags is None:
100
+ return
101
+ if not isinstance(tags, dict):
102
+ raise TypeError("'tags' must be a Dict[str, str].")
103
+ for k, v in tags.items():
104
+ if not isinstance(k, str) or not isinstance(v, str):
105
+ raise TypeError("'tags' must be a Dict[str, str].")
106
+
91
107
  compute_config: Union[ComputeConfigType, Dict, str, None] = field(
92
108
  default=None,
93
109
  metadata={
@@ -215,6 +231,9 @@ idle_termination_minutes: 220
215
231
  env_vars:
216
232
  key: value
217
233
  requirements: /tmp/requirements.txt
234
+ tags:
235
+ team: mlops
236
+ env: prod
218
237
  """
219
238
 
220
239
  project: Optional[str] = field(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anyscale
3
- Version: 0.26.70
3
+ Version: 0.26.71
4
4
  Summary: Command Line Interface for Anyscale
5
5
  Author: Anyscale Inc.
6
6
  License: AS License