anyscale 0.26.21__py3-none-any.whl → 0.26.22__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 +37 -7
- anyscale/_private/anyscale_client/fake_anyscale_client.py +98 -27
- 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/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/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/util.py +35 -1
- anyscale/version.py +1 -1
- {anyscale-0.26.21.dist-info → anyscale-0.26.22.dist-info}/METADATA +1 -1
- {anyscale-0.26.21.dist-info → anyscale-0.26.22.dist-info}/RECORD +26 -25
- {anyscale-0.26.21.dist-info → anyscale-0.26.22.dist-info}/LICENSE +0 -0
- {anyscale-0.26.21.dist-info → anyscale-0.26.22.dist-info}/NOTICE +0 -0
- {anyscale-0.26.21.dist-info → anyscale-0.26.22.dist-info}/WHEEL +0 -0
- {anyscale-0.26.21.dist-info → anyscale-0.26.22.dist-info}/entry_points.txt +0 -0
- {anyscale-0.26.21.dist-info → anyscale-0.26.22.dist-info}/top_level.txt +0 -0
anyscale/commands/util.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
from copy import deepcopy
|
2
|
-
from typing import Dict, Tuple
|
2
|
+
from typing import Dict, Tuple, TypeVar
|
3
3
|
|
4
4
|
import click
|
5
5
|
|
@@ -136,9 +136,10 @@ def convert_kv_strings_to_dict(strings: Tuple[str]) -> Dict[str, str]:
|
|
136
136
|
return ret_dict
|
137
137
|
|
138
138
|
|
139
|
-
|
140
|
-
|
141
|
-
|
139
|
+
T = TypeVar("T", bound=WorkloadConfig)
|
140
|
+
|
141
|
+
|
142
|
+
def override_env_vars(config: T, overrides: Dict[str, str]) -> T:
|
142
143
|
"""Returns a new copy of the WorkloadConfig with env vars overridden.
|
143
144
|
|
144
145
|
This is a per-key override, so keys already in the config that are not specified in
|
@@ -2,7 +2,6 @@ import os
|
|
2
2
|
from typing import Any, Dict, List, Optional
|
3
3
|
|
4
4
|
import click
|
5
|
-
import tabulate
|
6
5
|
import yaml
|
7
6
|
|
8
7
|
from anyscale import AnyscaleSDK
|
@@ -14,9 +13,6 @@ from anyscale.client.openapi_client.api.default_api import DefaultApi as Interna
|
|
14
13
|
from anyscale.client.openapi_client.models.decorated_list_service_api_model import (
|
15
14
|
DecoratedListServiceAPIModel,
|
16
15
|
)
|
17
|
-
from anyscale.client.openapi_client.models.decorated_production_service_v2_api_model import (
|
18
|
-
DecoratedProductionServiceV2APIModel,
|
19
|
-
)
|
20
16
|
from anyscale.controllers.base_controller import BaseController
|
21
17
|
from anyscale.models.service_model import ServiceConfig
|
22
18
|
from anyscale.project_utils import infer_project_id
|
@@ -81,6 +77,12 @@ class ServiceController(BaseController):
|
|
81
77
|
max_surge_percent=config.max_surge_percent,
|
82
78
|
)
|
83
79
|
|
80
|
+
def get_authenticated_user_id(self) -> str:
|
81
|
+
user_info_response = (
|
82
|
+
self.internal_api_client.get_user_info_api_v2_userinfo_get()
|
83
|
+
)
|
84
|
+
return user_info_response.result.id
|
85
|
+
|
84
86
|
def _get_services_by_name(
|
85
87
|
self,
|
86
88
|
*,
|
@@ -93,12 +95,7 @@ class ServiceController(BaseController):
|
|
93
95
|
|
94
96
|
Note that "name" is an *exact match* (different from the REST API semantics).
|
95
97
|
"""
|
96
|
-
creator_id = None
|
97
|
-
if created_by_me:
|
98
|
-
user_info_response = (
|
99
|
-
self.internal_api_client.get_user_info_api_v2_userinfo_get()
|
100
|
-
)
|
101
|
-
creator_id = user_info_response.result.id
|
98
|
+
creator_id = self.get_authenticated_user_id() if created_by_me else None
|
102
99
|
|
103
100
|
paging_token = None
|
104
101
|
services_list: List[DecoratedListServiceAPIModel] = []
|
@@ -364,75 +361,6 @@ class ServiceController(BaseController):
|
|
364
361
|
log=self.log,
|
365
362
|
)
|
366
363
|
|
367
|
-
def list(
|
368
|
-
self,
|
369
|
-
*,
|
370
|
-
name: Optional[str] = None,
|
371
|
-
service_id: Optional[str] = None,
|
372
|
-
project_id: Optional[str] = None,
|
373
|
-
created_by_me: bool = False,
|
374
|
-
max_items: int = 10,
|
375
|
-
) -> None:
|
376
|
-
self._list_via_service_list_endpoint(
|
377
|
-
name=name,
|
378
|
-
service_id=service_id,
|
379
|
-
project_id=project_id,
|
380
|
-
created_by_me=created_by_me,
|
381
|
-
max_items=max_items,
|
382
|
-
)
|
383
|
-
|
384
|
-
def _list_via_service_list_endpoint(
|
385
|
-
self,
|
386
|
-
*,
|
387
|
-
name: Optional[str] = None,
|
388
|
-
service_id: Optional[str] = None,
|
389
|
-
project_id: Optional[str] = None,
|
390
|
-
created_by_me: bool = False,
|
391
|
-
max_items: int,
|
392
|
-
):
|
393
|
-
services = []
|
394
|
-
if service_id:
|
395
|
-
service_v2_result: DecoratedProductionServiceV2APIModel = (
|
396
|
-
self.internal_api_client.get_service_api_v2_services_v2_service_id_get(
|
397
|
-
service_id
|
398
|
-
).result
|
399
|
-
)
|
400
|
-
services.append(
|
401
|
-
[
|
402
|
-
service_v2_result.name,
|
403
|
-
service_v2_result.id,
|
404
|
-
service_v2_result.current_state,
|
405
|
-
# TODO: change to email once https://github.com/anyscale/product/pull/18189 is merged
|
406
|
-
service_v2_result.creator_id,
|
407
|
-
]
|
408
|
-
)
|
409
|
-
|
410
|
-
else:
|
411
|
-
services_data = self._get_services_by_name(
|
412
|
-
name=name,
|
413
|
-
project_id=project_id,
|
414
|
-
created_by_me=created_by_me,
|
415
|
-
max_items=max_items,
|
416
|
-
)
|
417
|
-
|
418
|
-
services = [
|
419
|
-
[
|
420
|
-
service.name,
|
421
|
-
service.id,
|
422
|
-
service.current_state,
|
423
|
-
service.creator.email,
|
424
|
-
]
|
425
|
-
for service in services_data
|
426
|
-
]
|
427
|
-
|
428
|
-
table = tabulate.tabulate(
|
429
|
-
services,
|
430
|
-
headers=["NAME", "ID", "CURRENT STATE", "CREATOR"],
|
431
|
-
tablefmt="plain",
|
432
|
-
)
|
433
|
-
self.log.info(f'View your Services in the UI at {get_endpoint("/services")}')
|
434
|
-
self.log.info(f"Services:\n{table}")
|
435
|
-
|
436
364
|
def rollback(self, service_id: str, max_surge_percent: Optional[int] = None):
|
437
365
|
service: ServiceModel = self.sdk.rollback_service(
|
438
366
|
service_id,
|
@@ -445,10 +373,3 @@ class ServiceController(BaseController):
|
|
445
373
|
self.log.info(
|
446
374
|
f'View the service in the UI at {get_endpoint(f"/services/{service.id}")}'
|
447
375
|
)
|
448
|
-
|
449
|
-
def terminate(self, service_id: str):
|
450
|
-
service: ServiceModel = self.sdk.terminate_service(service_id).result
|
451
|
-
self.log.info(f"Service {service.id} terminate initiated.")
|
452
|
-
self.log.info(
|
453
|
-
f'View the service in the UI at {get_endpoint(f"/services/{service.id}")}'
|
454
|
-
)
|
anyscale/service/__init__.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
from typing import Optional, Union
|
1
|
+
from typing import List, Optional, Union
|
2
2
|
|
3
3
|
from anyscale._private.anyscale_client import AnyscaleClientInterface
|
4
|
+
from anyscale._private.models.model_base import ResultIterator
|
4
5
|
from anyscale._private.sdk import sdk_docs
|
5
6
|
from anyscale._private.sdk.base_sdk import Timer
|
6
7
|
from anyscale.cli_logger import BlockLogger
|
@@ -15,6 +16,8 @@ from anyscale.service.commands import (
|
|
15
16
|
_DELETE_EXAMPLE,
|
16
17
|
_DEPLOY_ARG_DOCSTRINGS,
|
17
18
|
_DEPLOY_EXAMPLE,
|
19
|
+
_LIST_ARG_DOCSTRINGS,
|
20
|
+
_LIST_EXAMPLE,
|
18
21
|
_ROLLBACK_ARG_DOCSTRINGS,
|
19
22
|
_ROLLBACK_EXAMPLE,
|
20
23
|
_STATUS_ARG_DOCSTRINGS,
|
@@ -26,6 +29,7 @@ from anyscale.service.commands import (
|
|
26
29
|
archive,
|
27
30
|
delete,
|
28
31
|
deploy,
|
32
|
+
list,
|
29
33
|
rollback,
|
30
34
|
status,
|
31
35
|
terminate,
|
@@ -34,8 +38,10 @@ from anyscale.service.commands import (
|
|
34
38
|
from anyscale.service.models import (
|
35
39
|
ServiceConfig,
|
36
40
|
ServiceLogMode,
|
41
|
+
ServiceSortField,
|
37
42
|
ServiceState,
|
38
43
|
ServiceStatus,
|
44
|
+
SortOrder,
|
39
45
|
)
|
40
46
|
|
41
47
|
|
@@ -122,6 +128,7 @@ class ServiceSDK:
|
|
122
128
|
)
|
123
129
|
def archive( # noqa: F811
|
124
130
|
self,
|
131
|
+
id: Optional[str] = None, # noqa: A002
|
125
132
|
name: Optional[str] = None,
|
126
133
|
*,
|
127
134
|
cloud: Optional[str] = None,
|
@@ -133,13 +140,14 @@ class ServiceSDK:
|
|
133
140
|
|
134
141
|
Returns the ID of the archived service.
|
135
142
|
"""
|
136
|
-
return self._private_sdk.archive(name=name, cloud=cloud, project=project)
|
143
|
+
return self._private_sdk.archive(id=id, name=name, cloud=cloud, project=project)
|
137
144
|
|
138
145
|
@sdk_docs(
|
139
146
|
doc_py_example=_DELETE_EXAMPLE, arg_docstrings=_DELETE_ARG_DOCSTRINGS,
|
140
147
|
)
|
141
148
|
def delete( # noqa: F811
|
142
149
|
self,
|
150
|
+
id: Optional[str] = None, # noqa: A002
|
143
151
|
name: Optional[str] = None,
|
144
152
|
*,
|
145
153
|
cloud: Optional[str] = None,
|
@@ -149,7 +157,49 @@ class ServiceSDK:
|
|
149
157
|
|
150
158
|
This command is asynchronous, so it always returns immediately.
|
151
159
|
"""
|
152
|
-
return self._private_sdk.delete(name=name, cloud=cloud, project=project)
|
160
|
+
return self._private_sdk.delete(id=id, name=name, cloud=cloud, project=project)
|
161
|
+
|
162
|
+
def list( # noqa: F811, A001
|
163
|
+
self,
|
164
|
+
*,
|
165
|
+
# Single-item lookup
|
166
|
+
service_id: Optional[str] = None,
|
167
|
+
# Filters
|
168
|
+
name: Optional[str] = None,
|
169
|
+
state_filter: Optional[Union[List[ServiceState], List[str]]] = None,
|
170
|
+
creator_id: Optional[str] = None,
|
171
|
+
cloud: Optional[str] = None,
|
172
|
+
project: Optional[str] = None,
|
173
|
+
include_archived: bool = False,
|
174
|
+
# Paging
|
175
|
+
max_items: Optional[int] = None,
|
176
|
+
page_size: Optional[int] = None,
|
177
|
+
# Sorting
|
178
|
+
sort_field: Optional[Union[str, ServiceSortField]] = None,
|
179
|
+
sort_order: Optional[Union[str, SortOrder]] = None,
|
180
|
+
) -> ResultIterator[ServiceStatus]:
|
181
|
+
"""List services.
|
182
|
+
|
183
|
+
Returns
|
184
|
+
-------
|
185
|
+
ResultIterator[ServiceStatus]
|
186
|
+
An iterator yielding ServiceStatus objects. Fetches pages
|
187
|
+
lazily as needed. Use a `for` loop to iterate or `list()`
|
188
|
+
to consume all at once.
|
189
|
+
"""
|
190
|
+
return self._private_sdk.list(
|
191
|
+
service_id=service_id,
|
192
|
+
name=name,
|
193
|
+
state_filter=state_filter,
|
194
|
+
creator_id=creator_id,
|
195
|
+
cloud=cloud,
|
196
|
+
project=project,
|
197
|
+
include_archived=include_archived,
|
198
|
+
max_items=max_items,
|
199
|
+
page_size=page_size,
|
200
|
+
sort_field=sort_field,
|
201
|
+
sort_order=sort_order,
|
202
|
+
)
|
153
203
|
|
154
204
|
@sdk_docs(
|
155
205
|
doc_py_example=_STATUS_EXAMPLE, arg_docstrings=_STATUS_ARG_DOCSTRINGS,
|
@@ -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
|