anyscale 0.26.68__py3-none-any.whl → 0.26.70__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 (71) hide show
  1. anyscale/_private/anyscale_client/anyscale_client.py +67 -1
  2. anyscale/_private/anyscale_client/common.py +20 -1
  3. anyscale/_private/anyscale_client/fake_anyscale_client.py +77 -10
  4. anyscale/client/README.md +16 -4
  5. anyscale/client/openapi_client/__init__.py +12 -4
  6. anyscale/client/openapi_client/api/default_api.py +588 -23
  7. anyscale/client/openapi_client/models/__init__.py +12 -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_production_service_multi_version_v2_model.py +207 -0
  11. anyscale/client/openapi_client/models/apply_production_service_v2_model.py +31 -3
  12. anyscale/client/openapi_client/models/baseimagesenum.py +70 -1
  13. anyscale/client/openapi_client/models/cloud_data_bucket_file_type.py +2 -1
  14. anyscale/client/openapi_client/models/{oauthconnectionresponse_response.py → clouddeployment_response.py} +11 -11
  15. anyscale/client/openapi_client/models/clusterdashboardnode_response.py +121 -0
  16. anyscale/client/openapi_client/models/create_experimental_workspace.py +29 -1
  17. anyscale/client/openapi_client/models/create_workspace_from_template.py +29 -1
  18. anyscale/client/openapi_client/models/create_workspace_template_version.py +31 -3
  19. anyscale/client/openapi_client/models/decorated_list_service_api_model.py +58 -1
  20. anyscale/client/openapi_client/models/decorated_production_service_v2_api_model.py +60 -3
  21. anyscale/client/openapi_client/models/decorated_service_event_api_model.py +3 -3
  22. anyscale/client/openapi_client/models/describe_machine_pool_machines_filters.py +33 -5
  23. anyscale/client/openapi_client/models/describe_machine_pool_workloads_filters.py +33 -5
  24. anyscale/client/openapi_client/models/{service_event_level.py → entity_type.py} +9 -9
  25. anyscale/client/openapi_client/models/event_level.py +2 -1
  26. anyscale/client/openapi_client/models/job_event_fields.py +206 -0
  27. anyscale/client/openapi_client/models/lineage_graph_node.py +70 -42
  28. anyscale/client/openapi_client/models/lineage_workload.py +31 -3
  29. anyscale/client/openapi_client/models/machine_type_partition_filter.py +152 -0
  30. anyscale/client/openapi_client/models/partition_info.py +30 -1
  31. anyscale/client/openapi_client/models/production_job_event.py +3 -3
  32. anyscale/client/openapi_client/models/rollout_strategy.py +2 -1
  33. anyscale/client/openapi_client/models/service_event_fields.py +318 -0
  34. anyscale/client/openapi_client/models/supportedbaseimagesenum.py +70 -1
  35. anyscale/client/openapi_client/models/task_summary_config.py +29 -3
  36. anyscale/client/openapi_client/models/task_table_config.py +29 -3
  37. anyscale/client/openapi_client/models/unified_event.py +377 -0
  38. anyscale/client/openapi_client/models/{ha_job_event_level.py → unified_origin_filter.py} +21 -9
  39. anyscale/client/openapi_client/models/unifiedevent_list_response.py +147 -0
  40. anyscale/client/openapi_client/models/workspace_event_fields.py +122 -0
  41. anyscale/client/openapi_client/models/workspace_template_version.py +30 -1
  42. anyscale/client/openapi_client/models/workspace_template_version_data_object.py +30 -1
  43. anyscale/cloud/models.py +2 -2
  44. anyscale/commands/cloud_commands.py +148 -11
  45. anyscale/commands/command_examples.py +53 -0
  46. anyscale/commands/job_commands.py +1 -1
  47. anyscale/commands/service_commands.py +130 -67
  48. anyscale/commands/setup_k8s.py +615 -49
  49. anyscale/controllers/cloud_controller.py +19 -5
  50. anyscale/controllers/kubernetes_verifier.py +80 -66
  51. anyscale/job/_private/job_sdk.py +47 -1
  52. anyscale/job/commands.py +3 -0
  53. anyscale/sdk/anyscale_client/models/apply_production_service_v2_model.py +31 -3
  54. anyscale/sdk/anyscale_client/models/apply_service_model.py +31 -3
  55. anyscale/sdk/anyscale_client/models/baseimagesenum.py +70 -1
  56. anyscale/sdk/anyscale_client/models/rollout_strategy.py +2 -1
  57. anyscale/sdk/anyscale_client/models/supportedbaseimagesenum.py +70 -1
  58. anyscale/service/__init__.py +11 -3
  59. anyscale/service/_private/service_sdk.py +361 -35
  60. anyscale/service/commands.py +15 -3
  61. anyscale/service/models.py +12 -0
  62. anyscale/shared_anyscale_utils/latest_ray_version.py +1 -1
  63. anyscale/version.py +1 -1
  64. {anyscale-0.26.68.dist-info → anyscale-0.26.70.dist-info}/METADATA +1 -1
  65. {anyscale-0.26.68.dist-info → anyscale-0.26.70.dist-info}/RECORD +70 -62
  66. anyscale/client/openapi_client/models/o_auth_connection_response.py +0 -229
  67. {anyscale-0.26.68.dist-info → anyscale-0.26.70.dist-info}/WHEEL +0 -0
  68. {anyscale-0.26.68.dist-info → anyscale-0.26.70.dist-info}/entry_points.txt +0 -0
  69. {anyscale-0.26.68.dist-info → anyscale-0.26.70.dist-info}/licenses/LICENSE +0 -0
  70. {anyscale-0.26.68.dist-info → anyscale-0.26.70.dist-info}/licenses/NOTICE +0 -0
  71. {anyscale-0.26.68.dist-info → anyscale-0.26.70.dist-info}/top_level.txt +0 -0
