anyscale 0.26.50__py3-none-any.whl → 0.26.52__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/README.md +1 -1
- anyscale/_private/anyscale_client/anyscale_client.py +178 -46
- anyscale/_private/anyscale_client/common.py +61 -2
- anyscale/_private/anyscale_client/fake_anyscale_client.py +145 -8
- anyscale/_private/docgen/__main__.py +34 -23
- anyscale/_private/docgen/generator.py +15 -18
- anyscale/_private/docgen/models.md +4 -2
- anyscale/_private/workload/workload_sdk.py +103 -8
- anyscale/client/README.md +5 -0
- anyscale/client/openapi_client/__init__.py +1 -0
- anyscale/client/openapi_client/api/default_api.py +538 -0
- anyscale/client/openapi_client/models/__init__.py +1 -0
- anyscale/client/openapi_client/models/baseimagesenum.py +83 -1
- anyscale/client/openapi_client/models/cloud_resource.py +59 -3
- anyscale/client/openapi_client/models/cloud_resource_gcp.py +59 -3
- anyscale/client/openapi_client/models/clouddeployment_response.py +121 -0
- anyscale/client/openapi_client/models/create_cloud_resource.py +59 -3
- anyscale/client/openapi_client/models/create_cloud_resource_gcp.py +59 -3
- anyscale/client/openapi_client/models/object_storage.py +2 -2
- anyscale/client/openapi_client/models/ray_runtime_env_config.py +57 -1
- anyscale/client/openapi_client/models/supportedbaseimagesenum.py +80 -1
- anyscale/cloud/models.py +1 -1
- anyscale/commands/cloud_commands.py +73 -70
- anyscale/commands/command_examples.py +28 -40
- anyscale/commands/project_commands.py +377 -106
- anyscale/commands/workspace_commands_v2.py +62 -29
- anyscale/controllers/cloud_controller.py +91 -91
- anyscale/job/_private/job_sdk.py +38 -20
- anyscale/project/__init__.py +101 -1
- anyscale/project/_private/project_sdk.py +90 -2
- anyscale/project/commands.py +188 -1
- anyscale/project/models.py +198 -2
- anyscale/sdk/anyscale_client/models/baseimagesenum.py +83 -1
- anyscale/sdk/anyscale_client/models/ray_runtime_env_config.py +57 -1
- anyscale/sdk/anyscale_client/models/supportedbaseimagesenum.py +80 -1
- anyscale/service/_private/service_sdk.py +2 -1
- anyscale/shared_anyscale_utils/latest_ray_version.py +1 -1
- anyscale/util.py +3 -0
- anyscale/utils/cloud_utils.py +20 -0
- anyscale/utils/runtime_env.py +3 -1
- anyscale/version.py +1 -1
- {anyscale-0.26.50.dist-info → anyscale-0.26.52.dist-info}/METADATA +1 -1
- {anyscale-0.26.50.dist-info → anyscale-0.26.52.dist-info}/RECORD +48 -47
- {anyscale-0.26.50.dist-info → anyscale-0.26.52.dist-info}/WHEEL +0 -0
- {anyscale-0.26.50.dist-info → anyscale-0.26.52.dist-info}/entry_points.txt +0 -0
- {anyscale-0.26.50.dist-info → anyscale-0.26.52.dist-info}/licenses/LICENSE +0 -0
- {anyscale-0.26.50.dist-info → anyscale-0.26.52.dist-info}/licenses/NOTICE +0 -0
- {anyscale-0.26.50.dist-info → anyscale-0.26.52.dist-info}/top_level.txt +0 -0
@@ -118,6 +118,7 @@ from anyscale.utils.cloud_utils import (
|
|
118
118
|
modify_memorydb_parameter_group,
|
119
119
|
validate_aws_credentials,
|
120
120
|
verify_anyscale_access,
|
121
|
+
wait_for_aws_lb_resource_termination,
|
121
122
|
wait_for_gcp_lb_resource_termination,
|
122
123
|
)
|
123
124
|
from anyscale.utils.imports.gcp import (
|
@@ -1424,18 +1425,6 @@ class CloudController(BaseController):
|
|
1424
1425
|
cloud_id, CloudProviders.AWS, functions_to_verify, yes,
|
1425
1426
|
)
|
1426
1427
|
|
1427
|
-
def get_cloud_deployment(
|
1428
|
-
self, cloud_id: str, cloud_deployment_id: str
|
1429
|
-
) -> CloudDeployment:
|
1430
|
-
try:
|
1431
|
-
return self.api_client.get_cloud_deployment_api_v2_clouds_cloud_id_deployment_get(
|
1432
|
-
cloud_id=cloud_id, cloud_deployment_id=cloud_deployment_id,
|
1433
|
-
).result
|
1434
|
-
except Exception as e: # noqa: BLE001
|
1435
|
-
raise ClickException(
|
1436
|
-
f"Failed to get cloud deployment {cloud_deployment_id} for cloud {cloud_id}. Error: {e}"
|
1437
|
-
)
|
1438
|
-
|
1439
1428
|
# Avoid displaying fields with empty values (since the values for optional fields default to None).
|
1440
1429
|
def _remove_empty_values(self, d):
|
1441
1430
|
if isinstance(d, dict):
|
@@ -1476,29 +1465,6 @@ class CloudController(BaseController):
|
|
1476
1465
|
],
|
1477
1466
|
}
|
1478
1467
|
|
1479
|
-
def get_cloud_deployment_dict_by_name(
|
1480
|
-
self, cloud_name: str, cloud_deployment_name: Optional[str]
|
1481
|
-
) -> Dict[str, Any]:
|
1482
|
-
cloud_id, _ = get_cloud_id_and_name(self.api_client, cloud_name=cloud_name)
|
1483
|
-
|
1484
|
-
result = self.get_cloud_deployments(cloud_id)
|
1485
|
-
deployments = result.get("deployments", [])
|
1486
|
-
if len(deployments) == 0:
|
1487
|
-
raise ClickException(f"Cloud {cloud_name} has no cloud deployments.")
|
1488
|
-
|
1489
|
-
if cloud_deployment_name is None:
|
1490
|
-
if len(deployments) > 1:
|
1491
|
-
self.log.warning(
|
1492
|
-
f"Cloud {cloud_name} has {len(deployments)} deployments, only the primary deployment will be returned."
|
1493
|
-
)
|
1494
|
-
return deployments[0]
|
1495
|
-
|
1496
|
-
for deployment in deployments:
|
1497
|
-
if deployment.get("name") == cloud_deployment_name:
|
1498
|
-
return deployment
|
1499
|
-
|
1500
|
-
raise ClickException(f"Cloud deployment {cloud_deployment_name} not found.")
|
1501
|
-
|
1502
1468
|
def update_aws_anyscale_iam_role(
|
1503
1469
|
self,
|
1504
1470
|
cloud_id: str,
|
@@ -1775,85 +1741,103 @@ class CloudController(BaseController):
|
|
1775
1741
|
f"Successfully created cloud deployment{' ' + new_deployment.name if new_deployment.name else ''} in cloud {existing_spec['name']}!"
|
1776
1742
|
)
|
1777
1743
|
|
1778
|
-
def
|
1744
|
+
def update_cloud_deployments( # noqa: PLR0912, C901
|
1779
1745
|
self,
|
1780
|
-
|
1781
|
-
|
1746
|
+
cloud_name: Optional[str],
|
1747
|
+
cloud_id: Optional[str],
|
1748
|
+
resources_file: str,
|
1782
1749
|
skip_verification: bool = False,
|
1783
1750
|
yes: bool = False,
|
1784
1751
|
):
|
1752
|
+
if not cloud_id:
|
1753
|
+
cloud_id, _ = get_cloud_id_and_name(self.api_client, cloud_name=cloud_name)
|
1754
|
+
assert cloud_id
|
1755
|
+
|
1785
1756
|
# Read the spec file.
|
1786
|
-
path = pathlib.Path(
|
1757
|
+
path = pathlib.Path(resources_file)
|
1787
1758
|
if not path.exists():
|
1788
|
-
raise ClickException(f"{
|
1759
|
+
raise ClickException(f"{resources_file} does not exist.")
|
1789
1760
|
if not path.is_file():
|
1790
|
-
raise ClickException(f"{
|
1761
|
+
raise ClickException(f"{resources_file} is not a file.")
|
1791
1762
|
|
1792
1763
|
spec = yaml.safe_load(path.read_text())
|
1793
|
-
try:
|
1794
|
-
updated_deployment = CloudDeployment(**spec)
|
1795
|
-
except Exception as e: # noqa: BLE001
|
1796
|
-
raise ClickException(f"Failed to parse cloud deployment: {e}")
|
1797
1764
|
|
1798
|
-
|
1765
|
+
# Get the existing spec.
|
1766
|
+
existing_spec = self.get_cloud_deployments(cloud_id=cloud_id)
|
1767
|
+
|
1768
|
+
if len(existing_spec["deployments"]) > len(spec):
|
1799
1769
|
raise ClickException(
|
1800
|
-
"
|
1770
|
+
"Please use `anyscale cloud deployment delete` to remove cloud deployments."
|
1801
1771
|
)
|
1802
|
-
|
1803
|
-
# Get the existing cloud deployment.
|
1804
|
-
cloud_id, _ = get_cloud_id_and_name(self.api_client, cloud_name=cloud)
|
1805
|
-
existing_deployment = self.get_cloud_deployment(
|
1806
|
-
cloud_id=cloud_id,
|
1807
|
-
cloud_deployment_id=updated_deployment.cloud_deployment_id,
|
1808
|
-
)
|
1809
|
-
if (
|
1810
|
-
updated_deployment.provider == CloudProviders.PCP
|
1811
|
-
or existing_deployment.provider == CloudProviders.PCP
|
1812
|
-
):
|
1772
|
+
if len(existing_spec["deployments"]) < len(spec):
|
1813
1773
|
raise ClickException(
|
1814
|
-
"Please use
|
1774
|
+
"Please use `anyscale cloud deployment create` to add cloud deployments."
|
1815
1775
|
)
|
1816
1776
|
|
1817
|
-
# Diff the existing and new
|
1818
|
-
diff = self._generate_diff(
|
1819
|
-
self._remove_empty_values(existing_deployment.to_dict()),
|
1820
|
-
self._remove_empty_values(updated_deployment.to_dict()),
|
1821
|
-
)
|
1777
|
+
# Diff the existing and new specs
|
1778
|
+
diff = self._generate_diff(existing_spec["deployments"], spec)
|
1822
1779
|
if not diff:
|
1823
1780
|
self.log.info("No changes detected.")
|
1824
1781
|
return
|
1825
1782
|
|
1826
|
-
|
1827
|
-
|
1828
|
-
|
1829
|
-
|
1830
|
-
|
1831
|
-
|
1832
|
-
|
1833
|
-
|
1834
|
-
|
1835
|
-
|
1836
|
-
|
1837
|
-
|
1838
|
-
|
1839
|
-
|
1783
|
+
existing_deployments = {
|
1784
|
+
deployment["cloud_deployment_id"]: CloudDeployment(**deployment)
|
1785
|
+
for deployment in existing_spec["deployments"]
|
1786
|
+
}
|
1787
|
+
|
1788
|
+
updated_deployments: List[CloudDeployment] = []
|
1789
|
+
for d in spec:
|
1790
|
+
try:
|
1791
|
+
deployment = CloudDeployment(**d)
|
1792
|
+
except Exception as e: # noqa: BLE001
|
1793
|
+
raise ClickException(f"Failed to parse deployment: {e}")
|
1794
|
+
|
1795
|
+
if not deployment.cloud_deployment_id:
|
1796
|
+
raise ClickException(
|
1797
|
+
"All cloud deployments must include a cloud_deployment_id."
|
1798
|
+
)
|
1799
|
+
if deployment.cloud_deployment_id not in existing_deployments:
|
1800
|
+
raise ClickException(
|
1801
|
+
f"Cloud deployment {deployment.cloud_deployment_id} not found."
|
1802
|
+
)
|
1803
|
+
if deployment.provider == CloudProviders.PCP:
|
1804
|
+
raise ClickException(
|
1805
|
+
"Please use the `anyscale machine-pool` CLI to update machine pools."
|
1806
|
+
)
|
1807
|
+
if deployment != existing_deployments[deployment.cloud_deployment_id]:
|
1808
|
+
updated_deployments.append(deployment)
|
1840
1809
|
|
1841
1810
|
# Log the diff and confirm.
|
1842
1811
|
self.log.info(f"Detected the following changes:\n{diff}")
|
1843
1812
|
|
1844
|
-
confirm("Would you like to proceed with updating this cloud
|
1813
|
+
confirm("Would you like to proceed with updating this cloud?", yes)
|
1814
|
+
|
1815
|
+
# Preprocess the deployments if necessary.
|
1816
|
+
for deployment in updated_deployments:
|
1817
|
+
if deployment.provider == CloudProviders.AWS:
|
1818
|
+
self._preprocess_aws(cloud_id=cloud_id, deployment=deployment)
|
1819
|
+
elif deployment.provider == CloudProviders.GCP:
|
1820
|
+
self._preprocess_gcp(deployment=deployment)
|
1821
|
+
|
1822
|
+
# Skip verification for Kubernetes stacks or if explicitly requested
|
1823
|
+
if deployment.compute_stack == ComputeStack.K8S:
|
1824
|
+
self.log.info("Skipping verification for Kubernetes compute stack.")
|
1825
|
+
elif not skip_verification and not self.verify_cloud_deployment(
|
1826
|
+
cloud_id=cloud_id, cloud_deployment=deployment
|
1827
|
+
):
|
1828
|
+
raise ClickException(
|
1829
|
+
f"Verification failed for cloud deployment {deployment.name or deployment.cloud_deployment_id}."
|
1830
|
+
)
|
1845
1831
|
|
1846
|
-
# Update the
|
1832
|
+
# Update the deployments.
|
1847
1833
|
try:
|
1848
|
-
self.api_client.
|
1849
|
-
cloud_id=cloud_id, cloud_deployment=
|
1834
|
+
self.api_client.update_cloud_deployments_api_v2_clouds_cloud_id_deployments_put(
|
1835
|
+
cloud_id=cloud_id, cloud_deployment=updated_deployments,
|
1850
1836
|
)
|
1851
1837
|
except Exception as e: # noqa: BLE001
|
1852
|
-
raise ClickException(f"Failed to update cloud
|
1838
|
+
raise ClickException(f"Failed to update cloud deployments: {e}")
|
1853
1839
|
|
1854
|
-
self.log.info(
|
1855
|
-
f"Successfully updated cloud deployment {updated_deployment.name or updated_deployment.cloud_deployment_id} in cloud {cloud}."
|
1856
|
-
)
|
1840
|
+
self.log.info(f"Successfully updated cloud {cloud_name or cloud_id}.")
|
1857
1841
|
|
1858
1842
|
def remove_cloud_deployment(
|
1859
1843
|
self, cloud_name: str, deployment_name: str, yes: bool,
|
@@ -2516,6 +2500,8 @@ class CloudController(BaseController):
|
|
2516
2500
|
cloud_storage_bucket_region: Optional[str] = None,
|
2517
2501
|
nfs_mount_targets: Optional[List[str]] = None,
|
2518
2502
|
nfs_mount_path: Optional[str] = None,
|
2503
|
+
persistent_volume_claim: Optional[str] = None,
|
2504
|
+
csi_ephemeral_volume_driver: Optional[str] = None,
|
2519
2505
|
kubernetes_zones: Optional[List[str]] = None,
|
2520
2506
|
anyscale_operator_iam_identity: Optional[str] = None,
|
2521
2507
|
) -> None:
|
@@ -2587,6 +2573,8 @@ class CloudController(BaseController):
|
|
2587
2573
|
or region,
|
2588
2574
|
nfs_mount_targets=mount_targets,
|
2589
2575
|
nfs_mount_path=nfs_mount_path,
|
2576
|
+
persistent_volume_claim=persistent_volume_claim,
|
2577
|
+
csi_ephemeral_volume_driver=csi_ephemeral_volume_driver,
|
2590
2578
|
),
|
2591
2579
|
),
|
2592
2580
|
)
|
@@ -2660,6 +2648,8 @@ class CloudController(BaseController):
|
|
2660
2648
|
compute_stack: ComputeStack = ComputeStack.VM,
|
2661
2649
|
kubernetes_zones: Optional[List[str]] = None,
|
2662
2650
|
anyscale_operator_iam_identity: Optional[str] = None,
|
2651
|
+
persistent_volume_claim: Optional[str] = None,
|
2652
|
+
csi_ephemeral_volume_driver: Optional[str] = None,
|
2663
2653
|
):
|
2664
2654
|
functions_to_verify = self._validate_functional_verification_args(
|
2665
2655
|
functional_verify
|
@@ -2823,6 +2813,8 @@ class CloudController(BaseController):
|
|
2823
2813
|
kubernetes_zones=kubernetes_zones,
|
2824
2814
|
kubernetes_dataplane_identity=anyscale_operator_iam_identity,
|
2825
2815
|
cloud_storage_bucket_name=cloud_storage_bucket_name,
|
2816
|
+
persistent_volume_claim=persistent_volume_claim,
|
2817
|
+
csi_ephemeral_volume_driver=csi_ephemeral_volume_driver,
|
2826
2818
|
)
|
2827
2819
|
|
2828
2820
|
# Verification is only performed for VM compute stack.
|
@@ -3186,6 +3178,8 @@ class CloudController(BaseController):
|
|
3186
3178
|
compute_stack: ComputeStack = ComputeStack.VM,
|
3187
3179
|
kubernetes_zones: Optional[List[str]] = None,
|
3188
3180
|
anyscale_operator_iam_identity: Optional[str] = None,
|
3181
|
+
persistent_volume_claim: Optional[str] = None,
|
3182
|
+
csi_ephemeral_volume_driver: Optional[str] = None,
|
3189
3183
|
):
|
3190
3184
|
functions_to_verify = self._validate_functional_verification_args(
|
3191
3185
|
functional_verify
|
@@ -3335,6 +3329,8 @@ class CloudController(BaseController):
|
|
3335
3329
|
kubernetes_zones=kubernetes_zones,
|
3336
3330
|
kubernetes_dataplane_identity=anyscale_operator_iam_identity,
|
3337
3331
|
cloud_storage_bucket_name=cloud_storage_bucket_name,
|
3332
|
+
persistent_volume_claim=persistent_volume_claim,
|
3333
|
+
csi_ephemeral_volume_driver=csi_ephemeral_volume_driver,
|
3338
3334
|
)
|
3339
3335
|
|
3340
3336
|
# Verification is only performed for VM compute stack.
|
@@ -3525,6 +3521,15 @@ class CloudController(BaseController):
|
|
3525
3521
|
# Clean up cloud resources
|
3526
3522
|
try:
|
3527
3523
|
if cloud_provider == CloudProviders.AWS:
|
3524
|
+
if not (cloud.is_aioa or cloud.compute_stack == ComputeStack.K8S):
|
3525
|
+
# Delete services resources
|
3526
|
+
self.delete_aws_lb_cfn_stack(cloud=cloud)
|
3527
|
+
with self.log.spinner("Deleting load balancing resources..."):
|
3528
|
+
wait_for_aws_lb_resource_termination(
|
3529
|
+
api_client=self.api_client, cloud_id=cloud_id
|
3530
|
+
)
|
3531
|
+
self.delete_aws_tls_certificates(cloud=cloud)
|
3532
|
+
|
3528
3533
|
self.delete_all_aws_resources(cloud)
|
3529
3534
|
elif cloud_provider == CloudProviders.GCP:
|
3530
3535
|
with self.log.spinner("Deleting load balancing resources..."):
|
@@ -3585,10 +3590,6 @@ class CloudController(BaseController):
|
|
3585
3590
|
# No resources to delete for hosted and k8s clouds
|
3586
3591
|
return True
|
3587
3592
|
|
3588
|
-
# Delete services resources
|
3589
|
-
self.delete_aws_lb_cfn_stack(cloud=cloud)
|
3590
|
-
self.delete_aws_tls_certificates(cloud=cloud)
|
3591
|
-
|
3592
3593
|
# Clean up cloud resources
|
3593
3594
|
if cloud.is_bring_your_own_resource is False: # managed cloud
|
3594
3595
|
# Delete AWS cloud resources by deleting the cfn stack
|
@@ -3601,7 +3602,6 @@ class CloudController(BaseController):
|
|
3601
3602
|
return True
|
3602
3603
|
|
3603
3604
|
def delete_aws_lb_cfn_stack(self, cloud: CloudWithCloudResource) -> bool:
|
3604
|
-
|
3605
3605
|
tag_name = "anyscale-cloud-id"
|
3606
3606
|
key_name = cloud.id
|
3607
3607
|
|
anyscale/job/_private/job_sdk.py
CHANGED
@@ -82,19 +82,34 @@ class PrivateJobSDK(WorkloadSDK):
|
|
82
82
|
- 'working_dir' will be set to '.'.
|
83
83
|
- 'pip' will be set to the workspace-managed requirements file.
|
84
84
|
"""
|
85
|
-
|
86
|
-
|
87
|
-
[runtime_env],
|
88
|
-
working_dir_override=config.working_dir,
|
89
|
-
excludes_override=config.excludes,
|
90
|
-
cloud_id=cloud_id,
|
91
|
-
autopopulate_in_workspace=autopopulate_in_workspace,
|
92
|
-
additional_py_modules=config.py_modules,
|
93
|
-
py_executable_override=config.py_executable,
|
94
|
-
has_multiple_cloud_deployments=self._compute_config_has_multiple_cloud_deployments(
|
95
|
-
compute_config=config.compute_config, cloud=config.cloud
|
96
|
-
),
|
85
|
+
cloud_deployments = self._get_compute_config_cloud_deployments(
|
86
|
+
compute_config=config.compute_config, cloud=config.cloud
|
97
87
|
)
|
88
|
+
assert len(cloud_deployments) > 0
|
89
|
+
|
90
|
+
runtime_env: Dict[str, Any] = {}
|
91
|
+
if len(cloud_deployments) == 1:
|
92
|
+
[runtime_env] = self.override_and_upload_local_dirs_single_deployment(
|
93
|
+
[runtime_env],
|
94
|
+
working_dir_override=config.working_dir,
|
95
|
+
excludes_override=config.excludes,
|
96
|
+
cloud_id=cloud_id,
|
97
|
+
autopopulate_in_workspace=autopopulate_in_workspace,
|
98
|
+
additional_py_modules=config.py_modules,
|
99
|
+
py_executable_override=config.py_executable,
|
100
|
+
cloud_deployment=cloud_deployments[0],
|
101
|
+
)
|
102
|
+
else:
|
103
|
+
[runtime_env] = self.override_and_upload_local_dirs_multi_deployment(
|
104
|
+
[runtime_env],
|
105
|
+
working_dir_override=config.working_dir,
|
106
|
+
excludes_override=config.excludes,
|
107
|
+
cloud_id=cloud_id,
|
108
|
+
autopopulate_in_workspace=autopopulate_in_workspace,
|
109
|
+
additional_py_modules=config.py_modules,
|
110
|
+
py_executable_override=config.py_executable,
|
111
|
+
cloud_deployments=cloud_deployments,
|
112
|
+
)
|
98
113
|
[runtime_env] = self.override_and_load_requirements_files(
|
99
114
|
[runtime_env],
|
100
115
|
requirements_override=config.requirements,
|
@@ -106,15 +121,15 @@ class PrivateJobSDK(WorkloadSDK):
|
|
106
121
|
|
107
122
|
return runtime_env or None
|
108
123
|
|
109
|
-
def
|
124
|
+
def _get_compute_config_cloud_deployments(
|
110
125
|
self, compute_config: Union[ComputeConfigType, str, None], cloud: Optional[str]
|
111
|
-
) ->
|
126
|
+
) -> List[Optional[str]]:
|
112
127
|
if isinstance(compute_config, ComputeConfig):
|
113
128
|
# single-deployment compute config
|
114
|
-
return
|
129
|
+
return [compute_config.cloud_deployment]
|
115
130
|
|
116
131
|
if isinstance(compute_config, MultiDeploymentComputeConfig):
|
117
|
-
return
|
132
|
+
return [config.cloud_deployment for config in compute_config.configs]
|
118
133
|
|
119
134
|
compute_config_id = self._resolve_compute_config_id(
|
120
135
|
compute_config=compute_config, cloud=cloud
|
@@ -125,10 +140,13 @@ class PrivateJobSDK(WorkloadSDK):
|
|
125
140
|
f"The compute config '{compute_config_id}' does not exist."
|
126
141
|
)
|
127
142
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
143
|
+
if compute_template.config.deployment_configs is None:
|
144
|
+
return [None]
|
145
|
+
|
146
|
+
return [
|
147
|
+
config.cloud_deployment
|
148
|
+
for config in compute_template.config.deployment_configs
|
149
|
+
]
|
132
150
|
|
133
151
|
def get_default_name(self) -> str:
|
134
152
|
"""Get a default name for the job.
|
anyscale/project/__init__.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
from typing import List, Optional
|
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
|
@@ -8,9 +9,30 @@ from anyscale.project._private.project_sdk import PrivateProjectSDK
|
|
8
9
|
from anyscale.project.commands import (
|
9
10
|
_ADD_COLLABORATORS_DOCSTRINGS,
|
10
11
|
_ADD_COLLABORATORS_EXAMPLE,
|
12
|
+
_CREATE_PROJECT_DOCSTRINGS,
|
13
|
+
_CREATE_PROJECT_EXAMPLE,
|
14
|
+
_DELETE_PROJECT_DOCSTRINGS,
|
15
|
+
_DELETE_PROJECT_EXAMPLE,
|
16
|
+
_GET_DEFAULT_PROJECT_DOCSTRINGS,
|
17
|
+
_GET_DEFAULT_PROJECT_EXAMPLE,
|
18
|
+
_GET_PROJECT_DOCSTRINGS,
|
19
|
+
_GET_PROJECT_EXAMPLE,
|
20
|
+
_LIST_PROJECTS_DOCSTRINGS,
|
21
|
+
_LIST_PROJECTS_EXAMPLE,
|
11
22
|
add_collaborators,
|
23
|
+
create,
|
24
|
+
delete,
|
25
|
+
get,
|
26
|
+
get_default,
|
27
|
+
list,
|
28
|
+
)
|
29
|
+
from anyscale.project.models import (
|
30
|
+
CreateProjectCollaborator,
|
31
|
+
Project,
|
32
|
+
ProjectPermissionLevel,
|
33
|
+
ProjectSortField,
|
34
|
+
ProjectSortOrder,
|
12
35
|
)
|
13
|
-
from anyscale.project.models import CreateProjectCollaborator
|
14
36
|
|
15
37
|
|
16
38
|
class ProjectSDK:
|
@@ -33,3 +55,81 @@ class ProjectSDK:
|
|
33
55
|
"""Batch add collaborators to a project.
|
34
56
|
"""
|
35
57
|
self._private_sdk.add_collaborators(cloud, project, collaborators)
|
58
|
+
|
59
|
+
@sdk_docs(
|
60
|
+
doc_py_example=_GET_PROJECT_EXAMPLE, arg_docstrings=_GET_PROJECT_DOCSTRINGS,
|
61
|
+
)
|
62
|
+
def get(self, project_id: str) -> Project: # noqa: F811
|
63
|
+
"""Get details of a project."""
|
64
|
+
return self._private_sdk.get(project_id)
|
65
|
+
|
66
|
+
@sdk_docs(
|
67
|
+
doc_py_example=_LIST_PROJECTS_EXAMPLE, arg_docstrings=_LIST_PROJECTS_DOCSTRINGS,
|
68
|
+
)
|
69
|
+
def list( # noqa: F811
|
70
|
+
self,
|
71
|
+
*,
|
72
|
+
name_contains: Optional[str] = None,
|
73
|
+
creator_id: Optional[str] = None,
|
74
|
+
parent_cloud_id: Optional[str] = None,
|
75
|
+
include_defaults: bool = True,
|
76
|
+
max_items: Optional[int] = None,
|
77
|
+
page_size: Optional[int] = None,
|
78
|
+
sort_field: Optional[ProjectSortField] = None,
|
79
|
+
sort_order: Optional[ProjectSortOrder] = None,
|
80
|
+
) -> ResultIterator[Project]:
|
81
|
+
"""List all projects with optional filters.
|
82
|
+
|
83
|
+
Returns
|
84
|
+
-------
|
85
|
+
ResultIterator[Project]
|
86
|
+
An iterator yielding Project objects. Fetches pages
|
87
|
+
lazily as needed. Use a `for` loop to iterate or `list()`
|
88
|
+
to consume all at once.
|
89
|
+
"""
|
90
|
+
return self._private_sdk.list(
|
91
|
+
name_contains=name_contains,
|
92
|
+
creator_id=creator_id,
|
93
|
+
parent_cloud_id=parent_cloud_id,
|
94
|
+
include_defaults=include_defaults,
|
95
|
+
max_items=max_items,
|
96
|
+
page_size=page_size,
|
97
|
+
sort_field=sort_field,
|
98
|
+
sort_order=sort_order,
|
99
|
+
)
|
100
|
+
|
101
|
+
@sdk_docs(
|
102
|
+
doc_py_example=_CREATE_PROJECT_EXAMPLE,
|
103
|
+
arg_docstrings=_CREATE_PROJECT_DOCSTRINGS,
|
104
|
+
)
|
105
|
+
def create( # noqa: F811
|
106
|
+
self,
|
107
|
+
name: str,
|
108
|
+
parent_cloud_id: str,
|
109
|
+
*,
|
110
|
+
description: Optional[str] = None,
|
111
|
+
initial_cluster_config: Optional[str] = None,
|
112
|
+
) -> str:
|
113
|
+
"""Create a project."""
|
114
|
+
return self._private_sdk.create(
|
115
|
+
name,
|
116
|
+
description or "",
|
117
|
+
parent_cloud_id,
|
118
|
+
initial_cluster_config=initial_cluster_config,
|
119
|
+
)
|
120
|
+
|
121
|
+
@sdk_docs(
|
122
|
+
doc_py_example=_DELETE_PROJECT_EXAMPLE,
|
123
|
+
arg_docstrings=_DELETE_PROJECT_DOCSTRINGS,
|
124
|
+
)
|
125
|
+
def delete(self, project_id: str): # noqa: F811
|
126
|
+
"""Delete a project."""
|
127
|
+
self._private_sdk.delete(project_id)
|
128
|
+
|
129
|
+
@sdk_docs(
|
130
|
+
doc_py_example=_GET_DEFAULT_PROJECT_EXAMPLE,
|
131
|
+
arg_docstrings=_GET_DEFAULT_PROJECT_DOCSTRINGS,
|
132
|
+
)
|
133
|
+
def get_default(self, parent_cloud_id: str) -> Project: # noqa: F811
|
134
|
+
"""Get the default project for a cloud."""
|
135
|
+
return self._private_sdk.get_default(parent_cloud_id)
|
@@ -1,11 +1,23 @@
|
|
1
|
-
from typing import List
|
1
|
+
from typing import List, Optional
|
2
2
|
|
3
|
+
from anyscale._private.models.model_base import ResultIterator
|
3
4
|
from anyscale._private.sdk.base_sdk import BaseSDK
|
4
5
|
from anyscale.client.openapi_client import (
|
5
6
|
CreateUserProjectCollaborator,
|
6
7
|
CreateUserProjectCollaboratorValue,
|
8
|
+
Project as OpenAPIProject,
|
9
|
+
ProjectListResponse,
|
10
|
+
WriteProject,
|
7
11
|
)
|
8
|
-
from anyscale.project.models import
|
12
|
+
from anyscale.project.models import (
|
13
|
+
CreateProjectCollaborator,
|
14
|
+
Project,
|
15
|
+
ProjectSortField,
|
16
|
+
ProjectSortOrder,
|
17
|
+
)
|
18
|
+
|
19
|
+
|
20
|
+
MAX_PAGE_SIZE = 50
|
9
21
|
|
10
22
|
|
11
23
|
class PrivateProjectSDK(BaseSDK):
|
@@ -25,3 +37,79 @@ class PrivateProjectSDK(BaseSDK):
|
|
25
37
|
for collaborator in collaborators
|
26
38
|
],
|
27
39
|
)
|
40
|
+
|
41
|
+
def get(self, project_id: str) -> Optional[Project]:
|
42
|
+
project: OpenAPIProject = self.client.get_project(project_id)
|
43
|
+
return Project.from_dict(project.to_dict()) if project else None
|
44
|
+
|
45
|
+
def list(
|
46
|
+
self,
|
47
|
+
*,
|
48
|
+
# filters
|
49
|
+
name_contains: Optional[str] = None,
|
50
|
+
creator_id: Optional[str] = None,
|
51
|
+
parent_cloud_id: Optional[str] = None,
|
52
|
+
include_defaults: bool = True,
|
53
|
+
# pagination
|
54
|
+
max_items: Optional[int] = None,
|
55
|
+
page_size: Optional[int] = None,
|
56
|
+
# sorting
|
57
|
+
sort_field: Optional[ProjectSortField] = None,
|
58
|
+
sort_order: Optional[ProjectSortOrder] = None,
|
59
|
+
) -> ResultIterator[Project]:
|
60
|
+
if max_items is not None and (max_items <= 0):
|
61
|
+
raise ValueError("'max_items' must be greater than 0.")
|
62
|
+
|
63
|
+
if page_size is not None and (page_size <= 0 or page_size > MAX_PAGE_SIZE):
|
64
|
+
raise ValueError(
|
65
|
+
f"'page_size' must be between 1 and {MAX_PAGE_SIZE}, inclusive."
|
66
|
+
)
|
67
|
+
|
68
|
+
def _fetch_page(token: Optional[str],) -> ProjectListResponse:
|
69
|
+
return self.client.list_projects(
|
70
|
+
name_contains=name_contains,
|
71
|
+
creator_id=creator_id,
|
72
|
+
parent_cloud_id=parent_cloud_id,
|
73
|
+
include_defaults=include_defaults,
|
74
|
+
sort_field=str(sort_field) if sort_field else None,
|
75
|
+
sort_order=str(sort_order) if sort_order else None,
|
76
|
+
paging_token=token,
|
77
|
+
count=page_size,
|
78
|
+
)
|
79
|
+
|
80
|
+
def _openapi_project_to_sdk_project(openapi_project: OpenAPIProject) -> Project:
|
81
|
+
return Project.from_dict(openapi_project.to_dict())
|
82
|
+
|
83
|
+
return ResultIterator(
|
84
|
+
page_token=None,
|
85
|
+
max_items=max_items,
|
86
|
+
fetch_page=_fetch_page,
|
87
|
+
parse_fn=_openapi_project_to_sdk_project,
|
88
|
+
)
|
89
|
+
|
90
|
+
def create(
|
91
|
+
self,
|
92
|
+
name: str,
|
93
|
+
description: str,
|
94
|
+
parent_cloud_id: str,
|
95
|
+
*,
|
96
|
+
initial_cluster_config: Optional[str] = None,
|
97
|
+
) -> str:
|
98
|
+
project = self.client.create_project(
|
99
|
+
project=WriteProject(
|
100
|
+
name=name,
|
101
|
+
description=description,
|
102
|
+
parent_cloud_id=parent_cloud_id,
|
103
|
+
initial_cluster_config=initial_cluster_config,
|
104
|
+
)
|
105
|
+
)
|
106
|
+
return project.id # type: ignore
|
107
|
+
|
108
|
+
def delete(
|
109
|
+
self, project_id: str,
|
110
|
+
):
|
111
|
+
self.client.delete_project(project_id)
|
112
|
+
|
113
|
+
def get_default(self, parent_cloud_id: str,) -> Project:
|
114
|
+
project: OpenAPIProject = self.client.get_default_project(parent_cloud_id)
|
115
|
+
return Project.from_dict(project.to_dict())
|