anyscale 0.26.21__py3-none-any.whl → 0.26.23__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 (32) hide show
  1. anyscale/_private/anyscale_client/anyscale_client.py +103 -43
  2. anyscale/_private/anyscale_client/common.py +38 -8
  3. anyscale/_private/anyscale_client/fake_anyscale_client.py +98 -27
  4. anyscale/_private/docgen/models.md +2 -2
  5. anyscale/_private/models/model_base.py +95 -0
  6. anyscale/_private/workload/workload_sdk.py +3 -1
  7. anyscale/aggregated_instance_usage/models.py +4 -4
  8. anyscale/client/README.md +1 -0
  9. anyscale/client/openapi_client/api/default_api.py +122 -1
  10. anyscale/client/openapi_client/models/baseimagesenum.py +68 -1
  11. anyscale/client/openapi_client/models/supportedbaseimagesenum.py +68 -1
  12. anyscale/commands/command_examples.py +4 -0
  13. anyscale/commands/list_util.py +107 -0
  14. anyscale/commands/service_commands.py +267 -31
  15. anyscale/commands/util.py +5 -4
  16. anyscale/controllers/service_controller.py +7 -86
  17. anyscale/sdk/anyscale_client/models/baseimagesenum.py +68 -1
  18. anyscale/sdk/anyscale_client/models/supportedbaseimagesenum.py +68 -1
  19. anyscale/service/__init__.py +53 -3
  20. anyscale/service/_private/service_sdk.py +177 -41
  21. anyscale/service/commands.py +78 -1
  22. anyscale/service/models.py +65 -0
  23. anyscale/shared_anyscale_utils/latest_ray_version.py +1 -1
  24. anyscale/util.py +35 -1
  25. anyscale/version.py +1 -1
  26. {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/METADATA +1 -1
  27. {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/RECORD +32 -31
  28. {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/LICENSE +0 -0
  29. {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/NOTICE +0 -0
  30. {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/WHEEL +0 -0
  31. {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/entry_points.txt +0 -0
  32. {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/top_level.txt +0 -0
@@ -1,18 +1,30 @@
1
+ import asyncio
1
2
  import copy
2
- from typing import Any, Dict, Optional, Union
3
+ from typing import Any, Dict, List, Optional, Union
3
4
 
5
+ from anyscale._private.models.model_base import ResultIterator
4
6
  from anyscale._private.workload import WorkloadSDK
7
+ from anyscale.client.openapi_client.models.decorated_production_service_v2_api_model import (
8
+ DecoratedProductionServiceV2APIModel,
9
+ )
10
+ from anyscale.client.openapi_client.models.decoratedlistserviceapimodel_list_response import (
11
+ DecoratedlistserviceapimodelListResponse,
12
+ )
13
+ from anyscale.client.openapi_client.models.list_response_metadata import (
14
+ ListResponseMetadata,
15
+ )
5
16
  from anyscale.compute_config.models import ComputeConfig
6
17
  from anyscale.sdk.anyscale_client.models import (
7
18
  AccessConfig,
8
- ApplyServiceModel,
19
+ ApplyProductionServiceV2Model,
9
20
  GrpcProtocolConfig,
10
21
  ProductionServiceV2VersionModel,
11
22
  Protocols,
12
23
  RayGCSExternalStorageConfig as APIRayGCSExternalStorageConfig,
13
24
  ServiceConfig as ExternalAPIServiceConfig,
14
25
  ServiceEventCurrentState,
15
- ServiceModel,
26
+ ServiceSortField,
27
+ SortOrder,
16
28
  TracingConfig as APITracingConfg,
17
29
  )
18
30
  from anyscale.service.models import (
@@ -32,6 +44,9 @@ from anyscale.utils.workspace_notification import (
32
44
  )
33
45
 
34
46
 
47
+ MAX_PAGE_SIZE = 50
48
+
49
+
35
50
  class PrivateServiceSDK(WorkloadSDK):
36
51
  def _override_application_runtime_envs(
37
52
  self,
@@ -101,7 +116,10 @@ class PrivateServiceSDK(WorkloadSDK):
101
116
  return name
102
117
 
103
118
  def _log_deployed_service_info(
104
- self, service: ServiceModel, *, canary_percent: Optional[int]
119
+ self,
120
+ service: DecoratedProductionServiceV2APIModel,
121
+ *,
122
+ canary_percent: Optional[int],
105
123
  ):
106
124
  """Log user-facing information about a deployed service."""
107
125
  version_id_info = "version ID: {version_id}".format(
@@ -177,9 +195,9 @@ class PrivateServiceSDK(WorkloadSDK):
177
195
  *,
178
196
  canary_percent: Optional[int] = None,
179
197
  max_surge_percent: Optional[int] = None,
180
- existing_service: Optional[ServiceModel] = None,
181
- ) -> ApplyServiceModel:
182
- """Build the ApplyServiceModel for an in_place update.
198
+ existing_service: Optional[DecoratedProductionServiceV2APIModel] = None,
199
+ ) -> ApplyProductionServiceV2Model:
200
+ """Build the ApplyProductionServiceV2Model for an in_place update.
183
201
 
184
202
  in_place updates:
185
203
  - must be performed on an existing service.
@@ -246,7 +264,7 @@ class PrivateServiceSDK(WorkloadSDK):
246
264
  project_id = self.client.get_project_id(
247
265
  parent_cloud_id=cloud_id, name=config.project
248
266
  )
249
- return ApplyServiceModel(
267
+ return ApplyProductionServiceV2Model(
250
268
  name=name,
251
269
  project_id=project_id,
252
270
  ray_serve_config=self._build_ray_serve_config(config),
@@ -269,9 +287,9 @@ class PrivateServiceSDK(WorkloadSDK):
269
287
  *,
270
288
  canary_percent: Optional[int] = None,
271
289
  max_surge_percent: Optional[int] = None,
272
- existing_service: Optional[ServiceModel] = None,
273
- ) -> ApplyServiceModel:
274
- """Build the ApplyServiceModel for a rolling update."""
290
+ existing_service: Optional[DecoratedProductionServiceV2APIModel] = None,
291
+ ) -> ApplyProductionServiceV2Model:
292
+ """Build the ApplyProductionServiceV2Model for a rolling update."""
275
293
 
276
294
  build_id = None
277
295
  if config.containerfile is not None:
@@ -375,7 +393,7 @@ class PrivateServiceSDK(WorkloadSDK):
375
393
  if config.tracing_config.sampling_ratio is not None:
376
394
  tracing_config.sampling_ratio = config.tracing_config.sampling_ratio
377
395
 
378
- return ApplyServiceModel(
396
+ return ApplyProductionServiceV2Model(
379
397
  name=name,
380
398
  project_id=project_id,
381
399
  ray_serve_config=self._build_ray_serve_config(config),
@@ -417,7 +435,9 @@ class PrivateServiceSDK(WorkloadSDK):
417
435
  raise ValueError("max_surge_percent must be between 0 and 100.")
418
436
 
419
437
  name = config.name or self._get_default_name()
420
- existing_service: Optional[ServiceModel] = self.client.get_service(
438
+ existing_service: Optional[
439
+ DecoratedProductionServiceV2APIModel
440
+ ] = self.client.get_service(
421
441
  name=name, cloud=config.cloud, project=config.project
422
442
  )
423
443
  if existing_service is None:
@@ -444,7 +464,7 @@ class PrivateServiceSDK(WorkloadSDK):
444
464
  existing_service=existing_service,
445
465
  )
446
466
 
447
- service: ServiceModel = self.client.rollout_service(model)
467
+ service = self.client.rollout_service(model)
448
468
  self._log_deployed_service_info(service, canary_percent=canary_percent)
449
469
 
450
470
  return service.id
@@ -456,11 +476,11 @@ class PrivateServiceSDK(WorkloadSDK):
456
476
  cloud: Optional[str] = None,
457
477
  project: Optional[str] = None,
458
478
  include_archived: bool = False,
459
- ) -> ServiceModel:
479
+ ) -> DecoratedProductionServiceV2APIModel:
460
480
  if name is None:
461
481
  name = self._get_default_name()
462
482
 
463
- model: Optional[ServiceModel] = self.client.get_service(
483
+ model = self.client.get_service(
464
484
  name=name, cloud=cloud, project=project, include_archived=include_archived
465
485
  )
466
486
  if model is None:
@@ -539,7 +559,7 @@ class PrivateServiceSDK(WorkloadSDK):
539
559
  # in the DB to avoid breakages, but for now I'm copying the existing logic.
540
560
  return model.id[-SERVICE_VERSION_ID_TRUNCATED_LEN:]
541
561
 
542
- def _service_version_model_to_status(
562
+ async def _service_version_model_to_status_async(
543
563
  self,
544
564
  model: ProductionServiceV2VersionModel,
545
565
  *,
@@ -547,10 +567,20 @@ class PrivateServiceSDK(WorkloadSDK):
547
567
  project_id: str,
548
568
  query_auth_token_enabled: bool,
549
569
  ) -> ServiceVersionStatus:
550
- image_uri = self._image_sdk.get_image_uri_from_build_id(model.build_id)
570
+
571
+ image_uri, image_build, project, compute_config = await asyncio.gather(
572
+ asyncio.to_thread(
573
+ self._image_sdk.get_image_uri_from_build_id, model.build_id
574
+ ),
575
+ asyncio.to_thread(self._image_sdk.get_image_build, model.build_id),
576
+ asyncio.to_thread(self.client.get_project, project_id),
577
+ asyncio.to_thread(
578
+ self.get_user_facing_compute_config, model.compute_config_id
579
+ ),
580
+ )
581
+
551
582
  if image_uri is None:
552
583
  raise RuntimeError(f"Failed to get image URI for ID {model.build_id}.")
553
- image_build = self._image_sdk.get_image_build(model.build_id)
554
584
  if image_build is None:
555
585
  raise RuntimeError(f"Failed to get image build for ID {model.build_id}.")
556
586
 
@@ -562,8 +592,6 @@ class PrivateServiceSDK(WorkloadSDK):
562
592
  certificate_path=model.ray_gcs_external_storage_config.redis_certificate_path,
563
593
  )
564
594
 
565
- project = self.client.get_project(project_id)
566
- compute_config = self.get_user_facing_compute_config(model.compute_config_id)
567
595
  tracing_config = None
568
596
  if model.tracing_config is not None:
569
597
  tracing_config = TracingConfig(
@@ -574,6 +602,7 @@ class PrivateServiceSDK(WorkloadSDK):
574
602
 
575
603
  return ServiceVersionStatus(
576
604
  id=self._get_user_facing_service_version_id(model),
605
+ created_at=model.created_at,
577
606
  state=model.current_state,
578
607
  # NOTE(edoakes): there is also a "current_weight" field but it does not match the UI.
579
608
  weight=model.weight,
@@ -615,50 +644,137 @@ class PrivateServiceSDK(WorkloadSDK):
615
644
 
616
645
  return state
617
646
 
618
- def _service_model_to_status(self, model: ServiceModel) -> ServiceStatus:
619
- # TODO(edoakes): for some reason the primary_version is populated
620
- # when the service is terminated. This should be fixed in the backend.
621
- is_terminated = model.current_state == ServiceEventCurrentState.TERMINATED
622
-
647
+ async def _service_model_to_status_async(
648
+ self, model: DecoratedProductionServiceV2APIModel
649
+ ) -> ServiceStatus:
623
650
  # TODO(edoakes): this is currently only exposed at the service level in the API,
624
651
  # which means that the per-version `query_auth_token_enabled` field will lie if
625
652
  # it's changed.
626
653
  query_auth_token_enabled = model.auth_token is not None
627
654
 
628
- primary_version = None
629
- if not is_terminated and model.primary_version is not None:
630
- primary_version = self._service_version_model_to_status(
631
- model.primary_version,
632
- service_name=model.name,
633
- project_id=model.project_id,
634
- query_auth_token_enabled=query_auth_token_enabled,
655
+ primary_version_task = None
656
+ if model.primary_version is not None:
657
+ primary_version_task = asyncio.create_task(
658
+ self._service_version_model_to_status_async(
659
+ model.primary_version,
660
+ service_name=model.name,
661
+ project_id=model.project_id,
662
+ query_auth_token_enabled=query_auth_token_enabled,
663
+ )
635
664
  )
636
665
 
637
- canary_version = None
638
- if not is_terminated and model.canary_version is not None:
639
- canary_version = self._service_version_model_to_status(
640
- model.canary_version,
641
- service_name=model.name,
642
- project_id=model.project_id,
643
- query_auth_token_enabled=query_auth_token_enabled,
666
+ canary_version_task = None
667
+ if model.canary_version is not None:
668
+ canary_version_task = asyncio.create_task(
669
+ self._service_version_model_to_status_async(
670
+ model.canary_version,
671
+ service_name=model.name,
672
+ project_id=model.project_id,
673
+ query_auth_token_enabled=query_auth_token_enabled,
674
+ )
644
675
  )
645
676
 
677
+ primary_version = await primary_version_task if primary_version_task else None
678
+ canary_version = await canary_version_task if canary_version_task else None
679
+
680
+ project_name = None
681
+ if primary_version and isinstance(primary_version.config, ServiceConfig):
682
+ project_name = primary_version.config.project
683
+
646
684
  return ServiceStatus(
647
685
  id=model.id,
648
686
  name=model.name,
687
+ creator=model.creator.email if model.creator else None,
649
688
  state=self._model_state_to_state(model.current_state),
650
689
  query_url=model.base_url,
651
690
  query_auth_token=model.auth_token,
652
691
  primary_version=primary_version,
653
692
  canary_version=canary_version,
693
+ project=project_name,
654
694
  )
655
695
 
656
696
  def status(
657
697
  self, name: str, *, cloud: Optional[str] = None, project: Optional[str] = None
658
698
  ) -> ServiceStatus:
659
699
  model = self._resolve_to_service_model(name=name, cloud=cloud, project=project)
700
+ return asyncio.run(self._service_model_to_status_async(model))
660
701
 
661
- return self._service_model_to_status(model)
702
+ def list( # noqa: PLR0912
703
+ self,
704
+ *,
705
+ # Single-item lookup
706
+ service_id: Optional[str] = None,
707
+ # Filters
708
+ name: Optional[str] = None,
709
+ state_filter: Optional[Union[List[ServiceState], List[str]]] = None,
710
+ creator_id: Optional[str] = None,
711
+ cloud: Optional[str] = None,
712
+ project: Optional[str] = None,
713
+ include_archived: bool = False,
714
+ # Paging
715
+ max_items: Optional[int] = None, # Controls total items yielded by iterator
716
+ page_size: Optional[int] = None, # Controls items fetched per API call
717
+ # Sorting
718
+ sort_field: Optional[Union[str, ServiceSortField]] = None,
719
+ sort_order: Optional[Union[str, SortOrder]] = None,
720
+ ) -> ResultIterator[ServiceStatus]:
721
+
722
+ if page_size is not None and (page_size <= 0 or page_size > MAX_PAGE_SIZE):
723
+ raise ValueError(
724
+ f"page_size must be between 1 and {MAX_PAGE_SIZE}, inclusive."
725
+ )
726
+
727
+ if service_id is not None:
728
+ raw = self.client.get_service_by_id(service_id)
729
+
730
+ def _fetch_single_page(
731
+ _token: Optional[str],
732
+ ) -> DecoratedlistserviceapimodelListResponse:
733
+ # Only return data on the first call (token=None), simulate single-item page
734
+ if _token is None and raw is not None:
735
+ results = [raw]
736
+ metadata = ListResponseMetadata(total=1, next_paging_token=None)
737
+ else:
738
+ results = []
739
+ metadata = ListResponseMetadata(total=0, next_paging_token=None)
740
+
741
+ return DecoratedlistserviceapimodelListResponse(
742
+ results=results, metadata=metadata,
743
+ )
744
+
745
+ return ResultIterator(
746
+ page_token=None,
747
+ max_items=1,
748
+ fetch_page=_fetch_single_page,
749
+ async_parse_fn=self._service_model_to_status_async,
750
+ parse_fn=None,
751
+ )
752
+
753
+ normalised_states = _normalize_state_filter(state_filter)
754
+
755
+ def _fetch_page(
756
+ token: Optional[str],
757
+ ) -> DecoratedlistserviceapimodelListResponse:
758
+ return self.client.list_services(
759
+ name=name,
760
+ state_filter=normalised_states,
761
+ creator_id=creator_id,
762
+ cloud=cloud,
763
+ project=project,
764
+ include_archived=include_archived,
765
+ count=page_size,
766
+ paging_token=token,
767
+ sort_field=sort_field,
768
+ sort_order=sort_order,
769
+ )
770
+
771
+ return ResultIterator(
772
+ page_token=None,
773
+ max_items=max_items,
774
+ fetch_page=_fetch_page,
775
+ async_parse_fn=self._service_model_to_status_async,
776
+ parse_fn=None,
777
+ )
662
778
 
663
779
  def wait(
664
780
  self,
@@ -738,3 +854,23 @@ class PrivateServiceSDK(WorkloadSDK):
738
854
  return self.client.controller_logs_for_service_version(
739
855
  model.primary_version, head, max_lines
740
856
  )
857
+
858
+
859
+ def _normalize_state_filter(
860
+ states: Optional[Union[List[ServiceState], List[str]]]
861
+ ) -> Optional[List[str]]:
862
+ if states is None:
863
+ return None
864
+
865
+ normalized: List[str] = []
866
+ for s in states:
867
+ if isinstance(s, ServiceState):
868
+ normalized.append(s.value)
869
+ elif isinstance(s, str):
870
+ normalized.append(s.upper())
871
+ else:
872
+ raise TypeError(
873
+ "'state_filter' entries must be ServiceState or str, "
874
+ f"got {type(s).__name__}"
875
+ )
876
+ return normalized
@@ -1,12 +1,15 @@
1
- from typing import Optional, Union
1
+ from typing import List, Optional, Union
2
2
 
3
+ from anyscale._private.models.model_base import ResultIterator
3
4
  from anyscale._private.sdk import sdk_command
4
5
  from anyscale.service._private.service_sdk import PrivateServiceSDK
5
6
  from anyscale.service.models import (
6
7
  ServiceConfig,
7
8
  ServiceLogMode,
9
+ ServiceSortField,
8
10
  ServiceState,
9
11
  ServiceStatus,
12
+ SortOrder,
10
13
  )
11
14
 
12
15
 
@@ -333,3 +336,77 @@ def _controller_logs(
333
336
  mode=mode,
334
337
  max_lines=max_lines,
335
338
  )
339
+
340
+
341
+ _LIST_EXAMPLE = """
342
+ import anyscale
343
+ from anyscale.service.models import ServiceState
344
+
345
+ # Example: Get the first 50 running services
346
+ for svc in anyscale.service.list(max_items=50, state_filter=[ServiceState.RUNNING]):
347
+ print(svc.name)
348
+ """
349
+
350
+ _LIST_ARG_DOCSTRINGS = {
351
+ "service_id": (
352
+ "If provided, returns just the service with this ID "
353
+ "wrapped in a one-page iterator."
354
+ ),
355
+ "name": "Substring to match against the service name.",
356
+ "state_filter": (
357
+ "List of states to include. "
358
+ "May be `ServiceState` enums or case-insensitive strings."
359
+ ),
360
+ "creator_id": "Filter services by user ID.",
361
+ "cloud": "Name of the Anyscale Cloud to search in.",
362
+ "project": "Name of the Anyscale Project to search in.",
363
+ "include_archived": "Include archived services (default: False).",
364
+ # Paging
365
+ "max_items": "Maximum **total** number of items to yield (default: iterate all).",
366
+ "page_size": "Number of items to fetch per API request (default: API default).",
367
+ # Sorting
368
+ "sort_field": "Field to sort by (`NAME`, `STATUS`, `CREATED_AT`).",
369
+ "sort_order": "Sort direction (`ASC` or `DESC`).",
370
+ }
371
+
372
+ # Public command
373
+ @sdk_command(
374
+ _SERVICE_SDK_SINGLETON_KEY,
375
+ PrivateServiceSDK,
376
+ doc_py_example=_LIST_EXAMPLE,
377
+ arg_docstrings=_LIST_ARG_DOCSTRINGS,
378
+ )
379
+ def list( # noqa: A001
380
+ *,
381
+ # Single-item lookup
382
+ service_id: Optional[str] = None,
383
+ # Filters
384
+ name: Optional[str] = None,
385
+ state_filter: Optional[Union[List[ServiceState], List[str]]] = None,
386
+ creator_id: Optional[str] = None,
387
+ cloud: Optional[str] = None,
388
+ project: Optional[str] = None,
389
+ include_archived: bool = False,
390
+ # Paging
391
+ max_items: Optional[int] = None,
392
+ page_size: Optional[int] = None,
393
+ # Sorting
394
+ sort_field: Optional[Union[str, ServiceSortField]] = None,
395
+ sort_order: Optional[Union[str, SortOrder]] = None,
396
+ # Injected SDK
397
+ _private_sdk: Optional[PrivateServiceSDK] = None,
398
+ ) -> ResultIterator[ServiceStatus]:
399
+ """List services or fetch a single service by ID."""
400
+ return _private_sdk.list( # type: ignore
401
+ service_id=service_id,
402
+ name=name,
403
+ state_filter=state_filter,
404
+ creator_id=creator_id,
405
+ cloud=cloud,
406
+ project=project,
407
+ include_archived=include_archived,
408
+ max_items=max_items,
409
+ page_size=page_size,
410
+ sort_field=sort_field,
411
+ sort_order=sort_order,
412
+ )
@@ -1,4 +1,5 @@
1
1
  from dataclasses import dataclass, field
2
+ from datetime import datetime
2
3
  from typing import Any, Dict, List, Optional, Union
3
4
 
4
5
  from anyscale._private.models import ModelBase, ModelEnum
@@ -488,6 +489,7 @@ primary_version:
488
489
  id: 601bd56c4b
489
490
  state: RUNNING
490
491
  weight: 100
492
+ created_at: 2025-04-18 17:21:28.323174+00:00
491
493
  """
492
494
 
493
495
  id: str = field(
@@ -525,6 +527,14 @@ primary_version:
525
527
  if not isinstance(weight, int):
526
528
  raise TypeError("'weight' must be an int.")
527
529
 
530
+ created_at: datetime = field(
531
+ metadata={"docstring": "Creation time of the service version."},
532
+ )
533
+
534
+ def _validate_created_at(self, created_at: datetime):
535
+ if not isinstance(created_at, datetime):
536
+ raise TypeError("created_at must be a datetime.")
537
+
528
538
  config: Union[ServiceConfig, Dict] = field(
529
539
  repr=False, metadata={"docstring": "Configuration of this service version."}
530
540
  )
@@ -598,6 +608,15 @@ primary_version:
598
608
  if not isinstance(query_url, str):
599
609
  raise TypeError("'query_url' must be a string.")
600
610
 
611
+ creator: Optional[str] = field(
612
+ default=None,
613
+ metadata={"docstring": "Email of the user or entity that created the service."},
614
+ )
615
+
616
+ def _validate_creator(self, creator: Optional[str]):
617
+ if creator is not None and not isinstance(creator, str):
618
+ raise TypeError("'creator' must be a string.")
619
+
601
620
  query_auth_token: Optional[str] = field(
602
621
  default=None,
603
622
  repr=False,
@@ -658,6 +677,31 @@ primary_version:
658
677
 
659
678
  return canary_version
660
679
 
680
+ project: Optional[str] = field(
681
+ default=None,
682
+ metadata={
683
+ "docstring": "Name of the project that this service version belongs to."
684
+ },
685
+ )
686
+
687
+ def _validate_project(self, project: Optional[str]):
688
+ if project is not None and not isinstance(project, str):
689
+ raise TypeError("project must be a string.")
690
+
691
+
692
+ class ServiceSortField(ModelEnum):
693
+ """Fields available for sorting services."""
694
+
695
+ STATUS = "STATUS"
696
+ NAME = "NAME"
697
+ CREATED_AT = "CREATED_AT"
698
+
699
+ __docstrings__ = {
700
+ STATUS: "Sort by service status (active first by default).",
701
+ NAME: "Sort by service name.",
702
+ CREATED_AT: "Sort by creation timestamp.",
703
+ }
704
+
661
705
 
662
706
  class ServiceLogMode(ModelEnum):
663
707
  """Mode to use for getting job logs."""
@@ -669,3 +713,24 @@ class ServiceLogMode(ModelEnum):
669
713
  HEAD: "Fetch logs from the start.",
670
714
  TAIL: "Fetch logs from the end.",
671
715
  }
716
+
717
+
718
+ @dataclass(frozen=True)
719
+ class ServiceModel(ModelBase):
720
+ """A model for a service."""
721
+
722
+ id: str = field(metadata={"docstring": "Unique ID of the service."})
723
+ name: str = field(metadata={"docstring": "Name of the service."})
724
+ state: ServiceState = field(metadata={"docstring": "Current state of the service."})
725
+
726
+
727
+ class SortOrder(ModelEnum):
728
+ """Enum for sort order directions."""
729
+
730
+ ASC = "ASC"
731
+ DESC = "DESC"
732
+
733
+ __docstrings__ = {
734
+ ASC: "Sort in ascending order.",
735
+ DESC: "Sort in descending order.",
736
+ }
@@ -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.45.0"
3
+ LATEST_RAY_VERSION = "2.46.0"
anyscale/util.py CHANGED
@@ -37,7 +37,10 @@ from anyscale.authenticate import get_auth_api_client
37
37
  from anyscale.aws_iam_policies import ANYSCALE_IAM_POLICIES, AnyscaleIAMPolicy
38
38
  from anyscale.cli_logger import BlockLogger, CloudSetupLogger
39
39
  from anyscale.client.openapi_client.api.default_api import DefaultApi as ProductApi
40
- from anyscale.client.openapi_client.models import AWSMemoryDBClusterConfig
40
+ from anyscale.client.openapi_client.models import (
41
+ AWSMemoryDBClusterConfig,
42
+ ServiceEventCurrentState,
43
+ )
41
44
  from anyscale.client.openapi_client.models.cloud_analytics_event_cloud_resource import (
42
45
  CloudAnalyticsEventCloudResource,
43
46
  )
@@ -127,6 +130,15 @@ MEMORY_DB_RESOURCE = """ MemoryDBSubnetGroup:
127
130
  GCP_DEPLOYMENT_MANAGER_TIMEOUT_SECONDS_LONG = 600 # 10 minutes
128
131
 
129
132
 
133
+ class AnyscaleJSONEncoder(json.JSONEncoder):
134
+ """Custom JSON encoder for Anyscale models, handling datetime objects."""
135
+
136
+ def default(self, obj):
137
+ if isinstance(obj, datetime.datetime):
138
+ return obj.isoformat()
139
+ return json.JSONEncoder.default(self, obj)
140
+
141
+
130
142
  def confirm(msg: str, yes: bool) -> Optional[bool]:
131
143
  return None if yes else click.confirm(msg, abort=True)
132
144
 
@@ -623,6 +635,28 @@ def validate_non_negative_arg(ctx, param, value): # noqa: ARG001
623
635
  return value
624
636
 
625
637
 
638
+ def validate_service_state_filter(
639
+ ctx, param, value: Tuple[str, ...] # noqa: ARG001
640
+ ) -> List[str]:
641
+ """Validate ServiceEventCurrentState values."""
642
+ if not value:
643
+ return []
644
+
645
+ allowable_values_upper = {
646
+ s.upper() for s in ServiceEventCurrentState.allowable_values
647
+ }
648
+ allowed_values_str = ", ".join(ServiceEventCurrentState.allowable_values)
649
+
650
+ for state_str in value:
651
+ state_upper = state_str.upper()
652
+ if state_upper not in allowable_values_upper:
653
+ raise click.ClickException(
654
+ f"'{state_str}' is not a valid value for {param.opts[0]}. Allowed values: {allowed_values_str}"
655
+ )
656
+
657
+ return [s.upper() for s in value]
658
+
659
+
626
660
  def _update_external_ids_for_policy(
627
661
  original_policy: Dict[str, Any], new_external_id: str
628
662
  ):
anyscale/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.26.21"
1
+ __version__ = "0.26.23"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: anyscale
3
- Version: 0.26.21
3
+ Version: 0.26.23
4
4
  Summary: Command Line Interface for Anyscale
5
5
  Author: Anyscale Inc.
6
6
  License: AS License