@@ -1,12 +1,19 @@
1
1
  import asyncio
2
2
  import copy
3
- from typing import Any, Dict, List, Optional, Union
3
+ import json
4
+ from typing import Any, Dict, List, Optional, Tuple, Union
4
5
 
5
6
  from anyscale._private.models.model_base import ResultIterator
6
7
  from anyscale._private.workload import WorkloadSDK
8
+ from anyscale.client.openapi_client.models.apply_production_service_multi_version_v2_model import (
9
+ ApplyProductionServiceMultiVersionV2Model,
10
+ )
7
11
  from anyscale.client.openapi_client.models.decorated_production_service_v2_api_model import (
8
12
  DecoratedProductionServiceV2APIModel,
9
13
  )
14
+ from anyscale.client.openapi_client.models.decorated_production_service_v2_version_api_model import (
15
+ DecoratedProductionServiceV2VersionAPIModel,
16
+ )
10
17
  from anyscale.client.openapi_client.models.decoratedlistserviceapimodel_list_response import (
11
18
  DecoratedlistserviceapimodelListResponse,
12
19
  )
@@ -21,6 +28,7 @@ from anyscale.sdk.anyscale_client.models import (
21
28
  ProductionServiceV2VersionModel,
22
29
  Protocols,
23
30
  RayGCSExternalStorageConfig as APIRayGCSExternalStorageConfig,
31
+ RolloutStrategy,
24
32
  ServiceConfig as ExternalAPIServiceConfig,
25
33
  ServiceEventCurrentState,
26
34
  ServiceSortField,
@@ -124,16 +132,21 @@ class PrivateServiceSDK(WorkloadSDK):
124
132
  canary_percent: Optional[int],
125
133
  ):
126
134
  """Log user-facing information about a deployed service."""
