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.
- anyscale/_private/anyscale_client/anyscale_client.py +103 -43
- anyscale/_private/anyscale_client/common.py +38 -8
- anyscale/_private/anyscale_client/fake_anyscale_client.py +98 -27
- anyscale/_private/docgen/models.md +2 -2
- anyscale/_private/models/model_base.py +95 -0
- anyscale/_private/workload/workload_sdk.py +3 -1
- anyscale/aggregated_instance_usage/models.py +4 -4
- anyscale/client/README.md +1 -0
- anyscale/client/openapi_client/api/default_api.py +122 -1
- anyscale/client/openapi_client/models/baseimagesenum.py +68 -1
- anyscale/client/openapi_client/models/supportedbaseimagesenum.py +68 -1
- anyscale/commands/command_examples.py +4 -0
- anyscale/commands/list_util.py +107 -0
- anyscale/commands/service_commands.py +267 -31
- anyscale/commands/util.py +5 -4
- anyscale/controllers/service_controller.py +7 -86
- anyscale/sdk/anyscale_client/models/baseimagesenum.py +68 -1
- anyscale/sdk/anyscale_client/models/supportedbaseimagesenum.py +68 -1
- anyscale/service/__init__.py +53 -3
- anyscale/service/_private/service_sdk.py +177 -41
- anyscale/service/commands.py +78 -1
- anyscale/service/models.py +65 -0
- anyscale/shared_anyscale_utils/latest_ray_version.py +1 -1
- anyscale/util.py +35 -1
- anyscale/version.py +1 -1
- {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/METADATA +1 -1
- {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/RECORD +32 -31
- {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/LICENSE +0 -0
- {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/NOTICE +0 -0
- {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/WHEEL +0 -0
- {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
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
|
-
|
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,
|
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[
|
181
|
-
) ->
|
182
|
-
"""Build the
|
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
|
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[
|
273
|
-
) ->
|
274
|
-
"""Build the
|
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
|
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[
|
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
|
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
|
-
) ->
|
479
|
+
) -> DecoratedProductionServiceV2APIModel:
|
460
480
|
if name is None:
|
461
481
|
name = self._get_default_name()
|
462
482
|
|
463
|
-
model
|
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
|
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
|
-
|
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
|
619
|
-
|
620
|
-
|
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
|
-
|
629
|
-
if
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
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
|
-
|
638
|
-
if
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
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
|
-
|
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
|
anyscale/service/commands.py
CHANGED
@@ -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
|
+
)
|
anyscale/service/models.py
CHANGED
@@ -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
|
+
}
|
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
|
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.
|
1
|
+
__version__ = "0.26.23"
|