127
- version_id_info = "version ID: {version_id}".format(
128
- version_id=self._get_user_facing_service_version_id(
129
- service.canary_version
130
- if service.canary_version is not None
131
- else service.primary_version
135
+ if not service.primary_version:
136
+ version_info = "versions: {versions}".format(
137
+ versions=[v.version for v in service.versions]
138
+ )
139
+ else:
140
+ version_info = "version ID: {version_id}".format(
141
+ version_id=self._get_user_facing_service_version_id(
142
+ service.canary_version
143
+ if service.canary_version is not None
144
+ else service.primary_version
145
+ )
132
146
  )
133
- )
134
147
  details = (
135
148
  "("
136
- + version_id_info
149
+ + version_info
137
150
  + (
138
151
  ")"
139
152
  if canary_percent is None
@@ -270,6 +283,7 @@ class PrivateServiceSDK(WorkloadSDK):
270
283
  )
271
284
  return ApplyProductionServiceV2Model(
272
285
  name=name,
286
+ version=config.version_name,
273
287
  project_id=project_id,
274
288
  ray_serve_config=self._build_ray_serve_config(config),
275
289
  build_id=existing_config.build_id,
@@ -288,10 +302,12 @@ class PrivateServiceSDK(WorkloadSDK):
288
302
  self,
289
303
  name: str,
290
304
  config: ServiceConfig,
305
+ rollout_strategy: RolloutStrategy,
291
306
  *,
292
307
  canary_percent: Optional[int] = None,
293
308
  max_surge_percent: Optional[int] = None,
294
309
  existing_service: Optional[DecoratedProductionServiceV2APIModel] = None,
310
+ traffic_percent: Optional[int] = None,
295
311
  ) -> ApplyProductionServiceV2Model:
296
312
  """Build the ApplyProductionServiceV2Model for a rolling update."""
297
313
 
@@ -399,13 +415,14 @@ class PrivateServiceSDK(WorkloadSDK):
399
415
 
400
416
  return ApplyProductionServiceV2Model(
401
417
  name=name,
418
+ version=config.version_name,
402
419
  project_id=project_id,
403
420
  ray_serve_config=self._build_ray_serve_config(config),
404
421
  build_id=build_id,
405
422
  compute_config_id=compute_config_id,
406
423
  canary_percent=canary_percent,
407
424
  max_surge_percent=max_surge_percent,
408
- rollout_strategy="ROLLOUT",
425
+ rollout_strategy=rollout_strategy,
409
426
  config=ExternalAPIServiceConfig(
410
427
  access=AccessConfig(use_bearer_token=config.query_auth_token_enabled),
411
428
  protocols=Protocols(grpc=self._build_grpc_protocol_config(config)),
@@ -413,36 +430,319 @@ class PrivateServiceSDK(WorkloadSDK):
413
430
  ),
414
431
  ray_gcs_external_storage_config=ray_gcs_external_storage_config,
415
432
  tracing_config=tracing_config,
433
+ traffic_percent=traffic_percent,
416
434
  )
417
435
 
418
- def deploy( # noqa: PLR0912
436
+ def _build_apply_service_model_for_multi_version(
419
437
  self,
420
- config: ServiceConfig,
438
+ name: str,
439
+ cloud: Optional[str],
440
+ project: Optional[str],
441
+ configs: List[ServiceConfig],
442
+ existing_service: Optional[DecoratedProductionServiceV2APIModel],
443
+ versions: List[Dict[str, Any]],
444
+ ) -> ApplyProductionServiceV2Model:
445
+ cloud_id = self.client.get_cloud_id(cloud_name=cloud)
446
+ project_id = self.client.get_project_id(parent_cloud_id=cloud_id, name=project)
447
+
448
+ # implement a map between version name and config file
449
+ # This way, we can iterate over the version config and corresponding config file to the _build_apply_service_model_for_rollout function
450
+ # Also, maybe add an assertion to confirm the version name is the same as the config file name.
451
+ # I also need to add a validation logic to confirm if all the version config passed(the version name) all has the corresponding config file.
452
+ version_service_map = self._get_service_version_map(configs, versions, existing_service) # type: ignore
453
+ existing_versions: Dict[str, DecoratedProductionServiceV2VersionAPIModel] = {}
454
+ if existing_service is not None:
455
+ existing_versions = {
456
+ v.version: v
457
+ for v in self.client.get_service_versions(existing_service.id)
458
+ }
459
+
460
+ service_versions: List[ApplyProductionServiceV2Model] = []
461
+ for _, (config, version) in version_service_map.items():
462
+ version_name = version["name"]
463
+ traffic_percent = version["traffic_percent"]
464
+ # 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
+ if config is None:
466
+ if version_name not in existing_versions:
467
+ raise ValueError(
468
+ f"Version {version_name} does not exist in the existing service."
469
+ )
470
+ existing_version = existing_versions[version_name]
471
+
472
+ service_version = ApplyProductionServiceV2Model(
473
+ name=name,
474
+ project_id=project_id,
475
+ ray_serve_config=existing_version.ray_serve_config,
476
+ build_id=existing_version.build_id,
477
+ compute_config_id=existing_version.compute_config_id,
478
+ rollout_strategy=RolloutStrategy.MULTI_VERSION,
479
+ # TODO(doyoung): "config" is a service level config, so I'm assuming this is not necessary when this model is
480
+ # representing a service version. If not, may need to add and store 'config' field in the version model, ProductionServiceV2VersionModel,
481
+ # from the backend when the service version first gets created so it can be read from here for reconstruction.
482
+ config=None,
483
+ ray_gcs_external_storage_config=existing_version.ray_gcs_external_storage_config,
484
+ tracing_config=existing_version.tracing_config,
485
+ version=version_name,
486
+ traffic_percent=traffic_percent,
487
+ )
488
+ else:
489
+ service_version = self._build_apply_service_model_for_rollout(
490
+ name,
491
+ config,
492
+ RolloutStrategy.MULTI_VERSION,
493
+ existing_service=existing_service,
494
+ traffic_percent=traffic_percent,
495
+ )
496
+
497
+ service_versions.append(service_version)
498
+
499
+ assert len(service_versions) > 0, "service_versions should not be empty"
500
+ return ApplyProductionServiceMultiVersionV2Model(
501
+ rollout_strategy=RolloutStrategy.MULTI_VERSION,
502
+ cloud_id=cloud_id,
503
+ project_id=project_id,
504
+ service_versions=service_versions,
505
+ )
506
+
507
+ def _validate_versions(
508
+ self, versions: str
509
+ ) -> List[Dict[str, Any]]: # noqa: C901, PLR0912
510
+ # Parse versions JSON string to list of dictionaries
511
+ try:
512
+ versions = json.loads(versions)
513
+ except json.JSONDecodeError as e:
514
+ raise ValueError(f"Invalid JSON format for --versions: {e}")
515
+
516
+ for version in versions:
517
+ # traffic_percent must be specified in all versions by the users.
518
+ # If some version does not specify traffic_percent, raise an error.
519
+ if "name" not in version:
520
+ raise ValueError("Name is required for each version.")
521
+ if "traffic_percent" not in version:
522
+ raise ValueError("Traffic percent is required for each version.")
523
+
524
+ # Verify the key names are valid. Only either of name, traffic_percent, or capacity_percent are allowed.
525
+ for key in version:
526
+ if key not in [
527
+ "name",
528
+ "traffic_percent",
529
+ ]:
530
+ raise ValueError(f"Invalid key in versions: {key}")
531
+
532
+ if isinstance(versions, dict):
533
+ # Convert object form {"v1": 25, "v2": 75} to full list form
534
+ versions = [ # type: ignore
535
+ {"name": name, "traffic_percent": percent}
536
+ for name, percent in versions.items()
537
+ ]
538
+ elif not isinstance(versions, list):
539
+ raise ValueError(
540
+ "--versions must be a JSON array of objects or a JSON object in text format."
541
+ )
542
+
543
+ # Verify sum of each traffic_percent from each version is 100.
544
+ traffic_percent_sum = sum(version["traffic_percent"] for version in versions)
545
+ if traffic_percent_sum != 100:
546
+ raise ValueError("Sum of traffic percent for all versions must be 100.")
547
+
548
+ return versions # type: ignore
549
+
550
+ def _read_and_validate_configs(
551
+ self, configs: List[ServiceConfig]
552
+ ) -> Tuple[str, Optional[str], Optional[str]]:
553
+ """The service name, cloud, and project must be identical for all versions."""
554
+
555
+ # If all the name field from configs are identical, use the first one as the name.
556
+ # If all the name field from configs are not provided, set to None, use the default name.
557
+ if all(config.name is None for config in configs):
558
+ name = self._get_default_name()
559
+ elif all(config.name == configs[0].name for config in configs):
560
+ name = configs[0].name
561
+ else:
562
+ raise ValueError("All provided names in config files must be identical.")
563
+
564
+ # If all the cloud field from configs are identical, use the first one as the cloud.
565
+ # If all the cloud field from configs are all set to None, set cloud to None. Default cloud will be used from backend.
566
+ if all(config.cloud is None for config in configs):
567
+ cloud = None
568
+ elif all(config.cloud == configs[0].cloud for config in configs):
569
+ cloud = configs[0].cloud
570
+ else:
571
+ raise ValueError("All provided clouds in config files must be identical.")
572
+
573
+ # If all the project field from configs are identical, use the first one as the project.
574
+ # If all the project field from configs are all set to None, set project to None. Default project will be used from backend.
575
+ if all(config.project is None for config in configs):
576
+ project = None
577
+ elif all(config.project == configs[0].project for config in configs):
578
+ project = configs[0].project
579
+ else:
580
+ raise ValueError("All provided projects in config files must be identical.")
581
+
582
+ return name, cloud, project
583
+
584
+ def _get_service_version_map(
585
+ self,
586
+ configs: List[ServiceConfig],
587
+ versions: Optional[List[Dict[str, Any]]],
588
+ existing_service: Optional[DecoratedProductionServiceV2APIModel],
589
+ ) -> Dict[str, Tuple[ServiceConfig, Dict[str, Any]]]:
590
+ service_config_to_version_config = {}
591
+ if versions is not None:
592
+ if len(configs) > len(versions):
593
+ raise ValueError(
594
+ "Number of config files passed must be less than or equal to the number of version configs when multiple versions are being deployed."
595
+ )
596
+
597
+ for service_config in configs:
598
+ for version_config in versions:
599
+ if service_config.version_name == version_config["name"]:
600
+ service_config_to_version_config[version_config["name"]] = (
601
+ service_config,
602
+ version_config,
603
+ )
604
+ break
605
+
606
+ # version name in the ServiceConfig must have a matching version name in the version config.
607
+ # If this point is reached, then, it means there's a config file passed with version name that doesn't exist in the version config.
608
+ if service_config.version_name not in service_config_to_version_config:
609
+ raise ValueError(
610
+ f"version_name {service_config.version_name!r} in service config {service_config} does not match any of the version names provided with --versions."
611
+ )
612
+
613
+ # version name in version config doesn't need to have a matching version name in service config. If there's not a matching, this
614
+ # means the version is already deployed, and the user is likely to be only updating the traffic/capacity percent.
615
+ for version_config in versions:
616
+ version_name = version_config["name"]
617
+ if version_name not in service_config_to_version_config:
618
+ if existing_service is None:
619
+ raise ValueError(
620
+ f"--config-file corresponding to version {version_name!r} must be provided if the Service is not already deployed."
621
+ )
622
+ # TODO(doyoung): Perhaps add a validaiton logic to confirm if the version name already exists in the existing service through existing_service.versions.
623
+ service_config_to_version_config[version_name] = (
624
+ None,
625
+ version_config,
626
+ )
627
+
628
+ return service_config_to_version_config
629
+
630
+ def _get_rollout_strategy( # noqa: PLR0912
631
+ self,
632
+ existing_service: Optional[DecoratedProductionServiceV2APIModel],
633
+ version_weights: Optional[List[Dict[str, Any]]],
634
+ in_place: bool,
635
+ configs: List[ServiceConfig],
636
+ ) -> RolloutStrategy:
637
+ """Determine the rollout strategy to use.
638
+
639
+ If `in_place` is True, return RolloutStrategy.IN_PLACE.
640
+ If `version_weights` is None or if the service is going from 0 or 1 versions to 1 version, return RolloutStrategy.ROLLOUT.
641
+ Otherwise, return RolloutStrategy.MULTI_VERSION.
642
+ """
643
+
644
+ active_versions = len(existing_service.versions) if existing_service else 0
645
+
646
+ if in_place:
647
+ if version_weights is not None:
648
+ raise ValueError(
649
+ "In-place updates are not supported for multi-version services."
650
+ )
651
+
652
+ return RolloutStrategy.IN_PLACE
653
+ elif version_weights is None or (
654
+ len(version_weights) == 1 and active_versions <= 1
655
+ ):
656
+ if existing_service is None and len(configs) == 0:
657
+ raise ValueError(
658
+ "A config file must be provided when deploying a new service."
659
+ )
660
+ if version_weights is not None:
661
+ self.logger.warning(
662
+ "Defaulting to ROLLOUT strategy for single version service. Ignoring --versions."
663
+ )
664
+
665
+ return RolloutStrategy.ROLLOUT
666
+ else:
667
+ return RolloutStrategy.MULTI_VERSION
668
+
669
+ def deploy( # noqa: PLR0912, C901
670
+ self,
671
+ configs: Union[ServiceConfig, List[ServiceConfig]],
421
672
  *,
422
673
  in_place: bool = False,
423
674
  canary_percent: Optional[int] = None,
424
675
  max_surge_percent: Optional[int] = None,
676
+ versions: Optional[str] = None,
677
+ name: Optional[str] = None,
678
+ cloud: Optional[str] = None,
679
+ project: Optional[str] = None,
425
680
  ) -> str:
426
- if not isinstance(in_place, bool):
427
- raise TypeError("in_place must be a bool.")
681
+ """Deploy or update a service.
428
682
 
429
- if canary_percent is not None:
430
- if not isinstance(canary_percent, int):
431
- raise TypeError("canary_percent must be an int.")
432
- if canary_percent < 0 or canary_percent > 100:
433
- raise ValueError("canary_percent must be between 0 and 100.")
683
+ Args:
684
+ configs: Service configuration(s) to deploy. Can be a single ServiceConfig or a list of ServiceConfig objects for multi-version deployments.
685
+ versions: JSON string specifying version configurations for multi-version deployments.
686
+ name: Name of the service. If not provided, defaults to workspace cluster name.
687
+ cloud: Cloud provider to deploy to.
688
+ project: Project to deploy the service in.
434
689
 
435
- if max_surge_percent is not None:
436
- if not isinstance(max_surge_percent, int):
437
- raise TypeError("max_surge_percent must be an int.")
438
- if max_surge_percent < 0 or max_surge_percent > 100:
439
- raise ValueError("max_surge_percent must be between 0 and 100.")
690
+ Returns:
691
+ The service ID.
692
+ """
693
+ if versions is not None:
694
+ versions = self._validate_versions(versions) # type: ignore
440
695
 
441
- name = config.name or self._get_default_name()
442
- existing_service: Optional[DecoratedProductionServiceV2APIModel] = (
443
- self.client.get_service(
444
- name=name, cloud=config.cloud, project=config.project
696
+ if isinstance(configs, ServiceConfig):
697
+ if versions is not None:
698
+ raise ValueError(
699
+ "To deploy with --versions, a list of ServiceConfig must be provided."
700
+ )
701
+
702
+ if not isinstance(in_place, bool):
703
+ raise TypeError("in_place must be a bool.")
704
+
705
+ if canary_percent is not None:
706
+ if not isinstance(canary_percent, int):
707
+ raise TypeError("canary_percent must be an int.")
708
+ if canary_percent < 0 or canary_percent > 100:
709
+ raise ValueError("canary_percent must be between 0 and 100.")
710
+
711
+ if max_surge_percent is not None:
712
+ if not isinstance(max_surge_percent, int):
713
+ raise TypeError("max_surge_percent must be an int.")
714
+ if max_surge_percent < 0 or max_surge_percent > 100:
715
+ raise ValueError("max_surge_percent must be between 0 and 100.")
716
+
717
+ name = configs.name
718
+ cloud = configs.cloud
719
+ project = configs.project
720
+ configs = [configs]
721
+
722
+ elif isinstance(configs, List):
723
+ if versions is None:
724
+ raise ValueError(
725
+ "--versions must be provided when deploying with multiple config files."
726
+ )
727
+
728
+ if len(configs) == 0:
729
+ if name is None:
730
+ raise ValueError(
731
+ "When using --versions, a service name must be provided if no config files are given."
732
+ )
733
+ else:
734
+ name, cloud, project = self._read_and_validate_configs(configs)
735
+
736
+ else:
737
+ ValueError(
738
+ "Single object of ServiceConfig or list of ServiceConfig must be provided."
445
739
  )
740
+
741
+ if name is None:
742
+ name = self._get_default_name()
743
+
744
+ existing_service: Optional[DecoratedProductionServiceV2APIModel] = (
745
+ self.client.get_service(name=name, cloud=cloud, project=project)
446
746
  )
447
747
  if existing_service is None:
448
748
  self.logger.info(f"Starting new service '{name}'.")
@@ -451,37 +751,63 @@ class PrivateServiceSDK(WorkloadSDK):
451
751
  else:
452
752
  self.logger.info(f"Updating existing service '{name}'.")
453
753
 
454
- # passed canary_percent is ignored when creating or restarting a service
754
+ # passed canary_percent is ignored when creating or restarting a service or multiple versions are being deployed.
455
755
  is_new_or_restarting = (
456
756
  existing_service is None
457
757
  or existing_service.current_state == ServiceEventCurrentState.TERMINATED
458
758
  )
459
- if canary_percent is not None and is_new_or_restarting:
759
+ if canary_percent is not None and (
760
+ is_new_or_restarting or versions is not None
761
+ ):
460
762
  canary_percent = None
461
763
  self.logger.warning(
462
- "canary_percent is ignored when creating or restarting a service."
764
+ "canary_percent is ignored when creating or restarting a service or multiple versions are being deployed."
463
765
  )
464
766
 
465
- if in_place:
767
+ rollout_strategy = self._get_rollout_strategy(
768
+ existing_service, versions, in_place, configs # type: ignore
769
+ )
770
+
771
+ service: DecoratedProductionServiceV2APIModel
772
+ # TODO(doyoung): Support multi version deployment for in_place update.
773
+ if rollout_strategy == RolloutStrategy.IN_PLACE:
774
+ if versions is not None:
775
+ raise ValueError(
776
+ "versions cannot be specified when doing an in_place update."
777
+ )
778
+ assert len(configs) == 1
466
779
  model = self._build_apply_service_model_for_in_place_update(
467
780
  name,
468
- config,
781
+ configs[0],
469
782
  canary_percent=canary_percent,
470
783
  max_surge_percent=max_surge_percent,
471
784
  existing_service=existing_service,
472
785
  )
786
+ service = self.client.rollout_service(model)
787
+ elif rollout_strategy == RolloutStrategy.MULTI_VERSION:
788
+ model = self._build_apply_service_model_for_multi_version(
789
+ name,
790
+ cloud,
791
+ project,
792
+ configs,
793
+ existing_service=existing_service,
794
+ versions=versions, # type: ignore
795
+ )
796
+ service = self.client.rollout_service_multi_version(model)
473
797
  else:
798
+ # rolling out a single version in new service without specifying versions.
799
+ assert len(configs) == 1
474
800
  model = self._build_apply_service_model_for_rollout(
475
801
  name,
476
- config,
802
+ configs[0],
803
+ RolloutStrategy.ROLLOUT,
477
804
  canary_percent=canary_percent,
478
805
  max_surge_percent=max_surge_percent,
479
806
  existing_service=existing_service,
480
807
  )
808
+ service = self.client.rollout_service(model)
481
809
 
482
- service = self.client.rollout_service(model)
483
810
  self._log_deployed_service_info(service, canary_percent=canary_percent)
484
-
485
811
  return service.id
486
812
 
487
813
  def _resolve_to_service_model(
@@ -32,10 +32,14 @@ anyscale.service.deploy(
32
32
  """
33
33
 
34
34
  _DEPLOY_ARG_DOCSTRINGS = {
35
- "config": "The config options defining the service.",
35
+ "configs": "The config options defining the service.",
36
36
  "in_place": "Perform an in-place upgrade without starting a new cluster. This can be used for faster iteration during development but is *not* currently recommended for production deploys. This *cannot* be used to change cluster-level options such as image and compute config (they will be ignored).",
37
37
  "canary_percent": "The percentage of traffic to send to the canary version of the service (0-100). This can be used to manually shift traffic toward (or away from) the canary version. If not provided, traffic will be shifted incrementally toward the canary version until it reaches 100. Not supported when using --in-place. This is ignored when restarting a service or creating a new service.",
38
38
  "max_surge_percent": "Amount of excess capacity allowed to be used while updating the service (0-100). Defaults to 100. Not supported when using --in-place.",
39
+ "versions": "Enable multi-version deployment by providing a JSON array of objects or a JSON object in text format. Defines the version name, traffic and capacity percents per version. Capacity defaults to traffic.",
40
+ "name": "Name of the service. When running in a workspace, this defaults to the workspace name.",
41
+ "cloud": "The Anyscale Cloud of this workload. If not provided, the organization default will be used (or, if running in a workspace, the cloud of the workspace).",
42
+ "project": "Named project to use for the service. If not provided, the default project for the cloud will be used (or, if running in a workspace, the project of the workspace).",
39
43
  }
40
44
 
41
45
 
@@ -46,11 +50,15 @@ _DEPLOY_ARG_DOCSTRINGS = {
46
50
  arg_docstrings=_DEPLOY_ARG_DOCSTRINGS,
47
51
  )
48
52
  def deploy(
49
- config: ServiceConfig,
53
+ configs: Union[ServiceConfig, List[ServiceConfig]],
50
54
  *,
51
55
  in_place: bool = False,
52
56
  canary_percent: Optional[int] = None,
53
57
  max_surge_percent: Optional[int] = None,
58
+ versions: Optional[str] = None,
59
+ name: Optional[str] = None,
60
+ cloud: Optional[str] = None,
61
+ project: Optional[str] = None,
54
62
  _private_sdk: Optional[PrivateServiceSDK] = None,
55
63
  ) -> str:
56
64
  """Deploy a service.
@@ -62,10 +70,14 @@ def deploy(
62
70
  Returns the id of the deployed service.
63
71
  """
64
72
  return _private_sdk.deploy( # type: ignore
65
- config,
73
+ configs=configs,
66
74
  in_place=in_place,
67
75
  canary_percent=canary_percent,
68
76
  max_surge_percent=max_surge_percent,
77
+ versions=versions,
78
+ name=name,
79
+ cloud=cloud,
80
+ project=project,
69
81
  )
70
82
 
71
83
 
@@ -231,6 +231,18 @@ tracing_config: # (Optional) Configuration options for tracing.
231
231
  },
232
232
  )
233
233
 
234
+ version_name: Optional[str] = field(
235
+ default=None, metadata={"docstring": "Unique name of the version."},
236
+ )
237
+
238
+ def _validate_version_name(self, version_name: Optional[str]):
239
+ # Allow None if optional; enforce non-empty string if required
240
+ if version_name is None:
241
+ 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
+ return version_name
245
+
234
246
  def _validate_applications(self, applications: List[Dict[str, Any]]):
235
247
  if not isinstance(applications, list):
236
248
  raise TypeError("'applications' must be a list.")
@@ -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.0"
3
+ LATEST_RAY_VERSION = "2.50.1"
anyscale/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.26.68"
1
+ __version__ = "0.26.70"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anyscale
3
- Version: 0.26.68
3
+ Version: 0.26.70
4
4
  Summary: Command Line Interface for Anyscale
5
5
  Author: Anyscale Inc.
6
6
  License: AS License