From 60e73b7aa343716caf4565aacaa93039f9ce099a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 19:14:04 +0000 Subject: [PATCH 01/14] feat(api): add cluster config/OIDC/add-ons params, project filtering, update storage types --- .stats.yml | 4 +- api.md | 4 +- .../resources/beta/clusters/clusters.py | 194 +++++++++- .../resources/beta/clusters/storage.py | 78 ++-- src/together/types/beta/__init__.py | 1 + src/together/types/beta/cluster.py | 349 +++++++++++++++++- .../types/beta/cluster_create_params.py | 224 ++++++++++- .../types/beta/cluster_list_params.py | 16 + .../types/beta/cluster_update_params.py | 122 +++++- src/together/types/beta/clusters/__init__.py | 1 + .../types/beta/clusters/cluster_storage.py | 8 +- .../beta/clusters/storage_create_params.py | 5 +- .../beta/clusters/storage_list_params.py | 16 + .../beta/clusters/storage_update_params.py | 8 +- .../beta/clusters/test_storage.py | 64 +++- tests/api_resources/beta/test_clusters.py | 182 ++++++++- 16 files changed, 1188 insertions(+), 88 deletions(-) create mode 100644 src/together/types/beta/cluster_list_params.py create mode 100644 src/together/types/beta/clusters/storage_list_params.py diff --git a/.stats.yml b/.stats.yml index 498815517..409398869 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 75 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-5f05c9669c67c3f4b0ebfe2317d2768cd96317424965ebb2acf06a7757a7d0ca.yml -openapi_spec_hash: 84f45151f4d0eed68551b5ffda61595a +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-e398785d31ad51e94f158b377be469e74748e0229a964d3602149c6bf964581e.yml +openapi_spec_hash: 3e4db6f27d100314926d87c9ae24cba2 config_hash: ec427df08d61d8888138f15cd53c6454 diff --git a/api.md b/api.md index 1ea19fb05..c32e307a7 100644 --- a/api.md +++ b/api.md @@ -87,7 +87,7 @@ Methods: - client.beta.clusters.create(\*\*params) -> Cluster - client.beta.clusters.retrieve(cluster_id) -> Cluster - client.beta.clusters.update(cluster_id, \*\*params) -> Cluster -- client.beta.clusters.list() -> ClusterListResponse +- client.beta.clusters.list(\*\*params) -> ClusterListResponse - client.beta.clusters.delete(cluster_id) -> ClusterDeleteResponse - client.beta.clusters.list_regions() -> ClusterListRegionsResponse @@ -104,7 +104,7 @@ Methods: - client.beta.clusters.storage.create(\*\*params) -> ClusterStorage - client.beta.clusters.storage.retrieve(volume_id) -> ClusterStorage - client.beta.clusters.storage.update(\*\*params) -> ClusterStorage -- client.beta.clusters.storage.list() -> StorageListResponse +- client.beta.clusters.storage.list(\*\*params) -> StorageListResponse - client.beta.clusters.storage.delete(volume_id) -> StorageDeleteResponse # Chat diff --git a/src/together/resources/beta/clusters/clusters.py b/src/together/resources/beta/clusters/clusters.py index 5014087ac..935d71153 100644 --- a/src/together/resources/beta/clusters/clusters.py +++ b/src/together/resources/beta/clusters/clusters.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Union +from typing import Union, Iterable from datetime import datetime from typing_extensions import Literal @@ -26,7 +26,7 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from ....types.beta import cluster_create_params, cluster_update_params +from ....types.beta import cluster_list_params, cluster_create_params, cluster_update_params from ...._base_client import make_request_options from ....types.beta.cluster import Cluster from ....types.beta.cluster_list_response import ClusterListResponse @@ -66,17 +66,26 @@ def create( billing_type: Literal["RESERVED", "ON_DEMAND", "SCHEDULED_CAPACITY"], cluster_name: str, cuda_version: str, + duration_days: int, gpu_type: Literal["H100_SXM", "H200_SXM", "RTX_6000_PCI", "L40_PCIE", "B200_SXM", "H100_SXM_INF"], num_gpus: int, nvidia_driver_version: str, region: str, + acceptance_tests_params: cluster_create_params.AcceptanceTestsParams | Omit = omit, + add_ons: Iterable[cluster_create_params.AddOn] | Omit = omit, + auto_scale: bool | Omit = omit, auto_scale_max_gpus: int | Omit = omit, auto_scaled: bool | Omit = omit, capacity_pool_id: str | Omit = omit, + cluster_config: cluster_create_params.ClusterConfig | Omit = omit, cluster_type: Literal["KUBERNETES", "SLURM"] | Omit = omit, - duration_days: int | Omit = omit, gpu_node_failover_enabled: bool | Omit = omit, install_traefik: bool | Omit = omit, + num_capacity_pool_gpus: int | Omit = omit, + num_preemptible_gpus: int | Omit = omit, + num_reserved_gpus: int | Omit = omit, + oidc_config: cluster_create_params.OidcConfig | Omit = omit, + project_id: str | Omit = omit, reservation_end_time: Union[str, datetime] | Omit = omit, reservation_start_time: Union[str, datetime] | Omit = omit, shared_volume: cluster_create_params.SharedVolume | Omit = omit, @@ -109,6 +118,8 @@ def create( cuda_version: CUDA version for this cluster. For example, 12.5 + duration_days: Duration in days to keep the cluster running. + gpu_type: Type of GPU to use in the cluster num_gpus: Number of GPUs to allocate in the cluster. This must be multiple of 8. For @@ -120,6 +131,15 @@ def create( region: Region to create the GPU cluster in. Usable regions can be found from `client.clusters.list_regions()` + acceptance_tests_params: AcceptanceTestsParams groups all GPU acceptance test options when enabled is + true. + + add_ons: Add-ons to enable on the cluster at creation time. + + auto_scale: Whether to enable auto-scaling for the cluster. If true, the cluster will + automatically scale the number of GPU worker nodes between num_gpus and + auto_scale_max_gpus based on the workload. + auto_scale_max_gpus: Maximum number of GPUs to which the cluster can be auto-scaled up. This field is required if auto_scaled is true. @@ -131,14 +151,26 @@ def create( cluster_type: Type of cluster to create. - duration_days: Duration in days to keep the cluster running. - gpu_node_failover_enabled: Whether automated GPU node failover should be enabled for this cluster. By default, it is disabled. install_traefik: Whether to install Traefik ingress controller in the cluster. This field is only applicable for Kubernetes clusters and is false by default. + num_capacity_pool_gpus: Number of GPUs to allocate from the capacity pool. Must be a multiple of 8 and + not exceed num_gpus. + + num_preemptible_gpus: Number of preemptible GPUs to request alongside on-demand capacity. Must be a + multiple of 8. Preemptible nodes are cheaper but may be reclaimed when on-demand + capacity is needed elsewhere; the system fulfills this asynchronously and + surfaces the actual count in allocated_preemptible_gpus. + + num_reserved_gpus: Number of prepaid (PLG) reserved GPUs for this cluster. When omitted for + RESERVED billing on create, the server defaults this to num_gpus. + + project_id: Project ID for the cluster. If not set, the project from the request context is + used. + reservation_end_time: Reservation end time of the cluster. This field is required for SCHEDULED billing to specify the reservation end time for the cluster. @@ -170,17 +202,26 @@ def create( "billing_type": billing_type, "cluster_name": cluster_name, "cuda_version": cuda_version, + "duration_days": duration_days, "gpu_type": gpu_type, "num_gpus": num_gpus, "nvidia_driver_version": nvidia_driver_version, "region": region, + "acceptance_tests_params": acceptance_tests_params, + "add_ons": add_ons, + "auto_scale": auto_scale, "auto_scale_max_gpus": auto_scale_max_gpus, "auto_scaled": auto_scaled, "capacity_pool_id": capacity_pool_id, + "cluster_config": cluster_config, "cluster_type": cluster_type, - "duration_days": duration_days, "gpu_node_failover_enabled": gpu_node_failover_enabled, "install_traefik": install_traefik, + "num_capacity_pool_gpus": num_capacity_pool_gpus, + "num_preemptible_gpus": num_preemptible_gpus, + "num_reserved_gpus": num_reserved_gpus, + "oidc_config": oidc_config, + "project_id": project_id, "reservation_end_time": reservation_end_time, "reservation_start_time": reservation_start_time, "shared_volume": shared_volume, @@ -235,8 +276,12 @@ def update( self, cluster_id: str, *, + add_ons: Iterable[cluster_update_params.AddOn] | Omit = omit, + cluster_config: cluster_update_params.ClusterConfig | Omit = omit, cluster_type: Literal["KUBERNETES", "SLURM"] | Omit = omit, num_gpus: int | Omit = omit, + num_preemptible_gpus: int | Omit = omit, + num_reserved_gpus: int | Omit = omit, reservation_end_time: Union[str, datetime] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -251,10 +296,20 @@ def update( Args: cluster_id: The ID of the cluster to update + add_ons: Add-ons to update on the cluster. Each entry identifies an existing add-on by + name and provides the new external config to merge. + cluster_type: Type of cluster to update. - num_gpus: Number of GPUs to allocate in the cluster. This must be multiple of 8. For - example, 8, 16 or 24 + num_gpus: Target GPU count for the cluster. When omitted, the server keeps the current GPU + count from cluster metadata (use for config-only or decommission-time-only + updates). + + num_preemptible_gpus: Updated desired number of preemptible GPUs for the cluster. When omitted, the + current value is preserved. Must be a multiple of 8. + + num_reserved_gpus: Number of reserved GPUs to update to. This field is only applicable for clusters + with RESERVED billing type. reservation_end_time: Timestamp at which the cluster should be decommissioned. Only accepted for prepaid clusters. @@ -273,8 +328,12 @@ def update( path_template("/compute/clusters/{cluster_id}", cluster_id=cluster_id), body=maybe_transform( { + "add_ons": add_ons, + "cluster_config": cluster_config, "cluster_type": cluster_type, "num_gpus": num_gpus, + "num_preemptible_gpus": num_preemptible_gpus, + "num_reserved_gpus": num_reserved_gpus, "reservation_end_time": reservation_end_time, }, cluster_update_params.ClusterUpdateParams, @@ -288,6 +347,7 @@ def update( def list( self, *, + project_id: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -295,11 +355,30 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ClusterListResponse: - """List all GPU clusters.""" + """ + List all GPU clusters. + + Args: + project_id: Optional UMS project ID to filter clusters by. When set, only clusters belonging + to this project are returned. The caller must be a member of the project; + otherwise the result set will be empty. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ return self._get( "/compute/clusters", options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"project_id": project_id}, cluster_list_params.ClusterListParams), ), cast_to=ClusterListResponse, ) @@ -389,17 +468,26 @@ async def create( billing_type: Literal["RESERVED", "ON_DEMAND", "SCHEDULED_CAPACITY"], cluster_name: str, cuda_version: str, + duration_days: int, gpu_type: Literal["H100_SXM", "H200_SXM", "RTX_6000_PCI", "L40_PCIE", "B200_SXM", "H100_SXM_INF"], num_gpus: int, nvidia_driver_version: str, region: str, + acceptance_tests_params: cluster_create_params.AcceptanceTestsParams | Omit = omit, + add_ons: Iterable[cluster_create_params.AddOn] | Omit = omit, + auto_scale: bool | Omit = omit, auto_scale_max_gpus: int | Omit = omit, auto_scaled: bool | Omit = omit, capacity_pool_id: str | Omit = omit, + cluster_config: cluster_create_params.ClusterConfig | Omit = omit, cluster_type: Literal["KUBERNETES", "SLURM"] | Omit = omit, - duration_days: int | Omit = omit, gpu_node_failover_enabled: bool | Omit = omit, install_traefik: bool | Omit = omit, + num_capacity_pool_gpus: int | Omit = omit, + num_preemptible_gpus: int | Omit = omit, + num_reserved_gpus: int | Omit = omit, + oidc_config: cluster_create_params.OidcConfig | Omit = omit, + project_id: str | Omit = omit, reservation_end_time: Union[str, datetime] | Omit = omit, reservation_start_time: Union[str, datetime] | Omit = omit, shared_volume: cluster_create_params.SharedVolume | Omit = omit, @@ -432,6 +520,8 @@ async def create( cuda_version: CUDA version for this cluster. For example, 12.5 + duration_days: Duration in days to keep the cluster running. + gpu_type: Type of GPU to use in the cluster num_gpus: Number of GPUs to allocate in the cluster. This must be multiple of 8. For @@ -443,6 +533,15 @@ async def create( region: Region to create the GPU cluster in. Usable regions can be found from `client.clusters.list_regions()` + acceptance_tests_params: AcceptanceTestsParams groups all GPU acceptance test options when enabled is + true. + + add_ons: Add-ons to enable on the cluster at creation time. + + auto_scale: Whether to enable auto-scaling for the cluster. If true, the cluster will + automatically scale the number of GPU worker nodes between num_gpus and + auto_scale_max_gpus based on the workload. + auto_scale_max_gpus: Maximum number of GPUs to which the cluster can be auto-scaled up. This field is required if auto_scaled is true. @@ -454,14 +553,26 @@ async def create( cluster_type: Type of cluster to create. - duration_days: Duration in days to keep the cluster running. - gpu_node_failover_enabled: Whether automated GPU node failover should be enabled for this cluster. By default, it is disabled. install_traefik: Whether to install Traefik ingress controller in the cluster. This field is only applicable for Kubernetes clusters and is false by default. + num_capacity_pool_gpus: Number of GPUs to allocate from the capacity pool. Must be a multiple of 8 and + not exceed num_gpus. + + num_preemptible_gpus: Number of preemptible GPUs to request alongside on-demand capacity. Must be a + multiple of 8. Preemptible nodes are cheaper but may be reclaimed when on-demand + capacity is needed elsewhere; the system fulfills this asynchronously and + surfaces the actual count in allocated_preemptible_gpus. + + num_reserved_gpus: Number of prepaid (PLG) reserved GPUs for this cluster. When omitted for + RESERVED billing on create, the server defaults this to num_gpus. + + project_id: Project ID for the cluster. If not set, the project from the request context is + used. + reservation_end_time: Reservation end time of the cluster. This field is required for SCHEDULED billing to specify the reservation end time for the cluster. @@ -493,17 +604,26 @@ async def create( "billing_type": billing_type, "cluster_name": cluster_name, "cuda_version": cuda_version, + "duration_days": duration_days, "gpu_type": gpu_type, "num_gpus": num_gpus, "nvidia_driver_version": nvidia_driver_version, "region": region, + "acceptance_tests_params": acceptance_tests_params, + "add_ons": add_ons, + "auto_scale": auto_scale, "auto_scale_max_gpus": auto_scale_max_gpus, "auto_scaled": auto_scaled, "capacity_pool_id": capacity_pool_id, + "cluster_config": cluster_config, "cluster_type": cluster_type, - "duration_days": duration_days, "gpu_node_failover_enabled": gpu_node_failover_enabled, "install_traefik": install_traefik, + "num_capacity_pool_gpus": num_capacity_pool_gpus, + "num_preemptible_gpus": num_preemptible_gpus, + "num_reserved_gpus": num_reserved_gpus, + "oidc_config": oidc_config, + "project_id": project_id, "reservation_end_time": reservation_end_time, "reservation_start_time": reservation_start_time, "shared_volume": shared_volume, @@ -558,8 +678,12 @@ async def update( self, cluster_id: str, *, + add_ons: Iterable[cluster_update_params.AddOn] | Omit = omit, + cluster_config: cluster_update_params.ClusterConfig | Omit = omit, cluster_type: Literal["KUBERNETES", "SLURM"] | Omit = omit, num_gpus: int | Omit = omit, + num_preemptible_gpus: int | Omit = omit, + num_reserved_gpus: int | Omit = omit, reservation_end_time: Union[str, datetime] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -574,10 +698,20 @@ async def update( Args: cluster_id: The ID of the cluster to update + add_ons: Add-ons to update on the cluster. Each entry identifies an existing add-on by + name and provides the new external config to merge. + cluster_type: Type of cluster to update. - num_gpus: Number of GPUs to allocate in the cluster. This must be multiple of 8. For - example, 8, 16 or 24 + num_gpus: Target GPU count for the cluster. When omitted, the server keeps the current GPU + count from cluster metadata (use for config-only or decommission-time-only + updates). + + num_preemptible_gpus: Updated desired number of preemptible GPUs for the cluster. When omitted, the + current value is preserved. Must be a multiple of 8. + + num_reserved_gpus: Number of reserved GPUs to update to. This field is only applicable for clusters + with RESERVED billing type. reservation_end_time: Timestamp at which the cluster should be decommissioned. Only accepted for prepaid clusters. @@ -596,8 +730,12 @@ async def update( path_template("/compute/clusters/{cluster_id}", cluster_id=cluster_id), body=await async_maybe_transform( { + "add_ons": add_ons, + "cluster_config": cluster_config, "cluster_type": cluster_type, "num_gpus": num_gpus, + "num_preemptible_gpus": num_preemptible_gpus, + "num_reserved_gpus": num_reserved_gpus, "reservation_end_time": reservation_end_time, }, cluster_update_params.ClusterUpdateParams, @@ -611,6 +749,7 @@ async def update( async def list( self, *, + project_id: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -618,11 +757,30 @@ async def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ClusterListResponse: - """List all GPU clusters.""" + """ + List all GPU clusters. + + Args: + project_id: Optional UMS project ID to filter clusters by. When set, only clusters belonging + to this project are returned. The caller must be a member of the project; + otherwise the result set will be empty. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ return await self._get( "/compute/clusters", options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform({"project_id": project_id}, cluster_list_params.ClusterListParams), ), cast_to=ClusterListResponse, ) diff --git a/src/together/resources/beta/clusters/storage.py b/src/together/resources/beta/clusters/storage.py index c6abf44e8..c295061f3 100644 --- a/src/together/resources/beta/clusters/storage.py +++ b/src/together/resources/beta/clusters/storage.py @@ -15,7 +15,7 @@ async_to_streamed_response_wrapper, ) from ...._base_client import make_request_options -from ....types.beta.clusters import storage_create_params, storage_update_params +from ....types.beta.clusters import storage_list_params, storage_create_params, storage_update_params from ....types.beta.clusters.cluster_storage import ClusterStorage from ....types.beta.clusters.storage_list_response import StorageListResponse from ....types.beta.clusters.storage_delete_response import StorageDeleteResponse @@ -49,6 +49,7 @@ def create( region: str, size_tib: int, volume_name: str, + is_lifecycle_independent: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -64,11 +65,9 @@ def create( performance for shared storage. Args: - region: Region name. Usable regions can be found from `client.clusters.list_regions()` - size_tib: Volume size in whole tebibytes (TiB). - volume_name: Customizable name of the volume to create. + is_lifecycle_independent: When true, the shared volume is not deleted when the cluster is decommissioned. extra_headers: Send extra headers @@ -85,6 +84,7 @@ def create( "region": region, "size_tib": size_tib, "volume_name": volume_name, + "is_lifecycle_independent": is_lifecycle_independent, }, storage_create_params.StorageCreateParams, ), @@ -132,8 +132,8 @@ def retrieve( def update( self, *, - size_tib: int | Omit = omit, - volume_id: str | Omit = omit, + size_tib: int, + volume_id: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -145,10 +145,6 @@ def update( Update the configuration of an existing shared volume. Args: - size_tib: Size of the volume in whole tebibytes (TiB). - - volume_id: ID of the volume to update. - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -175,6 +171,7 @@ def update( def list( self, *, + project_id: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -182,11 +179,30 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> StorageListResponse: - """List all shared volumes.""" + """ + List all shared volumes. + + Args: + project_id: Optional UMS project ID to filter volumes by. When set, only volumes belonging + to this project are returned. The caller must be a member of the project; + otherwise the result set will be empty. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ return self._get( "/compute/clusters/storage/volumes", options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"project_id": project_id}, storage_list_params.StorageListParams), ), cast_to=StorageListResponse, ) @@ -255,6 +271,7 @@ async def create( region: str, size_tib: int, volume_name: str, + is_lifecycle_independent: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -270,11 +287,9 @@ async def create( performance for shared storage. Args: - region: Region name. Usable regions can be found from `client.clusters.list_regions()` - size_tib: Volume size in whole tebibytes (TiB). - volume_name: Customizable name of the volume to create. + is_lifecycle_independent: When true, the shared volume is not deleted when the cluster is decommissioned. extra_headers: Send extra headers @@ -291,6 +306,7 @@ async def create( "region": region, "size_tib": size_tib, "volume_name": volume_name, + "is_lifecycle_independent": is_lifecycle_independent, }, storage_create_params.StorageCreateParams, ), @@ -338,8 +354,8 @@ async def retrieve( async def update( self, *, - size_tib: int | Omit = omit, - volume_id: str | Omit = omit, + size_tib: int, + volume_id: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -351,10 +367,6 @@ async def update( Update the configuration of an existing shared volume. Args: - size_tib: Size of the volume in whole tebibytes (TiB). - - volume_id: ID of the volume to update. - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -381,6 +393,7 @@ async def update( async def list( self, *, + project_id: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -388,11 +401,30 @@ async def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> StorageListResponse: - """List all shared volumes.""" + """ + List all shared volumes. + + Args: + project_id: Optional UMS project ID to filter volumes by. When set, only volumes belonging + to this project are returned. The caller must be a member of the project; + otherwise the result set will be empty. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ return await self._get( "/compute/clusters/storage/volumes", options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform({"project_id": project_id}, storage_list_params.StorageListParams), ), cast_to=StorageListResponse, ) diff --git a/src/together/types/beta/__init__.py b/src/together/types/beta/__init__.py index ab66b141e..e7c1d8d94 100644 --- a/src/together/types/beta/__init__.py +++ b/src/together/types/beta/__init__.py @@ -8,6 +8,7 @@ from .jig_deploy_params import JigDeployParams as JigDeployParams from .jig_list_response import JigListResponse as JigListResponse from .jig_update_params import JigUpdateParams as JigUpdateParams +from .cluster_list_params import ClusterListParams as ClusterListParams from .cluster_create_params import ClusterCreateParams as ClusterCreateParams from .cluster_list_response import ClusterListResponse as ClusterListResponse from .cluster_update_params import ClusterUpdateParams as ClusterUpdateParams diff --git a/src/together/types/beta/cluster.py b/src/together/types/beta/cluster.py index 9d5f254c7..400108cfb 100644 --- a/src/together/types/beta/cluster.py +++ b/src/together/types/beta/cluster.py @@ -6,7 +6,85 @@ from ..._models import BaseModel -__all__ = ["Cluster", "ControlPlaneNode", "GPUWorkerNode", "Volume"] +__all__ = [ + "Cluster", + "AddOn", + "AddOnConfig", + "AddOnConfigDashboard", + "AddOnConfigIngress", + "AddOnState", + "AddOnStateDashboard", + "AddOnStateIngress", + "ControlPlaneNode", + "ControlPlaneNodePhaseTransition", + "GPUWorkerNode", + "GPUWorkerNodePhaseTransition", + "GPUWorkerNodeLatestRemediation", + "PhaseTransition", + "Volume", + "ClusterConfig", + "ClusterConfigIngress", + "ClusterConfigObservability", + "ClusterConfigSlurmStartupScripts", + "OidcConfig", +] + + +class AddOnConfigDashboard(BaseModel): + enabled: Optional[bool] = None + + +class AddOnConfigIngress(BaseModel): + enabled: Optional[bool] = None + + +class AddOnConfig(BaseModel): + dashboard: Optional[AddOnConfigDashboard] = None + + ingress: Optional[AddOnConfigIngress] = None + + +class AddOnStateDashboard(BaseModel): + pass + + +class AddOnStateIngress(BaseModel): + pass + + +class AddOnState(BaseModel): + dashboard: Optional[AddOnStateDashboard] = None + + ingress: Optional[AddOnStateIngress] = None + + +class AddOn(BaseModel): + """AddOnInfo is returned in cluster responses and add-on CRUD operations.""" + + add_on_type: str + + config: AddOnConfig + + name: str + + state: AddOnState + + +class ControlPlaneNodePhaseTransition(BaseModel): + phase: Literal[ + "NODE_PHASE_PENDING", + "NODE_PHASE_SCHEDULING", + "NODE_PHASE_BOOTING", + "NODE_PHASE_BOOTSTRAPPING", + "NODE_PHASE_RUNNING", + "NODE_PHASE_SUCCEEDED", + "NODE_PHASE_FAILED", + "NODE_PHASE_PAUSED", + ] + """Node phase.""" + + transition_time: datetime + """Timestamp when the phase transition occurred.""" class ControlPlaneNode(BaseModel): @@ -22,9 +100,114 @@ class ControlPlaneNode(BaseModel): num_cpu_cores: int + phase_transitions: List[ControlPlaneNodePhaseTransition] + """Phase transition history for this control plane node.""" + status: str +class GPUWorkerNodePhaseTransition(BaseModel): + phase: Literal[ + "NODE_PHASE_PENDING", + "NODE_PHASE_SCHEDULING", + "NODE_PHASE_BOOTING", + "NODE_PHASE_BOOTSTRAPPING", + "NODE_PHASE_RUNNING", + "NODE_PHASE_SUCCEEDED", + "NODE_PHASE_FAILED", + "NODE_PHASE_PAUSED", + ] + """Node phase.""" + + transition_time: datetime + """Timestamp when the phase transition occurred.""" + + +class GPUWorkerNodeLatestRemediation(BaseModel): + """ + Remediation represents a node remediation request for an instance. + An instance can have multiple remediations over time (e.g., failed attempts followed by retries). + """ + + id: str + + cluster_id: str + + instance_id: str + + mode: Literal[ + "REMEDIATION_MODE_VM_ONLY", + "REMEDIATION_MODE_HOST_AWARE", + "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT", + "REMEDIATION_MODE_REBOOT_VM", + ] + """Remediation mode specifies how the remediation should be performed. + + - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any + available host. + - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and + provisions a new one on a different host. + """ + + state: Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"] + """RemediationState represents the lifecycle state of a remediation. + + - `PENDING_APPROVAL`: Awaiting approval before processing can begin. + - `PENDING`: Approved and queued for processing. + - `RUNNING`: Actively being processed. + - `SUCCEEDED`: Successfully completed. + - `FAILED`: Failed with an error. + - `CANCELLED`: Cancelled by user or system. + - `AUTO_RESOLVED`: The underlying issue was automatically resolved before + processing. + """ + + trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"] + """RemediationTrigger specifies how the remediation was triggered. + + - `REMEDIATION_TRIGGER_MANUAL`: A user-initiated remediation (either via web UI + or API call). + - `REMEDIATION_TRIGGER_AUTOMATED`: A system-initiated remediation that requires + approval. + """ + + active_health_check_run_id: Optional[str] = None + """Active health check run ID (UUID) that triggered this remediation.""" + + create_time: Optional[datetime] = None + """When the remediation was created.""" + + end_time: Optional[datetime] = None + """When the remediation completed.""" + + error_message: Optional[str] = None + """Error message if the remediation failed.""" + + passive_health_check_event_id: Optional[str] = None + """Passive health check event ID that triggered this remediation.""" + + reason: Optional[str] = None + """User-provided reason for the remediation.""" + + requested_by: Optional[str] = None + """Who requested the remediation.""" + + review_comment: Optional[str] = None + """Review comment.""" + + review_time: Optional[datetime] = None + """When the remediation was reviewed.""" + + reviewed_by: Optional[str] = None + """Who reviewed the remediation.""" + + start_time: Optional[datetime] = None + """When processing started.""" + + update_time: Optional[datetime] = None + """When the remediation was last updated.""" + + class GPUWorkerNode(BaseModel): host_name: str @@ -40,10 +223,48 @@ class GPUWorkerNode(BaseModel): num_gpus: int + phase_transitions: List[GPUWorkerNodePhaseTransition] + """Phase transition history for this GPU worker node.""" + status: str instance_id: Optional[str] = None + latest_remediation: Optional[GPUWorkerNodeLatestRemediation] = None + """ + Remediation represents a node remediation request for an instance. An instance + can have multiple remediations over time (e.g., failed attempts followed by + retries). + """ + + slurm_worker_hostname: Optional[str] = None + + +class PhaseTransition(BaseModel): + phase: Literal[ + "CLUSTER_PHASE_QUEUED", + "CLUSTER_PHASE_SCHEDULED", + "CLUSTER_PHASE_WAITING_FOR_CONTROL_PLANE_NODES", + "CLUSTER_PHASE_WAITING_FOR_DATA_PLANE_NODES", + "CLUSTER_PHASE_WAITING_FOR_SUBNET", + "CLUSTER_PHASE_WAITING_FOR_SHARED_VOLUME", + "CLUSTER_PHASE_WAITING_FOR_AUTO_SCALER", + "CLUSTER_PHASE_INSTALLING_DRIVERS", + "CLUSTER_PHASE_RUNNING_ACCEPTANCE_TESTS", + "CLUSTER_PHASE_ACCEPTANCE_TESTS_FAILED", + "CLUSTER_PHASE_RUNNING_NCCL_TESTS", + "CLUSTER_PHASE_NCCL_TESTS_FAILED", + "CLUSTER_PHASE_READY", + "CLUSTER_PHASE_PAUSED", + "CLUSTER_PHASE_ON_DEMAND_COMPUTE_PAUSED", + "CLUSTER_PHASE_DEGRADED", + "CLUSTER_PHASE_DELETING", + ] + """Cluster phase.""" + + transition_time: datetime + """Timestamp when the phase transition occurred.""" + class Volume(BaseModel): size_tib: int @@ -55,7 +276,115 @@ class Volume(BaseModel): volume_name: str +class ClusterConfigIngress(BaseModel): + enabled: Optional[bool] = None + + +class ClusterConfigObservability(BaseModel): + enabled: Optional[bool] = None + + +class ClusterConfigSlurmStartupScripts(BaseModel): + """ + SlurmStartupScripts carries optional Slurm lifecycle scripts (prolog/epilog, init, extra conf). + """ + + controller_epilog: Optional[str] = None + """Slurm controller epilog script.""" + + controller_prolog: Optional[str] = None + """Slurm controller prolog script.""" + + extra_slurm_conf: Optional[str] = None + """Additional slurm.conf fragments.""" + + login_init_script: Optional[str] = None + """Script run on Slurm login node init.""" + + nodeset_init_script: Optional[str] = None + """Script run on Slurm nodeset init.""" + + worker_epilog: Optional[str] = None + """Slurm worker node epilog script.""" + + worker_prolog: Optional[str] = None + """Slurm worker node prolog script.""" + + +class ClusterConfig(BaseModel): + load_balancer: Literal["NONE", "TRAEFIK", "NGINX", "ISTIO"] + + gpu_operator_version: Optional[str] = None + """NVIDIA GPU Operator chart/version for the tenant cluster (e.g. + + v24.6.2). When omitted, a service default is applied. + """ + + ingress: Optional[ClusterConfigIngress] = None + + jumphost_enabled: Optional[bool] = None + + kubernetes_dashboard_enabled: Optional[bool] = None + + observability: Optional[ClusterConfigObservability] = None + + slurm_startup_scripts: Optional[ClusterConfigSlurmStartupScripts] = None + """ + SlurmStartupScripts carries optional Slurm lifecycle scripts (prolog/epilog, + init, extra conf). + """ + + +class OidcConfig(BaseModel): + client_id: str + """OIDC client ID for authentication.""" + + group_claim: str + """JWT claim to use for user groups. For example, 'groups'""" + + group_prefix: str + """Prefix to add to the group claim to form the final group name. + + For example, 'oidc:' + """ + + issuer_url: str + """OIDC issuer URL for authentication. For example, https://accounts.google.com""" + + username_claim: str + """JWT claim to use as the username. For example, 'sub' or 'email'""" + + username_prefix: str + """Prefix to add to the username claim to form the final username. + + For example, 'oidc:' + """ + + ca_cert: Optional[str] = None + """CA certificate in PEM format to validate the OIDC issuer's TLS certificate. + + This field is optional but recommended if the issuer uses a private CA or + self-signed certificate. + """ + + class Cluster(BaseModel): + add_ons: List[AddOn] + """Enabled add-ons on this cluster. + + Only add-ons with enabled=true in their config are returned. + """ + + allocated_preemptible_gpus: int + """Actual number of preemptible GPUs currently allocated to the cluster. + + Updated asynchronously by the fulfillment and reclamation workers; may be less + than desired_preemptible_gpus when capacity is constrained. + """ + + billing_type: Literal["RESERVED", "ON_DEMAND", "SCHEDULED_CAPACITY"] + """Billing type for the cluster (RESERVED, ON_DEMAND, or SCHEDULED_CAPACITY).""" + cluster_id: str cluster_name: str @@ -67,16 +396,30 @@ class Cluster(BaseModel): cuda_version: str + desired_preemptible_gpus: int + """Customer's requested number of preemptible GPUs. + + Set on cluster create or update; persists until changed. + """ + gpu_type: Literal["H100_SXM", "H200_SXM", "RTX_6000_PCI", "L40_PCIE", "B200_SXM", "H100_SXM_INF"] gpu_worker_nodes: List[GPUWorkerNode] kube_config: str + num_cpu_workers: int + """Number of CPU-only worker nodes in the cluster.""" + num_gpus: int nvidia_driver_version: str + phase_transitions: List[PhaseTransition] + """Cluster-level phase transition history.""" + + project_id: str + region: str status: Literal[ @@ -98,12 +441,16 @@ class Cluster(BaseModel): capacity_pool_id: Optional[str] = None + cluster_config: Optional[ClusterConfig] = None + created_at: Optional[datetime] = None duration_hours: Optional[int] = None install_traefik: Optional[bool] = None + oidc_config: Optional[OidcConfig] = None + reservation_end_time: Optional[datetime] = None reservation_start_time: Optional[datetime] = None diff --git a/src/together/types/beta/cluster_create_params.py b/src/together/types/beta/cluster_create_params.py index e961052b7..4d9482da5 100644 --- a/src/together/types/beta/cluster_create_params.py +++ b/src/together/types/beta/cluster_create_params.py @@ -2,13 +2,26 @@ from __future__ import annotations -from typing import Union +from typing import Union, Iterable from datetime import datetime from typing_extensions import Literal, Required, Annotated, TypedDict from ..._utils import PropertyInfo -__all__ = ["ClusterCreateParams", "SharedVolume"] +__all__ = [ + "ClusterCreateParams", + "AcceptanceTestsParams", + "AddOn", + "AddOnConfig", + "AddOnConfigDashboard", + "AddOnConfigIngress", + "ClusterConfig", + "ClusterConfigIngress", + "ClusterConfigObservability", + "ClusterConfigSlurmStartupScripts", + "OidcConfig", + "SharedVolume", +] class ClusterCreateParams(TypedDict, total=False): @@ -27,6 +40,9 @@ class ClusterCreateParams(TypedDict, total=False): cuda_version: Required[str] """CUDA version for this cluster. For example, 12.5""" + duration_days: Required[int] + """Duration in days to keep the cluster running.""" + gpu_type: Required[Literal["H100_SXM", "H200_SXM", "RTX_6000_PCI", "L40_PCIE", "B200_SXM", "H100_SXM_INF"]] """Type of GPU to use in the cluster""" @@ -49,6 +65,22 @@ class ClusterCreateParams(TypedDict, total=False): Usable regions can be found from `client.clusters.list_regions()` """ + acceptance_tests_params: AcceptanceTestsParams + """ + AcceptanceTestsParams groups all GPU acceptance test options when enabled is + true. + """ + + add_ons: Iterable[AddOn] + """Add-ons to enable on the cluster at creation time.""" + + auto_scale: bool + """Whether to enable auto-scaling for the cluster. + + If true, the cluster will automatically scale the number of GPU worker nodes + between num_gpus and auto_scale_max_gpus based on the workload. + """ + auto_scale_max_gpus: int """Maximum number of GPUs to which the cluster can be auto-scaled up. @@ -68,12 +100,11 @@ class ClusterCreateParams(TypedDict, total=False): capacity pool. """ + cluster_config: ClusterConfig + cluster_type: Literal["KUBERNETES", "SLURM"] """Type of cluster to create.""" - duration_days: int - """Duration in days to keep the cluster running.""" - gpu_node_failover_enabled: bool """Whether automated GPU node failover should be enabled for this cluster. @@ -86,6 +117,35 @@ class ClusterCreateParams(TypedDict, total=False): This field is only applicable for Kubernetes clusters and is false by default. """ + num_capacity_pool_gpus: int + """Number of GPUs to allocate from the capacity pool. + + Must be a multiple of 8 and not exceed num_gpus. + """ + + num_preemptible_gpus: int + """Number of preemptible GPUs to request alongside on-demand capacity. + + Must be a multiple of 8. Preemptible nodes are cheaper but may be reclaimed when + on-demand capacity is needed elsewhere; the system fulfills this asynchronously + and surfaces the actual count in allocated_preemptible_gpus. + """ + + num_reserved_gpus: int + """Number of prepaid (PLG) reserved GPUs for this cluster. + + When omitted for RESERVED billing on create, the server defaults this to + num_gpus. + """ + + oidc_config: OidcConfig + + project_id: str + """Project ID for the cluster. + + If not set, the project from the request context is used. + """ + reservation_end_time: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] """Reservation end time of the cluster. @@ -116,14 +176,164 @@ class ClusterCreateParams(TypedDict, total=False): """ID of an existing volume to use with the cluster creation.""" +class AcceptanceTestsParams(TypedDict, total=False): + """ + AcceptanceTestsParams groups all GPU acceptance test options when enabled is true. + """ + + dcgm_diag_level: Literal[ + "DCGM_DIAG_LEVEL_SHORT", "DCGM_DIAG_LEVEL_MEDIUM", "DCGM_DIAG_LEVEL_LONG", "DCGM_DIAG_LEVEL_EXTENDED" + ] + """DCGM diagnostic depth. + + SHORT = readiness; MEDIUM = default; LONG = system validation; EXTENDED = + memtest. An omitted value selects MEDIUM when enabled. + """ + + dcgm_diag_skipped: bool + """Skip DCGM diagnostics acceptance test.""" + + enabled: bool + """Whether to run GPU acceptance tests during cluster bring-up.""" + + gpu_burn_duration: int + """GPU burn duration in seconds; 0 means use the default when enabled.""" + + gpu_burn_skipped: bool + """Skip GPU burn acceptance test.""" + + nccl_multi_node_skipped: bool + """Skip NCCL multi-node acceptance test.""" + + nccl_single_node_skipped: bool + """Skip NCCL single-node acceptance test.""" + + +class AddOnConfigDashboard(TypedDict, total=False): + enabled: bool + + +class AddOnConfigIngress(TypedDict, total=False): + enabled: bool + + +class AddOnConfig(TypedDict, total=False): + dashboard: AddOnConfigDashboard + + ingress: AddOnConfigIngress + + +class AddOn(TypedDict, total=False): + add_on_type: Required[str] + """Type of add-on. Valid values: 'dashboard', 'ingress'.""" + + name: Required[str] + """Human-readable name for this add-on instance.""" + + config: AddOnConfig + + +class ClusterConfigIngress(TypedDict, total=False): + enabled: bool + + +class ClusterConfigObservability(TypedDict, total=False): + enabled: bool + + +class ClusterConfigSlurmStartupScripts(TypedDict, total=False): + """ + SlurmStartupScripts carries optional Slurm lifecycle scripts (prolog/epilog, init, extra conf). + """ + + controller_epilog: str + """Slurm controller epilog script.""" + + controller_prolog: str + """Slurm controller prolog script.""" + + extra_slurm_conf: str + """Additional slurm.conf fragments.""" + + login_init_script: str + """Script run on Slurm login node init.""" + + nodeset_init_script: str + """Script run on Slurm nodeset init.""" + + worker_epilog: str + """Slurm worker node epilog script.""" + + worker_prolog: str + """Slurm worker node prolog script.""" + + +class ClusterConfig(TypedDict, total=False): + load_balancer: Required[Literal["NONE", "TRAEFIK", "NGINX", "ISTIO"]] + + gpu_operator_version: str + """NVIDIA GPU Operator chart/version for the tenant cluster (e.g. + + v24.6.2). When omitted, a service default is applied. + """ + + ingress: ClusterConfigIngress + + jumphost_enabled: bool + + kubernetes_dashboard_enabled: bool + + observability: ClusterConfigObservability + + slurm_startup_scripts: ClusterConfigSlurmStartupScripts + """ + SlurmStartupScripts carries optional Slurm lifecycle scripts (prolog/epilog, + init, extra conf). + """ + + +class OidcConfig(TypedDict, total=False): + client_id: Required[str] + """OIDC client ID for authentication.""" + + group_claim: Required[str] + """JWT claim to use for user groups. For example, 'groups'""" + + group_prefix: Required[str] + """Prefix to add to the group claim to form the final group name. + + For example, 'oidc:' + """ + + issuer_url: Required[str] + """OIDC issuer URL for authentication. For example, https://accounts.google.com""" + + username_claim: Required[str] + """JWT claim to use as the username. For example, 'sub' or 'email'""" + + username_prefix: Required[str] + """Prefix to add to the username claim to form the final username. + + For example, 'oidc:' + """ + + ca_cert: str + """CA certificate in PEM format to validate the OIDC issuer's TLS certificate. + + This field is optional but recommended if the issuer uses a private CA or + self-signed certificate. + """ + + class SharedVolume(TypedDict, total=False): """Inline configuration to create a shared volume with the cluster creation.""" region: Required[str] - """Region name. Usable regions can be found from `client.clusters.list_regions()`""" size_tib: Required[int] """Volume size in whole tebibytes (TiB).""" volume_name: Required[str] - """Customizable name of the volume to create.""" + + is_lifecycle_independent: bool + """When true, the shared volume is not deleted when the cluster is decommissioned.""" diff --git a/src/together/types/beta/cluster_list_params.py b/src/together/types/beta/cluster_list_params.py new file mode 100644 index 000000000..69fa8e9e4 --- /dev/null +++ b/src/together/types/beta/cluster_list_params.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["ClusterListParams"] + + +class ClusterListParams(TypedDict, total=False): + project_id: str + """Optional UMS project ID to filter clusters by. + + When set, only clusters belonging to this project are returned. The caller must + be a member of the project; otherwise the result set will be empty. + """ diff --git a/src/together/types/beta/cluster_update_params.py b/src/together/types/beta/cluster_update_params.py index a98f77653..46cf388aa 100644 --- a/src/together/types/beta/cluster_update_params.py +++ b/src/together/types/beta/cluster_update_params.py @@ -2,23 +2,55 @@ from __future__ import annotations -from typing import Union +from typing import Union, Iterable from datetime import datetime -from typing_extensions import Literal, Annotated, TypedDict +from typing_extensions import Literal, Required, Annotated, TypedDict from ..._utils import PropertyInfo -__all__ = ["ClusterUpdateParams"] +__all__ = [ + "ClusterUpdateParams", + "AddOn", + "AddOnConfig", + "AddOnConfigDashboard", + "AddOnConfigIngress", + "ClusterConfig", + "ClusterConfigIngress", + "ClusterConfigObservability", + "ClusterConfigSlurmStartupScripts", +] class ClusterUpdateParams(TypedDict, total=False): + add_ons: Iterable[AddOn] + """Add-ons to update on the cluster. + + Each entry identifies an existing add-on by name and provides the new external + config to merge. + """ + + cluster_config: ClusterConfig + cluster_type: Literal["KUBERNETES", "SLURM"] """Type of cluster to update.""" num_gpus: int - """Number of GPUs to allocate in the cluster. + """Target GPU count for the cluster. + + When omitted, the server keeps the current GPU count from cluster metadata (use + for config-only or decommission-time-only updates). + """ - This must be multiple of 8. For example, 8, 16 or 24 + num_preemptible_gpus: int + """Updated desired number of preemptible GPUs for the cluster. + + When omitted, the current value is preserved. Must be a multiple of 8. + """ + + num_reserved_gpus: int + """Number of reserved GPUs to update to. + + This field is only applicable for clusters with RESERVED billing type. """ reservation_end_time: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] @@ -26,3 +58,83 @@ class ClusterUpdateParams(TypedDict, total=False): Only accepted for prepaid clusters. """ + + +class AddOnConfigDashboard(TypedDict, total=False): + enabled: bool + + +class AddOnConfigIngress(TypedDict, total=False): + enabled: bool + + +class AddOnConfig(TypedDict, total=False): + dashboard: AddOnConfigDashboard + + ingress: AddOnConfigIngress + + +class AddOn(TypedDict, total=False): + name: Required[str] + """Name of the add-on to update. Must match an existing add-on on the cluster.""" + + config: AddOnConfig + + +class ClusterConfigIngress(TypedDict, total=False): + enabled: bool + + +class ClusterConfigObservability(TypedDict, total=False): + enabled: bool + + +class ClusterConfigSlurmStartupScripts(TypedDict, total=False): + """ + SlurmStartupScripts carries optional Slurm lifecycle scripts (prolog/epilog, init, extra conf). + """ + + controller_epilog: str + """Slurm controller epilog script.""" + + controller_prolog: str + """Slurm controller prolog script.""" + + extra_slurm_conf: str + """Additional slurm.conf fragments.""" + + login_init_script: str + """Script run on Slurm login node init.""" + + nodeset_init_script: str + """Script run on Slurm nodeset init.""" + + worker_epilog: str + """Slurm worker node epilog script.""" + + worker_prolog: str + """Slurm worker node prolog script.""" + + +class ClusterConfig(TypedDict, total=False): + load_balancer: Required[Literal["NONE", "TRAEFIK", "NGINX", "ISTIO"]] + + gpu_operator_version: str + """NVIDIA GPU Operator chart/version for the tenant cluster (e.g. + + v24.6.2). When omitted, a service default is applied. + """ + + ingress: ClusterConfigIngress + + jumphost_enabled: bool + + kubernetes_dashboard_enabled: bool + + observability: ClusterConfigObservability + + slurm_startup_scripts: ClusterConfigSlurmStartupScripts + """ + SlurmStartupScripts carries optional Slurm lifecycle scripts (prolog/epilog, + init, extra conf). + """ diff --git a/src/together/types/beta/clusters/__init__.py b/src/together/types/beta/clusters/__init__.py index a85f4c49a..9c4f18a76 100644 --- a/src/together/types/beta/clusters/__init__.py +++ b/src/together/types/beta/clusters/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from .cluster_storage import ClusterStorage as ClusterStorage +from .storage_list_params import StorageListParams as StorageListParams from .storage_create_params import StorageCreateParams as StorageCreateParams from .storage_list_response import StorageListResponse as StorageListResponse from .storage_update_params import StorageUpdateParams as StorageUpdateParams diff --git a/src/together/types/beta/clusters/cluster_storage.py b/src/together/types/beta/clusters/cluster_storage.py index 6d7a0bfe4..22392753f 100644 --- a/src/together/types/beta/clusters/cluster_storage.py +++ b/src/together/types/beta/clusters/cluster_storage.py @@ -1,7 +1,5 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing_extensions import Literal - from ...._models import BaseModel __all__ = ["ClusterStorage"] @@ -9,13 +7,9 @@ class ClusterStorage(BaseModel): size_tib: int - """Size of the volume in whole tebibytes (TiB).""" - status: Literal["available", "bound", "provisioning"] - """Deployment status of the volume.""" + status: str volume_id: str - """ID of the volume.""" volume_name: str - """Provided name of the volume.""" diff --git a/src/together/types/beta/clusters/storage_create_params.py b/src/together/types/beta/clusters/storage_create_params.py index 5629cb113..2dfb9218f 100644 --- a/src/together/types/beta/clusters/storage_create_params.py +++ b/src/together/types/beta/clusters/storage_create_params.py @@ -9,10 +9,11 @@ class StorageCreateParams(TypedDict, total=False): region: Required[str] - """Region name. Usable regions can be found from `client.clusters.list_regions()`""" size_tib: Required[int] """Volume size in whole tebibytes (TiB).""" volume_name: Required[str] - """Customizable name of the volume to create.""" + + is_lifecycle_independent: bool + """When true, the shared volume is not deleted when the cluster is decommissioned.""" diff --git a/src/together/types/beta/clusters/storage_list_params.py b/src/together/types/beta/clusters/storage_list_params.py new file mode 100644 index 000000000..2dddfca79 --- /dev/null +++ b/src/together/types/beta/clusters/storage_list_params.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["StorageListParams"] + + +class StorageListParams(TypedDict, total=False): + project_id: str + """Optional UMS project ID to filter volumes by. + + When set, only volumes belonging to this project are returned. The caller must + be a member of the project; otherwise the result set will be empty. + """ diff --git a/src/together/types/beta/clusters/storage_update_params.py b/src/together/types/beta/clusters/storage_update_params.py index 449a62661..c87e4cd61 100644 --- a/src/together/types/beta/clusters/storage_update_params.py +++ b/src/together/types/beta/clusters/storage_update_params.py @@ -2,14 +2,12 @@ from __future__ import annotations -from typing_extensions import TypedDict +from typing_extensions import Required, TypedDict __all__ = ["StorageUpdateParams"] class StorageUpdateParams(TypedDict, total=False): - size_tib: int - """Size of the volume in whole tebibytes (TiB).""" + size_tib: Required[int] - volume_id: str - """ID of the volume to update.""" + volume_id: Required[str] diff --git a/tests/api_resources/beta/clusters/test_storage.py b/tests/api_resources/beta/clusters/test_storage.py index 7a78f0d7a..8403af1ae 100644 --- a/tests/api_resources/beta/clusters/test_storage.py +++ b/tests/api_resources/beta/clusters/test_storage.py @@ -30,6 +30,16 @@ def test_method_create(self, client: Together) -> None: ) assert_matches_type(ClusterStorage, storage, path=["response"]) + @parametrize + def test_method_create_with_all_params(self, client: Together) -> None: + storage = client.beta.clusters.storage.create( + region="region", + size_tib=0, + volume_name="volume_name", + is_lifecycle_independent=True, + ) + assert_matches_type(ClusterStorage, storage, path=["response"]) + @parametrize def test_raw_response_create(self, client: Together) -> None: response = client.beta.clusters.storage.with_raw_response.create( @@ -98,11 +108,6 @@ def test_path_params_retrieve(self, client: Together) -> None: @parametrize def test_method_update(self, client: Together) -> None: - storage = client.beta.clusters.storage.update() - assert_matches_type(ClusterStorage, storage, path=["response"]) - - @parametrize - def test_method_update_with_all_params(self, client: Together) -> None: storage = client.beta.clusters.storage.update( size_tib=0, volume_id="volume_id", @@ -111,7 +116,10 @@ def test_method_update_with_all_params(self, client: Together) -> None: @parametrize def test_raw_response_update(self, client: Together) -> None: - response = client.beta.clusters.storage.with_raw_response.update() + response = client.beta.clusters.storage.with_raw_response.update( + size_tib=0, + volume_id="volume_id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -120,7 +128,10 @@ def test_raw_response_update(self, client: Together) -> None: @parametrize def test_streaming_response_update(self, client: Together) -> None: - with client.beta.clusters.storage.with_streaming_response.update() as response: + with client.beta.clusters.storage.with_streaming_response.update( + size_tib=0, + volume_id="volume_id", + ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -134,6 +145,13 @@ def test_method_list(self, client: Together) -> None: storage = client.beta.clusters.storage.list() assert_matches_type(StorageListResponse, storage, path=["response"]) + @parametrize + def test_method_list_with_all_params(self, client: Together) -> None: + storage = client.beta.clusters.storage.list( + project_id="project_id", + ) + assert_matches_type(StorageListResponse, storage, path=["response"]) + @parametrize def test_raw_response_list(self, client: Together) -> None: response = client.beta.clusters.storage.with_raw_response.list() @@ -207,6 +225,16 @@ async def test_method_create(self, async_client: AsyncTogether) -> None: ) assert_matches_type(ClusterStorage, storage, path=["response"]) + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncTogether) -> None: + storage = await async_client.beta.clusters.storage.create( + region="region", + size_tib=0, + volume_name="volume_name", + is_lifecycle_independent=True, + ) + assert_matches_type(ClusterStorage, storage, path=["response"]) + @parametrize async def test_raw_response_create(self, async_client: AsyncTogether) -> None: response = await async_client.beta.clusters.storage.with_raw_response.create( @@ -275,11 +303,6 @@ async def test_path_params_retrieve(self, async_client: AsyncTogether) -> None: @parametrize async def test_method_update(self, async_client: AsyncTogether) -> None: - storage = await async_client.beta.clusters.storage.update() - assert_matches_type(ClusterStorage, storage, path=["response"]) - - @parametrize - async def test_method_update_with_all_params(self, async_client: AsyncTogether) -> None: storage = await async_client.beta.clusters.storage.update( size_tib=0, volume_id="volume_id", @@ -288,7 +311,10 @@ async def test_method_update_with_all_params(self, async_client: AsyncTogether) @parametrize async def test_raw_response_update(self, async_client: AsyncTogether) -> None: - response = await async_client.beta.clusters.storage.with_raw_response.update() + response = await async_client.beta.clusters.storage.with_raw_response.update( + size_tib=0, + volume_id="volume_id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -297,7 +323,10 @@ async def test_raw_response_update(self, async_client: AsyncTogether) -> None: @parametrize async def test_streaming_response_update(self, async_client: AsyncTogether) -> None: - async with async_client.beta.clusters.storage.with_streaming_response.update() as response: + async with async_client.beta.clusters.storage.with_streaming_response.update( + size_tib=0, + volume_id="volume_id", + ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -311,6 +340,13 @@ async def test_method_list(self, async_client: AsyncTogether) -> None: storage = await async_client.beta.clusters.storage.list() assert_matches_type(StorageListResponse, storage, path=["response"]) + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncTogether) -> None: + storage = await async_client.beta.clusters.storage.list( + project_id="project_id", + ) + assert_matches_type(StorageListResponse, storage, path=["response"]) + @parametrize async def test_raw_response_list(self, async_client: AsyncTogether) -> None: response = await async_client.beta.clusters.storage.with_raw_response.list() diff --git a/tests/api_resources/beta/test_clusters.py b/tests/api_resources/beta/test_clusters.py index e1f452619..119b86f46 100644 --- a/tests/api_resources/beta/test_clusters.py +++ b/tests/api_resources/beta/test_clusters.py @@ -29,6 +29,7 @@ def test_method_create(self, client: Together) -> None: billing_type="RESERVED", cluster_name="cluster_name", cuda_version="cuda_version", + duration_days=0, gpu_type="H100_SXM", num_gpus=0, nvidia_driver_version="nvidia_driver_version", @@ -42,23 +43,74 @@ def test_method_create_with_all_params(self, client: Together) -> None: billing_type="RESERVED", cluster_name="cluster_name", cuda_version="cuda_version", + duration_days=0, gpu_type="H100_SXM", num_gpus=0, nvidia_driver_version="nvidia_driver_version", region="region", + acceptance_tests_params={ + "dcgm_diag_level": "DCGM_DIAG_LEVEL_SHORT", + "dcgm_diag_skipped": True, + "enabled": True, + "gpu_burn_duration": 0, + "gpu_burn_skipped": True, + "nccl_multi_node_skipped": True, + "nccl_single_node_skipped": True, + }, + add_ons=[ + { + "add_on_type": "add_on_type", + "name": "name", + "config": { + "dashboard": {"enabled": True}, + "ingress": {"enabled": True}, + }, + } + ], + auto_scale=True, auto_scale_max_gpus=0, auto_scaled=True, capacity_pool_id="capacity_pool_id", + cluster_config={ + "load_balancer": "NONE", + "gpu_operator_version": "gpu_operator_version", + "ingress": {"enabled": True}, + "jumphost_enabled": True, + "kubernetes_dashboard_enabled": True, + "observability": {"enabled": True}, + "slurm_startup_scripts": { + "controller_epilog": "controller_epilog", + "controller_prolog": "controller_prolog", + "extra_slurm_conf": "extra_slurm_conf", + "login_init_script": "login_init_script", + "nodeset_init_script": "nodeset_init_script", + "worker_epilog": "worker_epilog", + "worker_prolog": "worker_prolog", + }, + }, cluster_type="KUBERNETES", - duration_days=0, gpu_node_failover_enabled=True, install_traefik=True, + num_capacity_pool_gpus=0, + num_preemptible_gpus=0, + num_reserved_gpus=0, + oidc_config={ + "client_id": "client_id", + "group_claim": "group_claim", + "group_prefix": "group_prefix", + "issuer_url": "issuer_url", + "username_claim": "username_claim", + "username_prefix": "username_prefix", + "ca_cert": "ca_cert", + }, + project_id="project_id", reservation_end_time=parse_datetime("2019-12-27T18:11:19.117Z"), reservation_start_time=parse_datetime("2019-12-27T18:11:19.117Z"), shared_volume={ "region": "region", "size_tib": 0, "volume_name": "volume_name", + "is_lifecycle_independent": True, }, slurm_image="slurm_image", slurm_shm_size_gib=0, @@ -72,6 +124,7 @@ def test_raw_response_create(self, client: Together) -> None: billing_type="RESERVED", cluster_name="cluster_name", cuda_version="cuda_version", + duration_days=0, gpu_type="H100_SXM", num_gpus=0, nvidia_driver_version="nvidia_driver_version", @@ -89,6 +142,7 @@ def test_streaming_response_create(self, client: Together) -> None: billing_type="RESERVED", cluster_name="cluster_name", cuda_version="cuda_version", + duration_days=0, gpu_type="H100_SXM", num_gpus=0, nvidia_driver_version="nvidia_driver_version", @@ -151,8 +205,36 @@ def test_method_update(self, client: Together) -> None: def test_method_update_with_all_params(self, client: Together) -> None: cluster = client.beta.clusters.update( cluster_id="cluster_id", + add_ons=[ + { + "name": "name", + "config": { + "dashboard": {"enabled": True}, + "ingress": {"enabled": True}, + }, + } + ], + cluster_config={ + "load_balancer": "NONE", + "gpu_operator_version": "gpu_operator_version", + "ingress": {"enabled": True}, + "jumphost_enabled": True, + "kubernetes_dashboard_enabled": True, + "observability": {"enabled": True}, + "slurm_startup_scripts": { + "controller_epilog": "controller_epilog", + "controller_prolog": "controller_prolog", + "extra_slurm_conf": "extra_slurm_conf", + "login_init_script": "login_init_script", + "nodeset_init_script": "nodeset_init_script", + "worker_epilog": "worker_epilog", + "worker_prolog": "worker_prolog", + }, + }, cluster_type="KUBERNETES", num_gpus=0, + num_preemptible_gpus=0, + num_reserved_gpus=0, reservation_end_time=parse_datetime("2019-12-27T18:11:19.117Z"), ) assert_matches_type(Cluster, cluster, path=["response"]) @@ -193,6 +275,13 @@ def test_method_list(self, client: Together) -> None: cluster = client.beta.clusters.list() assert_matches_type(ClusterListResponse, cluster, path=["response"]) + @parametrize + def test_method_list_with_all_params(self, client: Together) -> None: + cluster = client.beta.clusters.list( + project_id="project_id", + ) + assert_matches_type(ClusterListResponse, cluster, path=["response"]) + @parametrize def test_raw_response_list(self, client: Together) -> None: response = client.beta.clusters.with_raw_response.list() @@ -288,6 +377,7 @@ async def test_method_create(self, async_client: AsyncTogether) -> None: billing_type="RESERVED", cluster_name="cluster_name", cuda_version="cuda_version", + duration_days=0, gpu_type="H100_SXM", num_gpus=0, nvidia_driver_version="nvidia_driver_version", @@ -301,23 +391,74 @@ async def test_method_create_with_all_params(self, async_client: AsyncTogether) billing_type="RESERVED", cluster_name="cluster_name", cuda_version="cuda_version", + duration_days=0, gpu_type="H100_SXM", num_gpus=0, nvidia_driver_version="nvidia_driver_version", region="region", + acceptance_tests_params={ + "dcgm_diag_level": "DCGM_DIAG_LEVEL_SHORT", + "dcgm_diag_skipped": True, + "enabled": True, + "gpu_burn_duration": 0, + "gpu_burn_skipped": True, + "nccl_multi_node_skipped": True, + "nccl_single_node_skipped": True, + }, + add_ons=[ + { + "add_on_type": "add_on_type", + "name": "name", + "config": { + "dashboard": {"enabled": True}, + "ingress": {"enabled": True}, + }, + } + ], + auto_scale=True, auto_scale_max_gpus=0, auto_scaled=True, capacity_pool_id="capacity_pool_id", + cluster_config={ + "load_balancer": "NONE", + "gpu_operator_version": "gpu_operator_version", + "ingress": {"enabled": True}, + "jumphost_enabled": True, + "kubernetes_dashboard_enabled": True, + "observability": {"enabled": True}, + "slurm_startup_scripts": { + "controller_epilog": "controller_epilog", + "controller_prolog": "controller_prolog", + "extra_slurm_conf": "extra_slurm_conf", + "login_init_script": "login_init_script", + "nodeset_init_script": "nodeset_init_script", + "worker_epilog": "worker_epilog", + "worker_prolog": "worker_prolog", + }, + }, cluster_type="KUBERNETES", - duration_days=0, gpu_node_failover_enabled=True, install_traefik=True, + num_capacity_pool_gpus=0, + num_preemptible_gpus=0, + num_reserved_gpus=0, + oidc_config={ + "client_id": "client_id", + "group_claim": "group_claim", + "group_prefix": "group_prefix", + "issuer_url": "issuer_url", + "username_claim": "username_claim", + "username_prefix": "username_prefix", + "ca_cert": "ca_cert", + }, + project_id="project_id", reservation_end_time=parse_datetime("2019-12-27T18:11:19.117Z"), reservation_start_time=parse_datetime("2019-12-27T18:11:19.117Z"), shared_volume={ "region": "region", "size_tib": 0, "volume_name": "volume_name", + "is_lifecycle_independent": True, }, slurm_image="slurm_image", slurm_shm_size_gib=0, @@ -331,6 +472,7 @@ async def test_raw_response_create(self, async_client: AsyncTogether) -> None: billing_type="RESERVED", cluster_name="cluster_name", cuda_version="cuda_version", + duration_days=0, gpu_type="H100_SXM", num_gpus=0, nvidia_driver_version="nvidia_driver_version", @@ -348,6 +490,7 @@ async def test_streaming_response_create(self, async_client: AsyncTogether) -> N billing_type="RESERVED", cluster_name="cluster_name", cuda_version="cuda_version", + duration_days=0, gpu_type="H100_SXM", num_gpus=0, nvidia_driver_version="nvidia_driver_version", @@ -410,8 +553,36 @@ async def test_method_update(self, async_client: AsyncTogether) -> None: async def test_method_update_with_all_params(self, async_client: AsyncTogether) -> None: cluster = await async_client.beta.clusters.update( cluster_id="cluster_id", + add_ons=[ + { + "name": "name", + "config": { + "dashboard": {"enabled": True}, + "ingress": {"enabled": True}, + }, + } + ], + cluster_config={ + "load_balancer": "NONE", + "gpu_operator_version": "gpu_operator_version", + "ingress": {"enabled": True}, + "jumphost_enabled": True, + "kubernetes_dashboard_enabled": True, + "observability": {"enabled": True}, + "slurm_startup_scripts": { + "controller_epilog": "controller_epilog", + "controller_prolog": "controller_prolog", + "extra_slurm_conf": "extra_slurm_conf", + "login_init_script": "login_init_script", + "nodeset_init_script": "nodeset_init_script", + "worker_epilog": "worker_epilog", + "worker_prolog": "worker_prolog", + }, + }, cluster_type="KUBERNETES", num_gpus=0, + num_preemptible_gpus=0, + num_reserved_gpus=0, reservation_end_time=parse_datetime("2019-12-27T18:11:19.117Z"), ) assert_matches_type(Cluster, cluster, path=["response"]) @@ -452,6 +623,13 @@ async def test_method_list(self, async_client: AsyncTogether) -> None: cluster = await async_client.beta.clusters.list() assert_matches_type(ClusterListResponse, cluster, path=["response"]) + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncTogether) -> None: + cluster = await async_client.beta.clusters.list( + project_id="project_id", + ) + assert_matches_type(ClusterListResponse, cluster, path=["response"]) + @parametrize async def test_raw_response_list(self, async_client: AsyncTogether) -> None: response = await async_client.beta.clusters.with_raw_response.list() From 0d83e7edbfaae7e54461df8714cbe30c9519aad0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 00:12:06 +0000 Subject: [PATCH 02/14] fix(types): correct status field to enum in cluster_storage model --- .stats.yml | 4 ++-- src/together/types/beta/clusters/cluster_storage.py | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 409398869..1e6d5f98c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 75 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-e398785d31ad51e94f158b377be469e74748e0229a964d3602149c6bf964581e.yml -openapi_spec_hash: 3e4db6f27d100314926d87c9ae24cba2 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-5ba14fb08cb2129befa3b72c8bd20b67a863c4be6e010368e2184fbe1b18321b.yml +openapi_spec_hash: 5b1c21199bda07c852b8b48c87859784 config_hash: ec427df08d61d8888138f15cd53c6454 diff --git a/src/together/types/beta/clusters/cluster_storage.py b/src/together/types/beta/clusters/cluster_storage.py index 22392753f..024b79c04 100644 --- a/src/together/types/beta/clusters/cluster_storage.py +++ b/src/together/types/beta/clusters/cluster_storage.py @@ -1,5 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from typing_extensions import Literal + from ...._models import BaseModel __all__ = ["ClusterStorage"] @@ -8,7 +10,10 @@ class ClusterStorage(BaseModel): size_tib: int - status: str + status: Literal[ + "scheduled", "available", "bound", "provisioning", "deleting", "failed", "access_revoked", "unknown" + ] + """Current status of the shared volume.""" volume_id: str From 0e48b36f27b2eb7357198a026e7a4c548b590eef Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 15:25:37 +0000 Subject: [PATCH 03/14] feat(api): add h200-140gb gpu_type to jig deploy/update methods --- .stats.yml | 4 ++-- src/together/resources/beta/jig/jig.py | 8 ++++---- src/together/types/beta/deployment.py | 2 +- src/together/types/beta/jig_deploy_params.py | 2 +- src/together/types/beta/jig_update_params.py | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.stats.yml b/.stats.yml index 1e6d5f98c..88217d8a8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 75 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-5ba14fb08cb2129befa3b72c8bd20b67a863c4be6e010368e2184fbe1b18321b.yml -openapi_spec_hash: 5b1c21199bda07c852b8b48c87859784 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-8f89a16f91cc70112c8f61ccfa8e22d08f3d898afdcbb92d3dec0ad6efba8fb2.yml +openapi_spec_hash: c4dd7519d9865e6e2fcf5b9414b06bab config_hash: ec427df08d61d8888138f15cd53c6454 diff --git a/src/together/resources/beta/jig/jig.py b/src/together/resources/beta/jig/jig.py index 64ba1862f..fd20c3995 100644 --- a/src/together/resources/beta/jig/jig.py +++ b/src/together/resources/beta/jig/jig.py @@ -128,7 +128,7 @@ def update( description: str | Omit = omit, environment_variables: Iterable[jig_update_params.EnvironmentVariable] | Omit = omit, gpu_count: int | Omit = omit, - gpu_type: Literal["h100-80gb", "h100-40gb-mig", "b200-192gb"] | Omit = omit, + gpu_type: Literal["h100-80gb", "h100-40gb-mig", "h200-140gb", "b200-192gb"] | Omit = omit, health_check_path: str | Omit = omit, image: str | Omit = omit, max_replicas: int | Omit = omit, @@ -262,7 +262,7 @@ def list( def deploy( self, *, - gpu_type: Literal["h100-80gb", "h100-40gb-mig", "b200-192gb"], + gpu_type: Literal["h100-80gb", "h100-40gb-mig", "h200-140gb", "b200-192gb"], image: str, name: str, args: SequenceNotStr[str] | Omit = omit, @@ -538,7 +538,7 @@ async def update( description: str | Omit = omit, environment_variables: Iterable[jig_update_params.EnvironmentVariable] | Omit = omit, gpu_count: int | Omit = omit, - gpu_type: Literal["h100-80gb", "h100-40gb-mig", "b200-192gb"] | Omit = omit, + gpu_type: Literal["h100-80gb", "h100-40gb-mig", "h200-140gb", "b200-192gb"] | Omit = omit, health_check_path: str | Omit = omit, image: str | Omit = omit, max_replicas: int | Omit = omit, @@ -672,7 +672,7 @@ async def list( async def deploy( self, *, - gpu_type: Literal["h100-80gb", "h100-40gb-mig", "b200-192gb"], + gpu_type: Literal["h100-80gb", "h100-40gb-mig", "h200-140gb", "b200-192gb"], image: str, name: str, args: SequenceNotStr[str] | Omit = omit, diff --git a/src/together/types/beta/deployment.py b/src/together/types/beta/deployment.py index f7e36e0af..2ff5b0589 100644 --- a/src/together/types/beta/deployment.py +++ b/src/together/types/beta/deployment.py @@ -196,7 +196,7 @@ class Deployment(BaseModel): gpu_count: Optional[int] = None """GPUCount is the number of GPUs allocated to each replica in this deployment""" - gpu_type: Optional[Literal["h100-80gb", "h100-40gb-mig", "b200-192gb"]] = None + gpu_type: Optional[Literal["h100-80gb", "h100-40gb-mig", "h200-140gb", "b200-192gb"]] = None """GPUType specifies the type of GPU requested (if any) for this deployment""" health_check_path: Optional[str] = None diff --git a/src/together/types/beta/jig_deploy_params.py b/src/together/types/beta/jig_deploy_params.py index c0bdc9c54..9682a7a3a 100644 --- a/src/together/types/beta/jig_deploy_params.py +++ b/src/together/types/beta/jig_deploy_params.py @@ -19,7 +19,7 @@ class JigDeployParams(TypedDict, total=False): - gpu_type: Required[Literal["h100-80gb", "h100-40gb-mig", "b200-192gb"]] + gpu_type: Required[Literal["h100-80gb", "h100-40gb-mig", "h200-140gb", "b200-192gb"]] """GPUType specifies the GPU hardware to use (e.g., "h100-80gb").""" image: Required[str] diff --git a/src/together/types/beta/jig_update_params.py b/src/together/types/beta/jig_update_params.py index c91e46d92..56ad5d58c 100644 --- a/src/together/types/beta/jig_update_params.py +++ b/src/together/types/beta/jig_update_params.py @@ -52,7 +52,7 @@ class JigUpdateParams(TypedDict, total=False): gpu_count: int """GPUCount is the number of GPUs to allocate per container instance""" - gpu_type: Literal["h100-80gb", "h100-40gb-mig", "b200-192gb"] + gpu_type: Literal["h100-80gb", "h100-40gb-mig", "h200-140gb", "b200-192gb"] """GPUType specifies the GPU hardware to use (e.g., "h100-80gb")""" health_check_path: str From da57778c1fc871c440b10b1f486a573d5adeea46 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 16:11:23 +0000 Subject: [PATCH 04/14] fix(api): make duration_days optional in clusters create, size_tib optional in storage update --- .stats.yml | 4 ++-- .../resources/beta/clusters/clusters.py | 16 +++++++------- .../resources/beta/clusters/storage.py | 8 +++---- .../types/beta/cluster_create_params.py | 6 ++--- .../beta/clusters/storage_update_params.py | 4 ++-- .../beta/clusters/test_storage.py | 22 ++++++++++++++----- tests/api_resources/beta/test_clusters.py | 10 ++------- 7 files changed, 37 insertions(+), 33 deletions(-) diff --git a/.stats.yml b/.stats.yml index 88217d8a8..c39ae67d5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 75 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-8f89a16f91cc70112c8f61ccfa8e22d08f3d898afdcbb92d3dec0ad6efba8fb2.yml -openapi_spec_hash: c4dd7519d9865e6e2fcf5b9414b06bab +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-e070ac8df7cf6579d5f9f5f7e2896a85de93f46f0ab4aed2f6b48aa4af092204.yml +openapi_spec_hash: 4603775ed88b25a4307e5d87299a23bb config_hash: ec427df08d61d8888138f15cd53c6454 diff --git a/src/together/resources/beta/clusters/clusters.py b/src/together/resources/beta/clusters/clusters.py index 935d71153..7b8f2f67a 100644 --- a/src/together/resources/beta/clusters/clusters.py +++ b/src/together/resources/beta/clusters/clusters.py @@ -66,7 +66,6 @@ def create( billing_type: Literal["RESERVED", "ON_DEMAND", "SCHEDULED_CAPACITY"], cluster_name: str, cuda_version: str, - duration_days: int, gpu_type: Literal["H100_SXM", "H200_SXM", "RTX_6000_PCI", "L40_PCIE", "B200_SXM", "H100_SXM_INF"], num_gpus: int, nvidia_driver_version: str, @@ -79,6 +78,7 @@ def create( capacity_pool_id: str | Omit = omit, cluster_config: cluster_create_params.ClusterConfig | Omit = omit, cluster_type: Literal["KUBERNETES", "SLURM"] | Omit = omit, + duration_days: int | Omit = omit, gpu_node_failover_enabled: bool | Omit = omit, install_traefik: bool | Omit = omit, num_capacity_pool_gpus: int | Omit = omit, @@ -118,8 +118,6 @@ def create( cuda_version: CUDA version for this cluster. For example, 12.5 - duration_days: Duration in days to keep the cluster running. - gpu_type: Type of GPU to use in the cluster num_gpus: Number of GPUs to allocate in the cluster. This must be multiple of 8. For @@ -151,6 +149,8 @@ def create( cluster_type: Type of cluster to create. + duration_days: Duration in days to keep the cluster running. + gpu_node_failover_enabled: Whether automated GPU node failover should be enabled for this cluster. By default, it is disabled. @@ -202,7 +202,6 @@ def create( "billing_type": billing_type, "cluster_name": cluster_name, "cuda_version": cuda_version, - "duration_days": duration_days, "gpu_type": gpu_type, "num_gpus": num_gpus, "nvidia_driver_version": nvidia_driver_version, @@ -215,6 +214,7 @@ def create( "capacity_pool_id": capacity_pool_id, "cluster_config": cluster_config, "cluster_type": cluster_type, + "duration_days": duration_days, "gpu_node_failover_enabled": gpu_node_failover_enabled, "install_traefik": install_traefik, "num_capacity_pool_gpus": num_capacity_pool_gpus, @@ -468,7 +468,6 @@ async def create( billing_type: Literal["RESERVED", "ON_DEMAND", "SCHEDULED_CAPACITY"], cluster_name: str, cuda_version: str, - duration_days: int, gpu_type: Literal["H100_SXM", "H200_SXM", "RTX_6000_PCI", "L40_PCIE", "B200_SXM", "H100_SXM_INF"], num_gpus: int, nvidia_driver_version: str, @@ -481,6 +480,7 @@ async def create( capacity_pool_id: str | Omit = omit, cluster_config: cluster_create_params.ClusterConfig | Omit = omit, cluster_type: Literal["KUBERNETES", "SLURM"] | Omit = omit, + duration_days: int | Omit = omit, gpu_node_failover_enabled: bool | Omit = omit, install_traefik: bool | Omit = omit, num_capacity_pool_gpus: int | Omit = omit, @@ -520,8 +520,6 @@ async def create( cuda_version: CUDA version for this cluster. For example, 12.5 - duration_days: Duration in days to keep the cluster running. - gpu_type: Type of GPU to use in the cluster num_gpus: Number of GPUs to allocate in the cluster. This must be multiple of 8. For @@ -553,6 +551,8 @@ async def create( cluster_type: Type of cluster to create. + duration_days: Duration in days to keep the cluster running. + gpu_node_failover_enabled: Whether automated GPU node failover should be enabled for this cluster. By default, it is disabled. @@ -604,7 +604,6 @@ async def create( "billing_type": billing_type, "cluster_name": cluster_name, "cuda_version": cuda_version, - "duration_days": duration_days, "gpu_type": gpu_type, "num_gpus": num_gpus, "nvidia_driver_version": nvidia_driver_version, @@ -617,6 +616,7 @@ async def create( "capacity_pool_id": capacity_pool_id, "cluster_config": cluster_config, "cluster_type": cluster_type, + "duration_days": duration_days, "gpu_node_failover_enabled": gpu_node_failover_enabled, "install_traefik": install_traefik, "num_capacity_pool_gpus": num_capacity_pool_gpus, diff --git a/src/together/resources/beta/clusters/storage.py b/src/together/resources/beta/clusters/storage.py index c295061f3..48f6ccba7 100644 --- a/src/together/resources/beta/clusters/storage.py +++ b/src/together/resources/beta/clusters/storage.py @@ -132,8 +132,8 @@ def retrieve( def update( self, *, - size_tib: int, volume_id: str, + size_tib: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -157,8 +157,8 @@ def update( "/compute/clusters/storage/volumes", body=maybe_transform( { - "size_tib": size_tib, "volume_id": volume_id, + "size_tib": size_tib, }, storage_update_params.StorageUpdateParams, ), @@ -354,8 +354,8 @@ async def retrieve( async def update( self, *, - size_tib: int, volume_id: str, + size_tib: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -379,8 +379,8 @@ async def update( "/compute/clusters/storage/volumes", body=await async_maybe_transform( { - "size_tib": size_tib, "volume_id": volume_id, + "size_tib": size_tib, }, storage_update_params.StorageUpdateParams, ), diff --git a/src/together/types/beta/cluster_create_params.py b/src/together/types/beta/cluster_create_params.py index 4d9482da5..b950a246d 100644 --- a/src/together/types/beta/cluster_create_params.py +++ b/src/together/types/beta/cluster_create_params.py @@ -40,9 +40,6 @@ class ClusterCreateParams(TypedDict, total=False): cuda_version: Required[str] """CUDA version for this cluster. For example, 12.5""" - duration_days: Required[int] - """Duration in days to keep the cluster running.""" - gpu_type: Required[Literal["H100_SXM", "H200_SXM", "RTX_6000_PCI", "L40_PCIE", "B200_SXM", "H100_SXM_INF"]] """Type of GPU to use in the cluster""" @@ -105,6 +102,9 @@ class ClusterCreateParams(TypedDict, total=False): cluster_type: Literal["KUBERNETES", "SLURM"] """Type of cluster to create.""" + duration_days: int + """Duration in days to keep the cluster running.""" + gpu_node_failover_enabled: bool """Whether automated GPU node failover should be enabled for this cluster. diff --git a/src/together/types/beta/clusters/storage_update_params.py b/src/together/types/beta/clusters/storage_update_params.py index c87e4cd61..116645fa7 100644 --- a/src/together/types/beta/clusters/storage_update_params.py +++ b/src/together/types/beta/clusters/storage_update_params.py @@ -8,6 +8,6 @@ class StorageUpdateParams(TypedDict, total=False): - size_tib: Required[int] - volume_id: Required[str] + + size_tib: int diff --git a/tests/api_resources/beta/clusters/test_storage.py b/tests/api_resources/beta/clusters/test_storage.py index 8403af1ae..de120cc40 100644 --- a/tests/api_resources/beta/clusters/test_storage.py +++ b/tests/api_resources/beta/clusters/test_storage.py @@ -109,15 +109,21 @@ def test_path_params_retrieve(self, client: Together) -> None: @parametrize def test_method_update(self, client: Together) -> None: storage = client.beta.clusters.storage.update( - size_tib=0, volume_id="volume_id", ) assert_matches_type(ClusterStorage, storage, path=["response"]) + @parametrize + def test_method_update_with_all_params(self, client: Together) -> None: + storage = client.beta.clusters.storage.update( + volume_id="volume_id", + size_tib=0, + ) + assert_matches_type(ClusterStorage, storage, path=["response"]) + @parametrize def test_raw_response_update(self, client: Together) -> None: response = client.beta.clusters.storage.with_raw_response.update( - size_tib=0, volume_id="volume_id", ) @@ -129,7 +135,6 @@ def test_raw_response_update(self, client: Together) -> None: @parametrize def test_streaming_response_update(self, client: Together) -> None: with client.beta.clusters.storage.with_streaming_response.update( - size_tib=0, volume_id="volume_id", ) as response: assert not response.is_closed @@ -304,15 +309,21 @@ async def test_path_params_retrieve(self, async_client: AsyncTogether) -> None: @parametrize async def test_method_update(self, async_client: AsyncTogether) -> None: storage = await async_client.beta.clusters.storage.update( - size_tib=0, volume_id="volume_id", ) assert_matches_type(ClusterStorage, storage, path=["response"]) + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncTogether) -> None: + storage = await async_client.beta.clusters.storage.update( + volume_id="volume_id", + size_tib=0, + ) + assert_matches_type(ClusterStorage, storage, path=["response"]) + @parametrize async def test_raw_response_update(self, async_client: AsyncTogether) -> None: response = await async_client.beta.clusters.storage.with_raw_response.update( - size_tib=0, volume_id="volume_id", ) @@ -324,7 +335,6 @@ async def test_raw_response_update(self, async_client: AsyncTogether) -> None: @parametrize async def test_streaming_response_update(self, async_client: AsyncTogether) -> None: async with async_client.beta.clusters.storage.with_streaming_response.update( - size_tib=0, volume_id="volume_id", ) as response: assert not response.is_closed diff --git a/tests/api_resources/beta/test_clusters.py b/tests/api_resources/beta/test_clusters.py index 119b86f46..4d84e3b2b 100644 --- a/tests/api_resources/beta/test_clusters.py +++ b/tests/api_resources/beta/test_clusters.py @@ -29,7 +29,6 @@ def test_method_create(self, client: Together) -> None: billing_type="RESERVED", cluster_name="cluster_name", cuda_version="cuda_version", - duration_days=0, gpu_type="H100_SXM", num_gpus=0, nvidia_driver_version="nvidia_driver_version", @@ -43,7 +42,6 @@ def test_method_create_with_all_params(self, client: Together) -> None: billing_type="RESERVED", cluster_name="cluster_name", cuda_version="cuda_version", - duration_days=0, gpu_type="H100_SXM", num_gpus=0, nvidia_driver_version="nvidia_driver_version", @@ -89,6 +87,7 @@ def test_method_create_with_all_params(self, client: Together) -> None: }, }, cluster_type="KUBERNETES", + duration_days=0, gpu_node_failover_enabled=True, install_traefik=True, num_capacity_pool_gpus=0, @@ -124,7 +123,6 @@ def test_raw_response_create(self, client: Together) -> None: billing_type="RESERVED", cluster_name="cluster_name", cuda_version="cuda_version", - duration_days=0, gpu_type="H100_SXM", num_gpus=0, nvidia_driver_version="nvidia_driver_version", @@ -142,7 +140,6 @@ def test_streaming_response_create(self, client: Together) -> None: billing_type="RESERVED", cluster_name="cluster_name", cuda_version="cuda_version", - duration_days=0, gpu_type="H100_SXM", num_gpus=0, nvidia_driver_version="nvidia_driver_version", @@ -377,7 +374,6 @@ async def test_method_create(self, async_client: AsyncTogether) -> None: billing_type="RESERVED", cluster_name="cluster_name", cuda_version="cuda_version", - duration_days=0, gpu_type="H100_SXM", num_gpus=0, nvidia_driver_version="nvidia_driver_version", @@ -391,7 +387,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncTogether) billing_type="RESERVED", cluster_name="cluster_name", cuda_version="cuda_version", - duration_days=0, gpu_type="H100_SXM", num_gpus=0, nvidia_driver_version="nvidia_driver_version", @@ -437,6 +432,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncTogether) }, }, cluster_type="KUBERNETES", + duration_days=0, gpu_node_failover_enabled=True, install_traefik=True, num_capacity_pool_gpus=0, @@ -472,7 +468,6 @@ async def test_raw_response_create(self, async_client: AsyncTogether) -> None: billing_type="RESERVED", cluster_name="cluster_name", cuda_version="cuda_version", - duration_days=0, gpu_type="H100_SXM", num_gpus=0, nvidia_driver_version="nvidia_driver_version", @@ -490,7 +485,6 @@ async def test_streaming_response_create(self, async_client: AsyncTogether) -> N billing_type="RESERVED", cluster_name="cluster_name", cuda_version="cuda_version", - duration_days=0, gpu_type="H100_SXM", num_gpus=0, nvidia_driver_version="nvidia_driver_version", From f8b715ce616125d550a2bd1734e7fbef2d201788 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 17:37:16 +0000 Subject: [PATCH 05/14] feat: Sync deployments OpenAPI spec --- .stats.yml | 4 +- api.md | 2 +- src/together/resources/beta/jig/jig.py | 30 ++++++++++- src/together/resources/beta/jig/volumes.py | 20 +++++-- src/together/types/beta/deployment.py | 6 +++ src/together/types/beta/jig/__init__.py | 1 + .../types/beta/jig/volume_retrieve_params.py | 12 +++++ .../types/beta/jig_retrieve_logs_params.py | 9 ++++ tests/api_resources/beta/jig/test_volumes.py | 53 +++++++++++++------ tests/api_resources/beta/test_jig.py | 4 ++ 10 files changed, 116 insertions(+), 25 deletions(-) create mode 100644 src/together/types/beta/jig/volume_retrieve_params.py diff --git a/.stats.yml b/.stats.yml index c39ae67d5..bd80968e7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 75 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-e070ac8df7cf6579d5f9f5f7e2896a85de93f46f0ab4aed2f6b48aa4af092204.yml -openapi_spec_hash: 4603775ed88b25a4307e5d87299a23bb +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-a5de4cd2b7b07333c8566d7dc805319e5b1727078b57e5236f8076a7ab4925d2.yml +openapi_spec_hash: d087f1b24b5623db2c9a8d4a2d987a2f config_hash: ec427df08d61d8888138f15cd53c6454 diff --git a/api.md b/api.md index c32e307a7..abdf1cddd 100644 --- a/api.md +++ b/api.md @@ -48,7 +48,7 @@ from together.types.beta.jig import Volume, VolumeListResponse Methods: - client.beta.jig.volumes.create(\*\*params) -> Volume -- client.beta.jig.volumes.retrieve(id) -> Volume +- client.beta.jig.volumes.retrieve(id, \*\*params) -> Volume - client.beta.jig.volumes.update(id, \*\*params) -> Volume - client.beta.jig.volumes.list() -> VolumeListResponse - client.beta.jig.volumes.delete(id) -> object diff --git a/src/together/resources/beta/jig/jig.py b/src/together/resources/beta/jig/jig.py index fd20c3995..8beda0e29 100644 --- a/src/together/resources/beta/jig/jig.py +++ b/src/together/resources/beta/jig/jig.py @@ -422,6 +422,8 @@ def retrieve_logs( id: str, *, replica_id: str | Omit = omit, + revision: str | Omit = omit, + version: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -437,6 +439,11 @@ def retrieve_logs( replica_id: Replica ID to filter logs + revision: Deployment revision (UUID) to filter logs + + version: Deployment image version (tag or last 4 characters of image digest) to filter + logs + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -454,7 +461,14 @@ def retrieve_logs( extra_query=extra_query, extra_body=extra_body, timeout=timeout, - query=maybe_transform({"replica_id": replica_id}, jig_retrieve_logs_params.JigRetrieveLogsParams), + query=maybe_transform( + { + "replica_id": replica_id, + "revision": revision, + "version": version, + }, + jig_retrieve_logs_params.JigRetrieveLogsParams, + ), ), cast_to=DeploymentLogs, ) @@ -832,6 +846,8 @@ async def retrieve_logs( id: str, *, replica_id: str | Omit = omit, + revision: str | Omit = omit, + version: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -847,6 +863,11 @@ async def retrieve_logs( replica_id: Replica ID to filter logs + revision: Deployment revision (UUID) to filter logs + + version: Deployment image version (tag or last 4 characters of image digest) to filter + logs + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -865,7 +886,12 @@ async def retrieve_logs( extra_body=extra_body, timeout=timeout, query=await async_maybe_transform( - {"replica_id": replica_id}, jig_retrieve_logs_params.JigRetrieveLogsParams + { + "replica_id": replica_id, + "revision": revision, + "version": version, + }, + jig_retrieve_logs_params.JigRetrieveLogsParams, ), ), cast_to=DeploymentLogs, diff --git a/src/together/resources/beta/jig/volumes.py b/src/together/resources/beta/jig/volumes.py index 3b94490b7..c70d3ae85 100644 --- a/src/together/resources/beta/jig/volumes.py +++ b/src/together/resources/beta/jig/volumes.py @@ -17,7 +17,7 @@ async_to_streamed_response_wrapper, ) from ...._base_client import make_request_options -from ....types.beta.jig import volume_create_params, volume_update_params +from ....types.beta.jig import volume_create_params, volume_update_params, volume_retrieve_params from ....types.beta.jig.volume import Volume from ....types.beta.jig.volume_list_response import VolumeListResponse @@ -95,6 +95,7 @@ def retrieve( self, id: str, *, + version: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -108,6 +109,8 @@ def retrieve( Args: id: Volume ID or name + version: Volume version to describe (defaults to current version) + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -121,7 +124,11 @@ def retrieve( return self._get( path_template("/deployments/storage/volumes/{id}", id=id), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"version": version}, volume_retrieve_params.VolumeRetrieveParams), ), cast_to=Volume, ) @@ -304,6 +311,7 @@ async def retrieve( self, id: str, *, + version: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -317,6 +325,8 @@ async def retrieve( Args: id: Volume ID or name + version: Volume version to describe (defaults to current version) + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -330,7 +340,11 @@ async def retrieve( return await self._get( path_template("/deployments/storage/volumes/{id}", id=id), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform({"version": version}, volume_retrieve_params.VolumeRetrieveParams), ), cast_to=Volume, ) diff --git a/src/together/types/beta/deployment.py b/src/together/types/beta/deployment.py index 2ff5b0589..3ce2f599a 100644 --- a/src/together/types/beta/deployment.py +++ b/src/together/types/beta/deployment.py @@ -244,6 +244,12 @@ class Deployment(BaseModel): allocated to each replica """ + termination_grace_period_seconds: Optional[int] = None + """ + TerminationGracePeriodSeconds is the time in seconds to wait for graceful + shutdown before forcefully terminating the replica + """ + updated_at: Optional[datetime] = None """UpdatedAt is the ISO8601 timestamp when this deployment was last updated""" diff --git a/src/together/types/beta/jig/__init__.py b/src/together/types/beta/jig/__init__.py index ef03b5652..f8c024918 100644 --- a/src/together/types/beta/jig/__init__.py +++ b/src/together/types/beta/jig/__init__.py @@ -17,4 +17,5 @@ from .queue_retrieve_params import QueueRetrieveParams as QueueRetrieveParams from .queue_submit_response import QueueSubmitResponse as QueueSubmitResponse from .queue_metrics_response import QueueMetricsResponse as QueueMetricsResponse +from .volume_retrieve_params import VolumeRetrieveParams as VolumeRetrieveParams from .queue_retrieve_response import QueueRetrieveResponse as QueueRetrieveResponse diff --git a/src/together/types/beta/jig/volume_retrieve_params.py b/src/together/types/beta/jig/volume_retrieve_params.py new file mode 100644 index 000000000..7b194db4d --- /dev/null +++ b/src/together/types/beta/jig/volume_retrieve_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["VolumeRetrieveParams"] + + +class VolumeRetrieveParams(TypedDict, total=False): + version: int + """Volume version to describe (defaults to current version)""" diff --git a/src/together/types/beta/jig_retrieve_logs_params.py b/src/together/types/beta/jig_retrieve_logs_params.py index 8afbea2d8..59e0ca3cb 100644 --- a/src/together/types/beta/jig_retrieve_logs_params.py +++ b/src/together/types/beta/jig_retrieve_logs_params.py @@ -10,3 +10,12 @@ class JigRetrieveLogsParams(TypedDict, total=False): replica_id: str """Replica ID to filter logs""" + + revision: str + """Deployment revision (UUID) to filter logs""" + + version: str + """ + Deployment image version (tag or last 4 characters of image digest) to filter + logs + """ diff --git a/tests/api_resources/beta/jig/test_volumes.py b/tests/api_resources/beta/jig/test_volumes.py index bc547d755..a630f5a83 100644 --- a/tests/api_resources/beta/jig/test_volumes.py +++ b/tests/api_resources/beta/jig/test_volumes.py @@ -9,7 +9,10 @@ from together import Together, AsyncTogether from tests.utils import assert_matches_type -from together.types.beta.jig import Volume, VolumeListResponse +from together.types.beta.jig import ( + Volume, + VolumeListResponse, +) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -21,7 +24,7 @@ class TestVolumes: def test_method_create(self, client: Together) -> None: volume = client.beta.jig.volumes.create( content={}, - name="name", + name="x", type="readOnly", ) assert_matches_type(Volume, volume, path=["response"]) @@ -33,7 +36,7 @@ def test_method_create_with_all_params(self, client: Together) -> None: "source_prefix": "models/", "type": "files", }, - name="name", + name="x", type="readOnly", ) assert_matches_type(Volume, volume, path=["response"]) @@ -42,7 +45,7 @@ def test_method_create_with_all_params(self, client: Together) -> None: def test_raw_response_create(self, client: Together) -> None: response = client.beta.jig.volumes.with_raw_response.create( content={}, - name="name", + name="x", type="readOnly", ) @@ -55,7 +58,7 @@ def test_raw_response_create(self, client: Together) -> None: def test_streaming_response_create(self, client: Together) -> None: with client.beta.jig.volumes.with_streaming_response.create( content={}, - name="name", + name="x", type="readOnly", ) as response: assert not response.is_closed @@ -69,14 +72,22 @@ def test_streaming_response_create(self, client: Together) -> None: @parametrize def test_method_retrieve(self, client: Together) -> None: volume = client.beta.jig.volumes.retrieve( - "id", + id="id", + ) + assert_matches_type(Volume, volume, path=["response"]) + + @parametrize + def test_method_retrieve_with_all_params(self, client: Together) -> None: + volume = client.beta.jig.volumes.retrieve( + id="id", + version=0, ) assert_matches_type(Volume, volume, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Together) -> None: response = client.beta.jig.volumes.with_raw_response.retrieve( - "id", + id="id", ) assert response.is_closed is True @@ -87,7 +98,7 @@ def test_raw_response_retrieve(self, client: Together) -> None: @parametrize def test_streaming_response_retrieve(self, client: Together) -> None: with client.beta.jig.volumes.with_streaming_response.retrieve( - "id", + id="id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -101,7 +112,7 @@ def test_streaming_response_retrieve(self, client: Together) -> None: def test_path_params_retrieve(self, client: Together) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): client.beta.jig.volumes.with_raw_response.retrieve( - "", + id="", ) @parametrize @@ -228,7 +239,7 @@ class TestAsyncVolumes: async def test_method_create(self, async_client: AsyncTogether) -> None: volume = await async_client.beta.jig.volumes.create( content={}, - name="name", + name="x", type="readOnly", ) assert_matches_type(Volume, volume, path=["response"]) @@ -240,7 +251,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncTogether) "source_prefix": "models/", "type": "files", }, - name="name", + name="x", type="readOnly", ) assert_matches_type(Volume, volume, path=["response"]) @@ -249,7 +260,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncTogether) async def test_raw_response_create(self, async_client: AsyncTogether) -> None: response = await async_client.beta.jig.volumes.with_raw_response.create( content={}, - name="name", + name="x", type="readOnly", ) @@ -262,7 +273,7 @@ async def test_raw_response_create(self, async_client: AsyncTogether) -> None: async def test_streaming_response_create(self, async_client: AsyncTogether) -> None: async with async_client.beta.jig.volumes.with_streaming_response.create( content={}, - name="name", + name="x", type="readOnly", ) as response: assert not response.is_closed @@ -276,14 +287,22 @@ async def test_streaming_response_create(self, async_client: AsyncTogether) -> N @parametrize async def test_method_retrieve(self, async_client: AsyncTogether) -> None: volume = await async_client.beta.jig.volumes.retrieve( - "id", + id="id", + ) + assert_matches_type(Volume, volume, path=["response"]) + + @parametrize + async def test_method_retrieve_with_all_params(self, async_client: AsyncTogether) -> None: + volume = await async_client.beta.jig.volumes.retrieve( + id="id", + version=0, ) assert_matches_type(Volume, volume, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncTogether) -> None: response = await async_client.beta.jig.volumes.with_raw_response.retrieve( - "id", + id="id", ) assert response.is_closed is True @@ -294,7 +313,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncTogether) -> None: @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncTogether) -> None: async with async_client.beta.jig.volumes.with_streaming_response.retrieve( - "id", + id="id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -308,7 +327,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncTogether) -> async def test_path_params_retrieve(self, async_client: AsyncTogether) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): await async_client.beta.jig.volumes.with_raw_response.retrieve( - "", + id="", ) @parametrize diff --git a/tests/api_resources/beta/test_jig.py b/tests/api_resources/beta/test_jig.py index 30f5e8303..dabf106ca 100644 --- a/tests/api_resources/beta/test_jig.py +++ b/tests/api_resources/beta/test_jig.py @@ -290,6 +290,8 @@ def test_method_retrieve_logs_with_all_params(self, client: Together) -> None: jig = client.beta.jig.retrieve_logs( id="id", replica_id="replica_id", + revision="revision", + version="version", ) assert_matches_type(DeploymentLogs, jig, path=["response"]) @@ -599,6 +601,8 @@ async def test_method_retrieve_logs_with_all_params(self, async_client: AsyncTog jig = await async_client.beta.jig.retrieve_logs( id="id", replica_id="replica_id", + revision="revision", + version="version", ) assert_matches_type(DeploymentLogs, jig, path=["response"]) From d1b00a5652245b68c0be7ba6beb7623411b79fc9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 17:39:39 +0000 Subject: [PATCH 06/14] docs(api): add parameter descriptions to storage methods and types --- .stats.yml | 4 ++-- src/together/resources/beta/clusters/storage.py | 16 ++++++++++++++++ src/together/types/beta/cluster.py | 6 +++++- src/together/types/beta/cluster_create_params.py | 2 ++ .../types/beta/clusters/cluster_storage.py | 3 +++ .../types/beta/clusters/storage_create_params.py | 2 ++ .../types/beta/clusters/storage_update_params.py | 2 ++ 7 files changed, 32 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index bd80968e7..232dc112a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 75 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-a5de4cd2b7b07333c8566d7dc805319e5b1727078b57e5236f8076a7ab4925d2.yml -openapi_spec_hash: d087f1b24b5623db2c9a8d4a2d987a2f +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-ae3cfb67611b42139b6d37809a00cf0c26b4803f974a3defd3ce64394fa08e1b.yml +openapi_spec_hash: 50c3ce80ecc7d25283c96e3544567995 config_hash: ec427df08d61d8888138f15cd53c6454 diff --git a/src/together/resources/beta/clusters/storage.py b/src/together/resources/beta/clusters/storage.py index 48f6ccba7..2493f96cf 100644 --- a/src/together/resources/beta/clusters/storage.py +++ b/src/together/resources/beta/clusters/storage.py @@ -65,8 +65,12 @@ def create( performance for shared storage. Args: + region: Region name. Usable regions can be found from `clusters.list_regions()` + size_tib: Volume size in whole tebibytes (TiB). + volume_name: User provided name of the volume. + is_lifecycle_independent: When true, the shared volume is not deleted when the cluster is decommissioned. extra_headers: Send extra headers @@ -145,6 +149,10 @@ def update( Update the configuration of an existing shared volume. Args: + volume_id: ID of the volume. + + size_tib: Size of the volume in TiB. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -287,8 +295,12 @@ async def create( performance for shared storage. Args: + region: Region name. Usable regions can be found from `clusters.list_regions()` + size_tib: Volume size in whole tebibytes (TiB). + volume_name: User provided name of the volume. + is_lifecycle_independent: When true, the shared volume is not deleted when the cluster is decommissioned. extra_headers: Send extra headers @@ -367,6 +379,10 @@ async def update( Update the configuration of an existing shared volume. Args: + volume_id: ID of the volume. + + size_tib: Size of the volume in TiB. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request diff --git a/src/together/types/beta/cluster.py b/src/together/types/beta/cluster.py index 400108cfb..e78c9a07a 100644 --- a/src/together/types/beta/cluster.py +++ b/src/together/types/beta/cluster.py @@ -126,7 +126,7 @@ class GPUWorkerNodePhaseTransition(BaseModel): class GPUWorkerNodeLatestRemediation(BaseModel): """ Remediation represents a node remediation request for an instance. - An instance can have multiple remediations over time (e.g., failed attempts followed by retries). + An instance can have multiple remediations over time (e.g., failed attempts followed by retries). """ id: str @@ -268,12 +268,16 @@ class PhaseTransition(BaseModel): class Volume(BaseModel): size_tib: int + """Size of the volume in TiB.""" status: str + """Current status of the volume.""" volume_id: str + """ID of the volume.""" volume_name: str + """User provided name of the volume.""" class ClusterConfigIngress(BaseModel): diff --git a/src/together/types/beta/cluster_create_params.py b/src/together/types/beta/cluster_create_params.py index b950a246d..9ac607436 100644 --- a/src/together/types/beta/cluster_create_params.py +++ b/src/together/types/beta/cluster_create_params.py @@ -329,11 +329,13 @@ class SharedVolume(TypedDict, total=False): """Inline configuration to create a shared volume with the cluster creation.""" region: Required[str] + """Region name. Usable regions can be found from `clusters.list_regions()`""" size_tib: Required[int] """Volume size in whole tebibytes (TiB).""" volume_name: Required[str] + """User provided name of the volume.""" is_lifecycle_independent: bool """When true, the shared volume is not deleted when the cluster is decommissioned.""" diff --git a/src/together/types/beta/clusters/cluster_storage.py b/src/together/types/beta/clusters/cluster_storage.py index 024b79c04..8daaefc14 100644 --- a/src/together/types/beta/clusters/cluster_storage.py +++ b/src/together/types/beta/clusters/cluster_storage.py @@ -9,6 +9,7 @@ class ClusterStorage(BaseModel): size_tib: int + """Size of the volume in TiB.""" status: Literal[ "scheduled", "available", "bound", "provisioning", "deleting", "failed", "access_revoked", "unknown" @@ -16,5 +17,7 @@ class ClusterStorage(BaseModel): """Current status of the shared volume.""" volume_id: str + """ID of the volume.""" volume_name: str + """User provided name of the volume.""" diff --git a/src/together/types/beta/clusters/storage_create_params.py b/src/together/types/beta/clusters/storage_create_params.py index 2dfb9218f..f368b0e8e 100644 --- a/src/together/types/beta/clusters/storage_create_params.py +++ b/src/together/types/beta/clusters/storage_create_params.py @@ -9,11 +9,13 @@ class StorageCreateParams(TypedDict, total=False): region: Required[str] + """Region name. Usable regions can be found from `clusters.list_regions()`""" size_tib: Required[int] """Volume size in whole tebibytes (TiB).""" volume_name: Required[str] + """User provided name of the volume.""" is_lifecycle_independent: bool """When true, the shared volume is not deleted when the cluster is decommissioned.""" diff --git a/src/together/types/beta/clusters/storage_update_params.py b/src/together/types/beta/clusters/storage_update_params.py index 116645fa7..6e6a0162a 100644 --- a/src/together/types/beta/clusters/storage_update_params.py +++ b/src/together/types/beta/clusters/storage_update_params.py @@ -9,5 +9,7 @@ class StorageUpdateParams(TypedDict, total=False): volume_id: Required[str] + """ID of the volume.""" size_tib: int + """Size of the volume in TiB.""" From 5af43e9a8ccb6d62f1633adcc3394553fabd7ca8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 17:53:42 +0000 Subject: [PATCH 07/14] feat(api): Add node remediation APIs to clusters sdks --- .stats.yml | 6 +- api.md | 22 + .../resources/beta/clusters/__init__.py | 14 + .../resources/beta/clusters/clusters.py | 32 + .../resources/beta/clusters/remediations.py | 849 ++++++++++++++++++ src/together/types/beta/clusters/__init__.py | 9 + .../clusters/remediation_approve_params.py | 18 + .../clusters/remediation_approve_response.py | 94 ++ .../clusters/remediation_cancel_response.py | 94 ++ .../clusters/remediation_create_params.py | 34 + .../clusters/remediation_create_response.py | 94 ++ .../beta/clusters/remediation_list_params.py | 47 + .../clusters/remediation_list_response.py | 107 +++ .../clusters/remediation_reject_params.py | 18 + .../clusters/remediation_reject_response.py | 94 ++ .../beta/clusters/test_remediations.py | 682 ++++++++++++++ 16 files changed, 2211 insertions(+), 3 deletions(-) create mode 100644 src/together/resources/beta/clusters/remediations.py create mode 100644 src/together/types/beta/clusters/remediation_approve_params.py create mode 100644 src/together/types/beta/clusters/remediation_approve_response.py create mode 100644 src/together/types/beta/clusters/remediation_cancel_response.py create mode 100644 src/together/types/beta/clusters/remediation_create_params.py create mode 100644 src/together/types/beta/clusters/remediation_create_response.py create mode 100644 src/together/types/beta/clusters/remediation_list_params.py create mode 100644 src/together/types/beta/clusters/remediation_list_response.py create mode 100644 src/together/types/beta/clusters/remediation_reject_params.py create mode 100644 src/together/types/beta/clusters/remediation_reject_response.py create mode 100644 tests/api_resources/beta/clusters/test_remediations.py diff --git a/.stats.yml b/.stats.yml index 232dc112a..3b8397a01 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 75 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-ae3cfb67611b42139b6d37809a00cf0c26b4803f974a3defd3ce64394fa08e1b.yml +configured_endpoints: 80 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-496df09bdf8bfe14dddc5ba6dc70e219336f51295d2f0671285b4056ec756f21.yml openapi_spec_hash: 50c3ce80ecc7d25283c96e3544567995 -config_hash: ec427df08d61d8888138f15cd53c6454 +config_hash: 2ab62260e2c5e9527b47bef3f36e545d diff --git a/api.md b/api.md index abdf1cddd..1493164db 100644 --- a/api.md +++ b/api.md @@ -91,6 +91,28 @@ Methods: - client.beta.clusters.delete(cluster_id) -> ClusterDeleteResponse - client.beta.clusters.list_regions() -> ClusterListRegionsResponse +### Remediations + +Types: + +```python +from together.types.beta.clusters import ( + RemediationCreateResponse, + RemediationListResponse, + RemediationApproveResponse, + RemediationCancelResponse, + RemediationRejectResponse, +) +``` + +Methods: + +- client.beta.clusters.remediations.create(instance_id, \*, cluster_id, \*\*params) -> RemediationCreateResponse +- client.beta.clusters.remediations.list(instance_id, \*, cluster_id, \*\*params) -> RemediationListResponse +- client.beta.clusters.remediations.approve(remediation_id, \*, cluster_id, instance_id, \*\*params) -> RemediationApproveResponse +- client.beta.clusters.remediations.cancel(remediation_id, \*, cluster_id, instance_id) -> RemediationCancelResponse +- client.beta.clusters.remediations.reject(remediation_id, \*, cluster_id, instance_id, \*\*params) -> RemediationRejectResponse + ### Storage Types: diff --git a/src/together/resources/beta/clusters/__init__.py b/src/together/resources/beta/clusters/__init__.py index a428a2a51..58a0f2342 100644 --- a/src/together/resources/beta/clusters/__init__.py +++ b/src/together/resources/beta/clusters/__init__.py @@ -16,8 +16,22 @@ ClustersResourceWithStreamingResponse, AsyncClustersResourceWithStreamingResponse, ) +from .remediations import ( + RemediationsResource, + AsyncRemediationsResource, + RemediationsResourceWithRawResponse, + AsyncRemediationsResourceWithRawResponse, + RemediationsResourceWithStreamingResponse, + AsyncRemediationsResourceWithStreamingResponse, +) __all__ = [ + "RemediationsResource", + "AsyncRemediationsResource", + "RemediationsResourceWithRawResponse", + "AsyncRemediationsResourceWithRawResponse", + "RemediationsResourceWithStreamingResponse", + "AsyncRemediationsResourceWithStreamingResponse", "StorageResource", "AsyncStorageResource", "StorageResourceWithRawResponse", diff --git a/src/together/resources/beta/clusters/clusters.py b/src/together/resources/beta/clusters/clusters.py index 7b8f2f67a..75efca2fe 100644 --- a/src/together/resources/beta/clusters/clusters.py +++ b/src/together/resources/beta/clusters/clusters.py @@ -26,6 +26,14 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) +from .remediations import ( + RemediationsResource, + AsyncRemediationsResource, + RemediationsResourceWithRawResponse, + AsyncRemediationsResourceWithRawResponse, + RemediationsResourceWithStreamingResponse, + AsyncRemediationsResourceWithStreamingResponse, +) from ....types.beta import cluster_list_params, cluster_create_params, cluster_update_params from ...._base_client import make_request_options from ....types.beta.cluster import Cluster @@ -37,6 +45,10 @@ class ClustersResource(SyncAPIResource): + @cached_property + def remediations(self) -> RemediationsResource: + return RemediationsResource(self._client) + @cached_property def storage(self) -> StorageResource: return StorageResource(self._client) @@ -439,6 +451,10 @@ def list_regions( class AsyncClustersResource(AsyncAPIResource): + @cached_property + def remediations(self) -> AsyncRemediationsResource: + return AsyncRemediationsResource(self._client) + @cached_property def storage(self) -> AsyncStorageResource: return AsyncStorageResource(self._client) @@ -863,6 +879,10 @@ def __init__(self, clusters: ClustersResource) -> None: clusters.list_regions, ) + @cached_property + def remediations(self) -> RemediationsResourceWithRawResponse: + return RemediationsResourceWithRawResponse(self._clusters.remediations) + @cached_property def storage(self) -> StorageResourceWithRawResponse: return StorageResourceWithRawResponse(self._clusters.storage) @@ -891,6 +911,10 @@ def __init__(self, clusters: AsyncClustersResource) -> None: clusters.list_regions, ) + @cached_property + def remediations(self) -> AsyncRemediationsResourceWithRawResponse: + return AsyncRemediationsResourceWithRawResponse(self._clusters.remediations) + @cached_property def storage(self) -> AsyncStorageResourceWithRawResponse: return AsyncStorageResourceWithRawResponse(self._clusters.storage) @@ -919,6 +943,10 @@ def __init__(self, clusters: ClustersResource) -> None: clusters.list_regions, ) + @cached_property + def remediations(self) -> RemediationsResourceWithStreamingResponse: + return RemediationsResourceWithStreamingResponse(self._clusters.remediations) + @cached_property def storage(self) -> StorageResourceWithStreamingResponse: return StorageResourceWithStreamingResponse(self._clusters.storage) @@ -947,6 +975,10 @@ def __init__(self, clusters: AsyncClustersResource) -> None: clusters.list_regions, ) + @cached_property + def remediations(self) -> AsyncRemediationsResourceWithStreamingResponse: + return AsyncRemediationsResourceWithStreamingResponse(self._clusters.remediations) + @cached_property def storage(self) -> AsyncStorageResourceWithStreamingResponse: return AsyncStorageResourceWithStreamingResponse(self._clusters.storage) diff --git a/src/together/resources/beta/clusters/remediations.py b/src/together/resources/beta/clusters/remediations.py new file mode 100644 index 000000000..c130fb213 --- /dev/null +++ b/src/together/resources/beta/clusters/remediations.py @@ -0,0 +1,849 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List +from typing_extensions import Literal + +import httpx + +from ...._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ...._utils import path_template, maybe_transform, async_maybe_transform +from ...._compat import cached_property +from ...._resource import SyncAPIResource, AsyncAPIResource +from ...._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ...._base_client import make_request_options +from ....types.beta.clusters import ( + remediation_list_params, + remediation_create_params, + remediation_reject_params, + remediation_approve_params, +) +from ....types.beta.clusters.remediation_list_response import RemediationListResponse +from ....types.beta.clusters.remediation_cancel_response import RemediationCancelResponse +from ....types.beta.clusters.remediation_create_response import RemediationCreateResponse +from ....types.beta.clusters.remediation_reject_response import RemediationRejectResponse +from ....types.beta.clusters.remediation_approve_response import RemediationApproveResponse + +__all__ = ["RemediationsResource", "AsyncRemediationsResource"] + + +class RemediationsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> RemediationsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/togethercomputer/together-py#accessing-raw-response-data-eg-headers + """ + return RemediationsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> RemediationsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/togethercomputer/together-py#with_streaming_response + """ + return RemediationsResourceWithStreamingResponse(self) + + def create( + self, + instance_id: str, + *, + cluster_id: str, + mode: Literal[ + "REMEDIATION_MODE_VM_ONLY", + "REMEDIATION_MODE_HOST_AWARE", + "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT", + "REMEDIATION_MODE_REBOOT_VM", + ], + remediation_id: str | Omit = omit, + reason: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RemediationCreateResponse: + """ + Creates a new remediation for an instance. + + If mode is unspecified, it defaults to VM_ONLY. If trigger is unspecified, it + defaults to MANUAL. + + For MANUAL triggers: The remediation goes directly to PENDING state. + + For AUTOMATED triggers: The remediation is created with PENDING_APPROVAL state. + The caller must then use ApproveRemediation to start the remediation process. + + Args: + cluster_id: The cluster ID. + + instance_id: The instance ID. + + mode: Remediation mode specifies how the remediation should be performed. + + - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any + available host. + - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and + provisions a new one on a different host. + + remediation_id: Optional. Client-specified ID for idempotency. + + reason: User-provided reason for the remediation. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not cluster_id: + raise ValueError(f"Expected a non-empty value for `cluster_id` but received {cluster_id!r}") + if not instance_id: + raise ValueError(f"Expected a non-empty value for `instance_id` but received {instance_id!r}") + return self._post( + path_template( + "/compute/clusters/{cluster_id}/instances/{instance_id}/remediations", + cluster_id=cluster_id, + instance_id=instance_id, + ), + body=maybe_transform( + { + "mode": mode, + "reason": reason, + }, + remediation_create_params.RemediationCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + {"remediation_id": remediation_id}, remediation_create_params.RemediationCreateParams + ), + ), + cast_to=RemediationCreateResponse, + ) + + def list( + self, + instance_id: str, + *, + cluster_id: str, + mode: Literal[ + "REMEDIATION_MODE_VM_ONLY", + "REMEDIATION_MODE_HOST_AWARE", + "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT", + "REMEDIATION_MODE_REBOOT_VM", + ] + | Omit = omit, + order_by: str | Omit = omit, + page_size: int | Omit = omit, + page_token: str | Omit = omit, + state: List[ + Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"] + ] + | Omit = omit, + trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RemediationListResponse: + """Lists remediations for an instance or cluster. + + Use instances/- as wildcard to + list all remediations in a cluster. + + Args: + cluster_id: The cluster ID. + + instance_id: The instance ID. + + mode: Optional. Filter by remediation mode. Returns only remediations matching the + specified mode. + + order_by: Optional. Order by expression. + + page_size: Optional. Maximum results to return. + + page_token: Optional. Pagination token from previous request. + + state: Optional. Filter by state(s). Returns remediations matching any of the specified + states. + + trigger: Optional. Filter by trigger type. Returns only remediations matching the + specified trigger. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not cluster_id: + raise ValueError(f"Expected a non-empty value for `cluster_id` but received {cluster_id!r}") + if not instance_id: + raise ValueError(f"Expected a non-empty value for `instance_id` but received {instance_id!r}") + return self._get( + path_template( + "/compute/clusters/{cluster_id}/instances/{instance_id}/remediations", + cluster_id=cluster_id, + instance_id=instance_id, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "mode": mode, + "order_by": order_by, + "page_size": page_size, + "page_token": page_token, + "state": state, + "trigger": trigger, + }, + remediation_list_params.RemediationListParams, + ), + ), + cast_to=RemediationListResponse, + ) + + def approve( + self, + remediation_id: str, + *, + cluster_id: str, + instance_id: str, + comment: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RemediationApproveResponse: + """ + Approves a pending remediation. + + Only remediations with state PENDING_APPROVAL can be approved. + + On APPROVE: state changes to PENDING and the remediation process begins. The + reviewed_by, review_time, and review_comment fields are populated on the + remediation after approval. + + Args: + cluster_id: The cluster ID. + + instance_id: The instance ID. + + remediation_id: The remediation ID. + + comment: Comment explaining the action. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not cluster_id: + raise ValueError(f"Expected a non-empty value for `cluster_id` but received {cluster_id!r}") + if not instance_id: + raise ValueError(f"Expected a non-empty value for `instance_id` but received {instance_id!r}") + if not remediation_id: + raise ValueError(f"Expected a non-empty value for `remediation_id` but received {remediation_id!r}") + return self._post( + path_template( + "/compute/clusters/{cluster_id}/instances/{instance_id}/remediations/{remediation_id}/approve", + cluster_id=cluster_id, + instance_id=instance_id, + remediation_id=remediation_id, + ), + body=maybe_transform({"comment": comment}, remediation_approve_params.RemediationApproveParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RemediationApproveResponse, + ) + + def cancel( + self, + remediation_id: str, + *, + cluster_id: str, + instance_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RemediationCancelResponse: + """ + Cancels a pending remediation. + + Only remediations in PENDING_APPROVAL or PENDING state can be cancelled. + + Args: + cluster_id: The cluster ID. + + instance_id: The instance ID. + + remediation_id: The remediation ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not cluster_id: + raise ValueError(f"Expected a non-empty value for `cluster_id` but received {cluster_id!r}") + if not instance_id: + raise ValueError(f"Expected a non-empty value for `instance_id` but received {instance_id!r}") + if not remediation_id: + raise ValueError(f"Expected a non-empty value for `remediation_id` but received {remediation_id!r}") + return self._post( + path_template( + "/compute/clusters/{cluster_id}/instances/{instance_id}/remediations/{remediation_id}/cancel", + cluster_id=cluster_id, + instance_id=instance_id, + remediation_id=remediation_id, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RemediationCancelResponse, + ) + + def reject( + self, + remediation_id: str, + *, + cluster_id: str, + instance_id: str, + comment: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RemediationRejectResponse: + """ + Rejects a pending remediation. + + Only remediations with state PENDING_APPROVAL can be rejected. + + On REJECT: state changes to CANCELLED. The reviewed_by, review_time, and + review_comment fields are populated on the remediation after rejection. + + Args: + cluster_id: The cluster ID. + + instance_id: The instance ID. + + remediation_id: The remediation ID. + + comment: Comment explaining the action. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not cluster_id: + raise ValueError(f"Expected a non-empty value for `cluster_id` but received {cluster_id!r}") + if not instance_id: + raise ValueError(f"Expected a non-empty value for `instance_id` but received {instance_id!r}") + if not remediation_id: + raise ValueError(f"Expected a non-empty value for `remediation_id` but received {remediation_id!r}") + return self._post( + path_template( + "/compute/clusters/{cluster_id}/instances/{instance_id}/remediations/{remediation_id}/reject", + cluster_id=cluster_id, + instance_id=instance_id, + remediation_id=remediation_id, + ), + body=maybe_transform({"comment": comment}, remediation_reject_params.RemediationRejectParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RemediationRejectResponse, + ) + + +class AsyncRemediationsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncRemediationsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/togethercomputer/together-py#accessing-raw-response-data-eg-headers + """ + return AsyncRemediationsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncRemediationsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/togethercomputer/together-py#with_streaming_response + """ + return AsyncRemediationsResourceWithStreamingResponse(self) + + async def create( + self, + instance_id: str, + *, + cluster_id: str, + mode: Literal[ + "REMEDIATION_MODE_VM_ONLY", + "REMEDIATION_MODE_HOST_AWARE", + "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT", + "REMEDIATION_MODE_REBOOT_VM", + ], + remediation_id: str | Omit = omit, + reason: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RemediationCreateResponse: + """ + Creates a new remediation for an instance. + + If mode is unspecified, it defaults to VM_ONLY. If trigger is unspecified, it + defaults to MANUAL. + + For MANUAL triggers: The remediation goes directly to PENDING state. + + For AUTOMATED triggers: The remediation is created with PENDING_APPROVAL state. + The caller must then use ApproveRemediation to start the remediation process. + + Args: + cluster_id: The cluster ID. + + instance_id: The instance ID. + + mode: Remediation mode specifies how the remediation should be performed. + + - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any + available host. + - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and + provisions a new one on a different host. + + remediation_id: Optional. Client-specified ID for idempotency. + + reason: User-provided reason for the remediation. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not cluster_id: + raise ValueError(f"Expected a non-empty value for `cluster_id` but received {cluster_id!r}") + if not instance_id: + raise ValueError(f"Expected a non-empty value for `instance_id` but received {instance_id!r}") + return await self._post( + path_template( + "/compute/clusters/{cluster_id}/instances/{instance_id}/remediations", + cluster_id=cluster_id, + instance_id=instance_id, + ), + body=await async_maybe_transform( + { + "mode": mode, + "reason": reason, + }, + remediation_create_params.RemediationCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + {"remediation_id": remediation_id}, remediation_create_params.RemediationCreateParams + ), + ), + cast_to=RemediationCreateResponse, + ) + + async def list( + self, + instance_id: str, + *, + cluster_id: str, + mode: Literal[ + "REMEDIATION_MODE_VM_ONLY", + "REMEDIATION_MODE_HOST_AWARE", + "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT", + "REMEDIATION_MODE_REBOOT_VM", + ] + | Omit = omit, + order_by: str | Omit = omit, + page_size: int | Omit = omit, + page_token: str | Omit = omit, + state: List[ + Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"] + ] + | Omit = omit, + trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RemediationListResponse: + """Lists remediations for an instance or cluster. + + Use instances/- as wildcard to + list all remediations in a cluster. + + Args: + cluster_id: The cluster ID. + + instance_id: The instance ID. + + mode: Optional. Filter by remediation mode. Returns only remediations matching the + specified mode. + + order_by: Optional. Order by expression. + + page_size: Optional. Maximum results to return. + + page_token: Optional. Pagination token from previous request. + + state: Optional. Filter by state(s). Returns remediations matching any of the specified + states. + + trigger: Optional. Filter by trigger type. Returns only remediations matching the + specified trigger. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not cluster_id: + raise ValueError(f"Expected a non-empty value for `cluster_id` but received {cluster_id!r}") + if not instance_id: + raise ValueError(f"Expected a non-empty value for `instance_id` but received {instance_id!r}") + return await self._get( + path_template( + "/compute/clusters/{cluster_id}/instances/{instance_id}/remediations", + cluster_id=cluster_id, + instance_id=instance_id, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "mode": mode, + "order_by": order_by, + "page_size": page_size, + "page_token": page_token, + "state": state, + "trigger": trigger, + }, + remediation_list_params.RemediationListParams, + ), + ), + cast_to=RemediationListResponse, + ) + + async def approve( + self, + remediation_id: str, + *, + cluster_id: str, + instance_id: str, + comment: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RemediationApproveResponse: + """ + Approves a pending remediation. + + Only remediations with state PENDING_APPROVAL can be approved. + + On APPROVE: state changes to PENDING and the remediation process begins. The + reviewed_by, review_time, and review_comment fields are populated on the + remediation after approval. + + Args: + cluster_id: The cluster ID. + + instance_id: The instance ID. + + remediation_id: The remediation ID. + + comment: Comment explaining the action. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not cluster_id: + raise ValueError(f"Expected a non-empty value for `cluster_id` but received {cluster_id!r}") + if not instance_id: + raise ValueError(f"Expected a non-empty value for `instance_id` but received {instance_id!r}") + if not remediation_id: + raise ValueError(f"Expected a non-empty value for `remediation_id` but received {remediation_id!r}") + return await self._post( + path_template( + "/compute/clusters/{cluster_id}/instances/{instance_id}/remediations/{remediation_id}/approve", + cluster_id=cluster_id, + instance_id=instance_id, + remediation_id=remediation_id, + ), + body=await async_maybe_transform({"comment": comment}, remediation_approve_params.RemediationApproveParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RemediationApproveResponse, + ) + + async def cancel( + self, + remediation_id: str, + *, + cluster_id: str, + instance_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RemediationCancelResponse: + """ + Cancels a pending remediation. + + Only remediations in PENDING_APPROVAL or PENDING state can be cancelled. + + Args: + cluster_id: The cluster ID. + + instance_id: The instance ID. + + remediation_id: The remediation ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not cluster_id: + raise ValueError(f"Expected a non-empty value for `cluster_id` but received {cluster_id!r}") + if not instance_id: + raise ValueError(f"Expected a non-empty value for `instance_id` but received {instance_id!r}") + if not remediation_id: + raise ValueError(f"Expected a non-empty value for `remediation_id` but received {remediation_id!r}") + return await self._post( + path_template( + "/compute/clusters/{cluster_id}/instances/{instance_id}/remediations/{remediation_id}/cancel", + cluster_id=cluster_id, + instance_id=instance_id, + remediation_id=remediation_id, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RemediationCancelResponse, + ) + + async def reject( + self, + remediation_id: str, + *, + cluster_id: str, + instance_id: str, + comment: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RemediationRejectResponse: + """ + Rejects a pending remediation. + + Only remediations with state PENDING_APPROVAL can be rejected. + + On REJECT: state changes to CANCELLED. The reviewed_by, review_time, and + review_comment fields are populated on the remediation after rejection. + + Args: + cluster_id: The cluster ID. + + instance_id: The instance ID. + + remediation_id: The remediation ID. + + comment: Comment explaining the action. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not cluster_id: + raise ValueError(f"Expected a non-empty value for `cluster_id` but received {cluster_id!r}") + if not instance_id: + raise ValueError(f"Expected a non-empty value for `instance_id` but received {instance_id!r}") + if not remediation_id: + raise ValueError(f"Expected a non-empty value for `remediation_id` but received {remediation_id!r}") + return await self._post( + path_template( + "/compute/clusters/{cluster_id}/instances/{instance_id}/remediations/{remediation_id}/reject", + cluster_id=cluster_id, + instance_id=instance_id, + remediation_id=remediation_id, + ), + body=await async_maybe_transform({"comment": comment}, remediation_reject_params.RemediationRejectParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RemediationRejectResponse, + ) + + +class RemediationsResourceWithRawResponse: + def __init__(self, remediations: RemediationsResource) -> None: + self._remediations = remediations + + self.create = to_raw_response_wrapper( + remediations.create, + ) + self.list = to_raw_response_wrapper( + remediations.list, + ) + self.approve = to_raw_response_wrapper( + remediations.approve, + ) + self.cancel = to_raw_response_wrapper( + remediations.cancel, + ) + self.reject = to_raw_response_wrapper( + remediations.reject, + ) + + +class AsyncRemediationsResourceWithRawResponse: + def __init__(self, remediations: AsyncRemediationsResource) -> None: + self._remediations = remediations + + self.create = async_to_raw_response_wrapper( + remediations.create, + ) + self.list = async_to_raw_response_wrapper( + remediations.list, + ) + self.approve = async_to_raw_response_wrapper( + remediations.approve, + ) + self.cancel = async_to_raw_response_wrapper( + remediations.cancel, + ) + self.reject = async_to_raw_response_wrapper( + remediations.reject, + ) + + +class RemediationsResourceWithStreamingResponse: + def __init__(self, remediations: RemediationsResource) -> None: + self._remediations = remediations + + self.create = to_streamed_response_wrapper( + remediations.create, + ) + self.list = to_streamed_response_wrapper( + remediations.list, + ) + self.approve = to_streamed_response_wrapper( + remediations.approve, + ) + self.cancel = to_streamed_response_wrapper( + remediations.cancel, + ) + self.reject = to_streamed_response_wrapper( + remediations.reject, + ) + + +class AsyncRemediationsResourceWithStreamingResponse: + def __init__(self, remediations: AsyncRemediationsResource) -> None: + self._remediations = remediations + + self.create = async_to_streamed_response_wrapper( + remediations.create, + ) + self.list = async_to_streamed_response_wrapper( + remediations.list, + ) + self.approve = async_to_streamed_response_wrapper( + remediations.approve, + ) + self.cancel = async_to_streamed_response_wrapper( + remediations.cancel, + ) + self.reject = async_to_streamed_response_wrapper( + remediations.reject, + ) diff --git a/src/together/types/beta/clusters/__init__.py b/src/together/types/beta/clusters/__init__.py index 9c4f18a76..0ac6cbb1b 100644 --- a/src/together/types/beta/clusters/__init__.py +++ b/src/together/types/beta/clusters/__init__.py @@ -7,4 +7,13 @@ from .storage_create_params import StorageCreateParams as StorageCreateParams from .storage_list_response import StorageListResponse as StorageListResponse from .storage_update_params import StorageUpdateParams as StorageUpdateParams +from .remediation_list_params import RemediationListParams as RemediationListParams from .storage_delete_response import StorageDeleteResponse as StorageDeleteResponse +from .remediation_create_params import RemediationCreateParams as RemediationCreateParams +from .remediation_list_response import RemediationListResponse as RemediationListResponse +from .remediation_reject_params import RemediationRejectParams as RemediationRejectParams +from .remediation_approve_params import RemediationApproveParams as RemediationApproveParams +from .remediation_cancel_response import RemediationCancelResponse as RemediationCancelResponse +from .remediation_create_response import RemediationCreateResponse as RemediationCreateResponse +from .remediation_reject_response import RemediationRejectResponse as RemediationRejectResponse +from .remediation_approve_response import RemediationApproveResponse as RemediationApproveResponse diff --git a/src/together/types/beta/clusters/remediation_approve_params.py b/src/together/types/beta/clusters/remediation_approve_params.py new file mode 100644 index 000000000..cb86457e7 --- /dev/null +++ b/src/together/types/beta/clusters/remediation_approve_params.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["RemediationApproveParams"] + + +class RemediationApproveParams(TypedDict, total=False): + cluster_id: Required[str] + """The cluster ID.""" + + instance_id: Required[str] + """The instance ID.""" + + comment: str + """Comment explaining the action.""" diff --git a/src/together/types/beta/clusters/remediation_approve_response.py b/src/together/types/beta/clusters/remediation_approve_response.py new file mode 100644 index 000000000..040e6dab2 --- /dev/null +++ b/src/together/types/beta/clusters/remediation_approve_response.py @@ -0,0 +1,94 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime +from typing_extensions import Literal + +from ...._models import BaseModel + +__all__ = ["RemediationApproveResponse"] + + +class RemediationApproveResponse(BaseModel): + """ + Remediation represents a node remediation request for an instance. + An instance can have multiple remediations over time (e.g., failed attempts followed by retries). + """ + + id: str + + cluster_id: str + + instance_id: str + + mode: Literal[ + "REMEDIATION_MODE_VM_ONLY", + "REMEDIATION_MODE_HOST_AWARE", + "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT", + "REMEDIATION_MODE_REBOOT_VM", + ] + """Remediation mode specifies how the remediation should be performed. + + - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any + available host. + - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and + provisions a new one on a different host. + """ + + state: Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"] + """RemediationState represents the lifecycle state of a remediation. + + - `PENDING_APPROVAL`: Awaiting approval before processing can begin. + - `PENDING`: Approved and queued for processing. + - `RUNNING`: Actively being processed. + - `SUCCEEDED`: Successfully completed. + - `FAILED`: Failed with an error. + - `CANCELLED`: Cancelled by user or system. + - `AUTO_RESOLVED`: The underlying issue was automatically resolved before + processing. + """ + + trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"] + """RemediationTrigger specifies how the remediation was triggered. + + - `REMEDIATION_TRIGGER_MANUAL`: A user-initiated remediation (either via web UI + or API call). + - `REMEDIATION_TRIGGER_AUTOMATED`: A system-initiated remediation that requires + approval. + """ + + active_health_check_run_id: Optional[str] = None + """Active health check run ID (UUID) that triggered this remediation.""" + + create_time: Optional[datetime] = None + """When the remediation was created.""" + + end_time: Optional[datetime] = None + """When the remediation completed.""" + + error_message: Optional[str] = None + """Error message if the remediation failed.""" + + passive_health_check_event_id: Optional[str] = None + """Passive health check event ID that triggered this remediation.""" + + reason: Optional[str] = None + """User-provided reason for the remediation.""" + + requested_by: Optional[str] = None + """Who requested the remediation.""" + + review_comment: Optional[str] = None + """Review comment.""" + + review_time: Optional[datetime] = None + """When the remediation was reviewed.""" + + reviewed_by: Optional[str] = None + """Who reviewed the remediation.""" + + start_time: Optional[datetime] = None + """When processing started.""" + + update_time: Optional[datetime] = None + """When the remediation was last updated.""" diff --git a/src/together/types/beta/clusters/remediation_cancel_response.py b/src/together/types/beta/clusters/remediation_cancel_response.py new file mode 100644 index 000000000..ec8975c9b --- /dev/null +++ b/src/together/types/beta/clusters/remediation_cancel_response.py @@ -0,0 +1,94 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime +from typing_extensions import Literal + +from ...._models import BaseModel + +__all__ = ["RemediationCancelResponse"] + + +class RemediationCancelResponse(BaseModel): + """ + Remediation represents a node remediation request for an instance. + An instance can have multiple remediations over time (e.g., failed attempts followed by retries). + """ + + id: str + + cluster_id: str + + instance_id: str + + mode: Literal[ + "REMEDIATION_MODE_VM_ONLY", + "REMEDIATION_MODE_HOST_AWARE", + "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT", + "REMEDIATION_MODE_REBOOT_VM", + ] + """Remediation mode specifies how the remediation should be performed. + + - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any + available host. + - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and + provisions a new one on a different host. + """ + + state: Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"] + """RemediationState represents the lifecycle state of a remediation. + + - `PENDING_APPROVAL`: Awaiting approval before processing can begin. + - `PENDING`: Approved and queued for processing. + - `RUNNING`: Actively being processed. + - `SUCCEEDED`: Successfully completed. + - `FAILED`: Failed with an error. + - `CANCELLED`: Cancelled by user or system. + - `AUTO_RESOLVED`: The underlying issue was automatically resolved before + processing. + """ + + trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"] + """RemediationTrigger specifies how the remediation was triggered. + + - `REMEDIATION_TRIGGER_MANUAL`: A user-initiated remediation (either via web UI + or API call). + - `REMEDIATION_TRIGGER_AUTOMATED`: A system-initiated remediation that requires + approval. + """ + + active_health_check_run_id: Optional[str] = None + """Active health check run ID (UUID) that triggered this remediation.""" + + create_time: Optional[datetime] = None + """When the remediation was created.""" + + end_time: Optional[datetime] = None + """When the remediation completed.""" + + error_message: Optional[str] = None + """Error message if the remediation failed.""" + + passive_health_check_event_id: Optional[str] = None + """Passive health check event ID that triggered this remediation.""" + + reason: Optional[str] = None + """User-provided reason for the remediation.""" + + requested_by: Optional[str] = None + """Who requested the remediation.""" + + review_comment: Optional[str] = None + """Review comment.""" + + review_time: Optional[datetime] = None + """When the remediation was reviewed.""" + + reviewed_by: Optional[str] = None + """Who reviewed the remediation.""" + + start_time: Optional[datetime] = None + """When processing started.""" + + update_time: Optional[datetime] = None + """When the remediation was last updated.""" diff --git a/src/together/types/beta/clusters/remediation_create_params.py b/src/together/types/beta/clusters/remediation_create_params.py new file mode 100644 index 000000000..84374cec9 --- /dev/null +++ b/src/together/types/beta/clusters/remediation_create_params.py @@ -0,0 +1,34 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["RemediationCreateParams"] + + +class RemediationCreateParams(TypedDict, total=False): + cluster_id: Required[str] + """The cluster ID.""" + + mode: Required[ + Literal[ + "REMEDIATION_MODE_VM_ONLY", + "REMEDIATION_MODE_HOST_AWARE", + "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT", + "REMEDIATION_MODE_REBOOT_VM", + ] + ] + """Remediation mode specifies how the remediation should be performed. + + - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any + available host. + - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and + provisions a new one on a different host. + """ + + remediation_id: str + """Optional. Client-specified ID for idempotency.""" + + reason: str + """User-provided reason for the remediation.""" diff --git a/src/together/types/beta/clusters/remediation_create_response.py b/src/together/types/beta/clusters/remediation_create_response.py new file mode 100644 index 000000000..2b148f932 --- /dev/null +++ b/src/together/types/beta/clusters/remediation_create_response.py @@ -0,0 +1,94 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime +from typing_extensions import Literal + +from ...._models import BaseModel + +__all__ = ["RemediationCreateResponse"] + + +class RemediationCreateResponse(BaseModel): + """ + Remediation represents a node remediation request for an instance. + An instance can have multiple remediations over time (e.g., failed attempts followed by retries). + """ + + id: str + + cluster_id: str + + instance_id: str + + mode: Literal[ + "REMEDIATION_MODE_VM_ONLY", + "REMEDIATION_MODE_HOST_AWARE", + "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT", + "REMEDIATION_MODE_REBOOT_VM", + ] + """Remediation mode specifies how the remediation should be performed. + + - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any + available host. + - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and + provisions a new one on a different host. + """ + + state: Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"] + """RemediationState represents the lifecycle state of a remediation. + + - `PENDING_APPROVAL`: Awaiting approval before processing can begin. + - `PENDING`: Approved and queued for processing. + - `RUNNING`: Actively being processed. + - `SUCCEEDED`: Successfully completed. + - `FAILED`: Failed with an error. + - `CANCELLED`: Cancelled by user or system. + - `AUTO_RESOLVED`: The underlying issue was automatically resolved before + processing. + """ + + trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"] + """RemediationTrigger specifies how the remediation was triggered. + + - `REMEDIATION_TRIGGER_MANUAL`: A user-initiated remediation (either via web UI + or API call). + - `REMEDIATION_TRIGGER_AUTOMATED`: A system-initiated remediation that requires + approval. + """ + + active_health_check_run_id: Optional[str] = None + """Active health check run ID (UUID) that triggered this remediation.""" + + create_time: Optional[datetime] = None + """When the remediation was created.""" + + end_time: Optional[datetime] = None + """When the remediation completed.""" + + error_message: Optional[str] = None + """Error message if the remediation failed.""" + + passive_health_check_event_id: Optional[str] = None + """Passive health check event ID that triggered this remediation.""" + + reason: Optional[str] = None + """User-provided reason for the remediation.""" + + requested_by: Optional[str] = None + """Who requested the remediation.""" + + review_comment: Optional[str] = None + """Review comment.""" + + review_time: Optional[datetime] = None + """When the remediation was reviewed.""" + + reviewed_by: Optional[str] = None + """Who reviewed the remediation.""" + + start_time: Optional[datetime] = None + """When processing started.""" + + update_time: Optional[datetime] = None + """When the remediation was last updated.""" diff --git a/src/together/types/beta/clusters/remediation_list_params.py b/src/together/types/beta/clusters/remediation_list_params.py new file mode 100644 index 000000000..18e6b884e --- /dev/null +++ b/src/together/types/beta/clusters/remediation_list_params.py @@ -0,0 +1,47 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["RemediationListParams"] + + +class RemediationListParams(TypedDict, total=False): + cluster_id: Required[str] + """The cluster ID.""" + + mode: Literal[ + "REMEDIATION_MODE_VM_ONLY", + "REMEDIATION_MODE_HOST_AWARE", + "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT", + "REMEDIATION_MODE_REBOOT_VM", + ] + """Optional. + + Filter by remediation mode. Returns only remediations matching the specified + mode. + """ + + order_by: str + """Optional. Order by expression.""" + + page_size: int + """Optional. Maximum results to return.""" + + page_token: str + """Optional. Pagination token from previous request.""" + + state: List[Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"]] + """Optional. + + Filter by state(s). Returns remediations matching any of the specified states. + """ + + trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"] + """Optional. + + Filter by trigger type. Returns only remediations matching the specified + trigger. + """ diff --git a/src/together/types/beta/clusters/remediation_list_response.py b/src/together/types/beta/clusters/remediation_list_response.py new file mode 100644 index 000000000..aaf15a9fb --- /dev/null +++ b/src/together/types/beta/clusters/remediation_list_response.py @@ -0,0 +1,107 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import datetime +from typing_extensions import Literal + +from ...._models import BaseModel + +__all__ = ["RemediationListResponse", "Remediation"] + + +class Remediation(BaseModel): + """ + Remediation represents a node remediation request for an instance. + An instance can have multiple remediations over time (e.g., failed attempts followed by retries). + """ + + id: str + + cluster_id: str + + instance_id: str + + mode: Literal[ + "REMEDIATION_MODE_VM_ONLY", + "REMEDIATION_MODE_HOST_AWARE", + "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT", + "REMEDIATION_MODE_REBOOT_VM", + ] + """Remediation mode specifies how the remediation should be performed. + + - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any + available host. + - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and + provisions a new one on a different host. + """ + + state: Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"] + """RemediationState represents the lifecycle state of a remediation. + + - `PENDING_APPROVAL`: Awaiting approval before processing can begin. + - `PENDING`: Approved and queued for processing. + - `RUNNING`: Actively being processed. + - `SUCCEEDED`: Successfully completed. + - `FAILED`: Failed with an error. + - `CANCELLED`: Cancelled by user or system. + - `AUTO_RESOLVED`: The underlying issue was automatically resolved before + processing. + """ + + trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"] + """RemediationTrigger specifies how the remediation was triggered. + + - `REMEDIATION_TRIGGER_MANUAL`: A user-initiated remediation (either via web UI + or API call). + - `REMEDIATION_TRIGGER_AUTOMATED`: A system-initiated remediation that requires + approval. + """ + + active_health_check_run_id: Optional[str] = None + """Active health check run ID (UUID) that triggered this remediation.""" + + create_time: Optional[datetime] = None + """When the remediation was created.""" + + end_time: Optional[datetime] = None + """When the remediation completed.""" + + error_message: Optional[str] = None + """Error message if the remediation failed.""" + + passive_health_check_event_id: Optional[str] = None + """Passive health check event ID that triggered this remediation.""" + + reason: Optional[str] = None + """User-provided reason for the remediation.""" + + requested_by: Optional[str] = None + """Who requested the remediation.""" + + review_comment: Optional[str] = None + """Review comment.""" + + review_time: Optional[datetime] = None + """When the remediation was reviewed.""" + + reviewed_by: Optional[str] = None + """Who reviewed the remediation.""" + + start_time: Optional[datetime] = None + """When processing started.""" + + update_time: Optional[datetime] = None + """When the remediation was last updated.""" + + +class RemediationListResponse(BaseModel): + """ListRemediationsResponse is the response for ListRemediations.""" + + has_next: bool + """Indicates if there are more results available.""" + + next_page_token: str + """Token for the next page.""" + + remediations: List[Remediation] + """The list of remediations.""" diff --git a/src/together/types/beta/clusters/remediation_reject_params.py b/src/together/types/beta/clusters/remediation_reject_params.py new file mode 100644 index 000000000..de91e59f9 --- /dev/null +++ b/src/together/types/beta/clusters/remediation_reject_params.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["RemediationRejectParams"] + + +class RemediationRejectParams(TypedDict, total=False): + cluster_id: Required[str] + """The cluster ID.""" + + instance_id: Required[str] + """The instance ID.""" + + comment: str + """Comment explaining the action.""" diff --git a/src/together/types/beta/clusters/remediation_reject_response.py b/src/together/types/beta/clusters/remediation_reject_response.py new file mode 100644 index 000000000..e90306bb6 --- /dev/null +++ b/src/together/types/beta/clusters/remediation_reject_response.py @@ -0,0 +1,94 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime +from typing_extensions import Literal + +from ...._models import BaseModel + +__all__ = ["RemediationRejectResponse"] + + +class RemediationRejectResponse(BaseModel): + """ + Remediation represents a node remediation request for an instance. + An instance can have multiple remediations over time (e.g., failed attempts followed by retries). + """ + + id: str + + cluster_id: str + + instance_id: str + + mode: Literal[ + "REMEDIATION_MODE_VM_ONLY", + "REMEDIATION_MODE_HOST_AWARE", + "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT", + "REMEDIATION_MODE_REBOOT_VM", + ] + """Remediation mode specifies how the remediation should be performed. + + - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any + available host. + - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and + provisions a new one on a different host. + """ + + state: Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"] + """RemediationState represents the lifecycle state of a remediation. + + - `PENDING_APPROVAL`: Awaiting approval before processing can begin. + - `PENDING`: Approved and queued for processing. + - `RUNNING`: Actively being processed. + - `SUCCEEDED`: Successfully completed. + - `FAILED`: Failed with an error. + - `CANCELLED`: Cancelled by user or system. + - `AUTO_RESOLVED`: The underlying issue was automatically resolved before + processing. + """ + + trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"] + """RemediationTrigger specifies how the remediation was triggered. + + - `REMEDIATION_TRIGGER_MANUAL`: A user-initiated remediation (either via web UI + or API call). + - `REMEDIATION_TRIGGER_AUTOMATED`: A system-initiated remediation that requires + approval. + """ + + active_health_check_run_id: Optional[str] = None + """Active health check run ID (UUID) that triggered this remediation.""" + + create_time: Optional[datetime] = None + """When the remediation was created.""" + + end_time: Optional[datetime] = None + """When the remediation completed.""" + + error_message: Optional[str] = None + """Error message if the remediation failed.""" + + passive_health_check_event_id: Optional[str] = None + """Passive health check event ID that triggered this remediation.""" + + reason: Optional[str] = None + """User-provided reason for the remediation.""" + + requested_by: Optional[str] = None + """Who requested the remediation.""" + + review_comment: Optional[str] = None + """Review comment.""" + + review_time: Optional[datetime] = None + """When the remediation was reviewed.""" + + reviewed_by: Optional[str] = None + """Who reviewed the remediation.""" + + start_time: Optional[datetime] = None + """When processing started.""" + + update_time: Optional[datetime] = None + """When the remediation was last updated.""" diff --git a/tests/api_resources/beta/clusters/test_remediations.py b/tests/api_resources/beta/clusters/test_remediations.py new file mode 100644 index 000000000..3a359fd9a --- /dev/null +++ b/tests/api_resources/beta/clusters/test_remediations.py @@ -0,0 +1,682 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from together import Together, AsyncTogether +from tests.utils import assert_matches_type +from together.types.beta.clusters import ( + RemediationListResponse, + RemediationCancelResponse, + RemediationCreateResponse, + RemediationRejectResponse, + RemediationApproveResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestRemediations: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Together) -> None: + remediation = client.beta.clusters.remediations.create( + instance_id="instance_id", + cluster_id="cluster_id", + mode="REMEDIATION_MODE_VM_ONLY", + ) + assert_matches_type(RemediationCreateResponse, remediation, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Together) -> None: + remediation = client.beta.clusters.remediations.create( + instance_id="instance_id", + cluster_id="cluster_id", + mode="REMEDIATION_MODE_VM_ONLY", + remediation_id="remediation_id", + reason="reason", + ) + assert_matches_type(RemediationCreateResponse, remediation, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Together) -> None: + response = client.beta.clusters.remediations.with_raw_response.create( + instance_id="instance_id", + cluster_id="cluster_id", + mode="REMEDIATION_MODE_VM_ONLY", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + remediation = response.parse() + assert_matches_type(RemediationCreateResponse, remediation, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Together) -> None: + with client.beta.clusters.remediations.with_streaming_response.create( + instance_id="instance_id", + cluster_id="cluster_id", + mode="REMEDIATION_MODE_VM_ONLY", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + remediation = response.parse() + assert_matches_type(RemediationCreateResponse, remediation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_create(self, client: Together) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `cluster_id` but received ''"): + client.beta.clusters.remediations.with_raw_response.create( + instance_id="instance_id", + cluster_id="", + mode="REMEDIATION_MODE_VM_ONLY", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `instance_id` but received ''"): + client.beta.clusters.remediations.with_raw_response.create( + instance_id="", + cluster_id="cluster_id", + mode="REMEDIATION_MODE_VM_ONLY", + ) + + @parametrize + def test_method_list(self, client: Together) -> None: + remediation = client.beta.clusters.remediations.list( + instance_id="instance_id", + cluster_id="cluster_id", + ) + assert_matches_type(RemediationListResponse, remediation, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Together) -> None: + remediation = client.beta.clusters.remediations.list( + instance_id="instance_id", + cluster_id="cluster_id", + mode="REMEDIATION_MODE_VM_ONLY", + order_by="order_by", + page_size=0, + page_token="page_token", + state=["PENDING_APPROVAL"], + trigger="REMEDIATION_TRIGGER_MANUAL", + ) + assert_matches_type(RemediationListResponse, remediation, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Together) -> None: + response = client.beta.clusters.remediations.with_raw_response.list( + instance_id="instance_id", + cluster_id="cluster_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + remediation = response.parse() + assert_matches_type(RemediationListResponse, remediation, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Together) -> None: + with client.beta.clusters.remediations.with_streaming_response.list( + instance_id="instance_id", + cluster_id="cluster_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + remediation = response.parse() + assert_matches_type(RemediationListResponse, remediation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_list(self, client: Together) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `cluster_id` but received ''"): + client.beta.clusters.remediations.with_raw_response.list( + instance_id="instance_id", + cluster_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `instance_id` but received ''"): + client.beta.clusters.remediations.with_raw_response.list( + instance_id="", + cluster_id="cluster_id", + ) + + @parametrize + def test_method_approve(self, client: Together) -> None: + remediation = client.beta.clusters.remediations.approve( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + ) + assert_matches_type(RemediationApproveResponse, remediation, path=["response"]) + + @parametrize + def test_method_approve_with_all_params(self, client: Together) -> None: + remediation = client.beta.clusters.remediations.approve( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + comment="comment", + ) + assert_matches_type(RemediationApproveResponse, remediation, path=["response"]) + + @parametrize + def test_raw_response_approve(self, client: Together) -> None: + response = client.beta.clusters.remediations.with_raw_response.approve( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + remediation = response.parse() + assert_matches_type(RemediationApproveResponse, remediation, path=["response"]) + + @parametrize + def test_streaming_response_approve(self, client: Together) -> None: + with client.beta.clusters.remediations.with_streaming_response.approve( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + remediation = response.parse() + assert_matches_type(RemediationApproveResponse, remediation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_approve(self, client: Together) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `cluster_id` but received ''"): + client.beta.clusters.remediations.with_raw_response.approve( + remediation_id="remediation_id", + cluster_id="", + instance_id="instance_id", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `instance_id` but received ''"): + client.beta.clusters.remediations.with_raw_response.approve( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `remediation_id` but received ''"): + client.beta.clusters.remediations.with_raw_response.approve( + remediation_id="", + cluster_id="cluster_id", + instance_id="instance_id", + ) + + @parametrize + def test_method_cancel(self, client: Together) -> None: + remediation = client.beta.clusters.remediations.cancel( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + ) + assert_matches_type(RemediationCancelResponse, remediation, path=["response"]) + + @parametrize + def test_raw_response_cancel(self, client: Together) -> None: + response = client.beta.clusters.remediations.with_raw_response.cancel( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + remediation = response.parse() + assert_matches_type(RemediationCancelResponse, remediation, path=["response"]) + + @parametrize + def test_streaming_response_cancel(self, client: Together) -> None: + with client.beta.clusters.remediations.with_streaming_response.cancel( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + remediation = response.parse() + assert_matches_type(RemediationCancelResponse, remediation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_cancel(self, client: Together) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `cluster_id` but received ''"): + client.beta.clusters.remediations.with_raw_response.cancel( + remediation_id="remediation_id", + cluster_id="", + instance_id="instance_id", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `instance_id` but received ''"): + client.beta.clusters.remediations.with_raw_response.cancel( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `remediation_id` but received ''"): + client.beta.clusters.remediations.with_raw_response.cancel( + remediation_id="", + cluster_id="cluster_id", + instance_id="instance_id", + ) + + @parametrize + def test_method_reject(self, client: Together) -> None: + remediation = client.beta.clusters.remediations.reject( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + ) + assert_matches_type(RemediationRejectResponse, remediation, path=["response"]) + + @parametrize + def test_method_reject_with_all_params(self, client: Together) -> None: + remediation = client.beta.clusters.remediations.reject( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + comment="comment", + ) + assert_matches_type(RemediationRejectResponse, remediation, path=["response"]) + + @parametrize + def test_raw_response_reject(self, client: Together) -> None: + response = client.beta.clusters.remediations.with_raw_response.reject( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + remediation = response.parse() + assert_matches_type(RemediationRejectResponse, remediation, path=["response"]) + + @parametrize + def test_streaming_response_reject(self, client: Together) -> None: + with client.beta.clusters.remediations.with_streaming_response.reject( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + remediation = response.parse() + assert_matches_type(RemediationRejectResponse, remediation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_reject(self, client: Together) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `cluster_id` but received ''"): + client.beta.clusters.remediations.with_raw_response.reject( + remediation_id="remediation_id", + cluster_id="", + instance_id="instance_id", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `instance_id` but received ''"): + client.beta.clusters.remediations.with_raw_response.reject( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `remediation_id` but received ''"): + client.beta.clusters.remediations.with_raw_response.reject( + remediation_id="", + cluster_id="cluster_id", + instance_id="instance_id", + ) + + +class TestAsyncRemediations: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_create(self, async_client: AsyncTogether) -> None: + remediation = await async_client.beta.clusters.remediations.create( + instance_id="instance_id", + cluster_id="cluster_id", + mode="REMEDIATION_MODE_VM_ONLY", + ) + assert_matches_type(RemediationCreateResponse, remediation, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncTogether) -> None: + remediation = await async_client.beta.clusters.remediations.create( + instance_id="instance_id", + cluster_id="cluster_id", + mode="REMEDIATION_MODE_VM_ONLY", + remediation_id="remediation_id", + reason="reason", + ) + assert_matches_type(RemediationCreateResponse, remediation, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncTogether) -> None: + response = await async_client.beta.clusters.remediations.with_raw_response.create( + instance_id="instance_id", + cluster_id="cluster_id", + mode="REMEDIATION_MODE_VM_ONLY", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + remediation = await response.parse() + assert_matches_type(RemediationCreateResponse, remediation, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncTogether) -> None: + async with async_client.beta.clusters.remediations.with_streaming_response.create( + instance_id="instance_id", + cluster_id="cluster_id", + mode="REMEDIATION_MODE_VM_ONLY", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + remediation = await response.parse() + assert_matches_type(RemediationCreateResponse, remediation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_create(self, async_client: AsyncTogether) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `cluster_id` but received ''"): + await async_client.beta.clusters.remediations.with_raw_response.create( + instance_id="instance_id", + cluster_id="", + mode="REMEDIATION_MODE_VM_ONLY", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `instance_id` but received ''"): + await async_client.beta.clusters.remediations.with_raw_response.create( + instance_id="", + cluster_id="cluster_id", + mode="REMEDIATION_MODE_VM_ONLY", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncTogether) -> None: + remediation = await async_client.beta.clusters.remediations.list( + instance_id="instance_id", + cluster_id="cluster_id", + ) + assert_matches_type(RemediationListResponse, remediation, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncTogether) -> None: + remediation = await async_client.beta.clusters.remediations.list( + instance_id="instance_id", + cluster_id="cluster_id", + mode="REMEDIATION_MODE_VM_ONLY", + order_by="order_by", + page_size=0, + page_token="page_token", + state=["PENDING_APPROVAL"], + trigger="REMEDIATION_TRIGGER_MANUAL", + ) + assert_matches_type(RemediationListResponse, remediation, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncTogether) -> None: + response = await async_client.beta.clusters.remediations.with_raw_response.list( + instance_id="instance_id", + cluster_id="cluster_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + remediation = await response.parse() + assert_matches_type(RemediationListResponse, remediation, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncTogether) -> None: + async with async_client.beta.clusters.remediations.with_streaming_response.list( + instance_id="instance_id", + cluster_id="cluster_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + remediation = await response.parse() + assert_matches_type(RemediationListResponse, remediation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_list(self, async_client: AsyncTogether) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `cluster_id` but received ''"): + await async_client.beta.clusters.remediations.with_raw_response.list( + instance_id="instance_id", + cluster_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `instance_id` but received ''"): + await async_client.beta.clusters.remediations.with_raw_response.list( + instance_id="", + cluster_id="cluster_id", + ) + + @parametrize + async def test_method_approve(self, async_client: AsyncTogether) -> None: + remediation = await async_client.beta.clusters.remediations.approve( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + ) + assert_matches_type(RemediationApproveResponse, remediation, path=["response"]) + + @parametrize + async def test_method_approve_with_all_params(self, async_client: AsyncTogether) -> None: + remediation = await async_client.beta.clusters.remediations.approve( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + comment="comment", + ) + assert_matches_type(RemediationApproveResponse, remediation, path=["response"]) + + @parametrize + async def test_raw_response_approve(self, async_client: AsyncTogether) -> None: + response = await async_client.beta.clusters.remediations.with_raw_response.approve( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + remediation = await response.parse() + assert_matches_type(RemediationApproveResponse, remediation, path=["response"]) + + @parametrize + async def test_streaming_response_approve(self, async_client: AsyncTogether) -> None: + async with async_client.beta.clusters.remediations.with_streaming_response.approve( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + remediation = await response.parse() + assert_matches_type(RemediationApproveResponse, remediation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_approve(self, async_client: AsyncTogether) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `cluster_id` but received ''"): + await async_client.beta.clusters.remediations.with_raw_response.approve( + remediation_id="remediation_id", + cluster_id="", + instance_id="instance_id", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `instance_id` but received ''"): + await async_client.beta.clusters.remediations.with_raw_response.approve( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `remediation_id` but received ''"): + await async_client.beta.clusters.remediations.with_raw_response.approve( + remediation_id="", + cluster_id="cluster_id", + instance_id="instance_id", + ) + + @parametrize + async def test_method_cancel(self, async_client: AsyncTogether) -> None: + remediation = await async_client.beta.clusters.remediations.cancel( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + ) + assert_matches_type(RemediationCancelResponse, remediation, path=["response"]) + + @parametrize + async def test_raw_response_cancel(self, async_client: AsyncTogether) -> None: + response = await async_client.beta.clusters.remediations.with_raw_response.cancel( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + remediation = await response.parse() + assert_matches_type(RemediationCancelResponse, remediation, path=["response"]) + + @parametrize + async def test_streaming_response_cancel(self, async_client: AsyncTogether) -> None: + async with async_client.beta.clusters.remediations.with_streaming_response.cancel( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + remediation = await response.parse() + assert_matches_type(RemediationCancelResponse, remediation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_cancel(self, async_client: AsyncTogether) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `cluster_id` but received ''"): + await async_client.beta.clusters.remediations.with_raw_response.cancel( + remediation_id="remediation_id", + cluster_id="", + instance_id="instance_id", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `instance_id` but received ''"): + await async_client.beta.clusters.remediations.with_raw_response.cancel( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `remediation_id` but received ''"): + await async_client.beta.clusters.remediations.with_raw_response.cancel( + remediation_id="", + cluster_id="cluster_id", + instance_id="instance_id", + ) + + @parametrize + async def test_method_reject(self, async_client: AsyncTogether) -> None: + remediation = await async_client.beta.clusters.remediations.reject( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + ) + assert_matches_type(RemediationRejectResponse, remediation, path=["response"]) + + @parametrize + async def test_method_reject_with_all_params(self, async_client: AsyncTogether) -> None: + remediation = await async_client.beta.clusters.remediations.reject( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + comment="comment", + ) + assert_matches_type(RemediationRejectResponse, remediation, path=["response"]) + + @parametrize + async def test_raw_response_reject(self, async_client: AsyncTogether) -> None: + response = await async_client.beta.clusters.remediations.with_raw_response.reject( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + remediation = await response.parse() + assert_matches_type(RemediationRejectResponse, remediation, path=["response"]) + + @parametrize + async def test_streaming_response_reject(self, async_client: AsyncTogether) -> None: + async with async_client.beta.clusters.remediations.with_streaming_response.reject( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + remediation = await response.parse() + assert_matches_type(RemediationRejectResponse, remediation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_reject(self, async_client: AsyncTogether) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `cluster_id` but received ''"): + await async_client.beta.clusters.remediations.with_raw_response.reject( + remediation_id="remediation_id", + cluster_id="", + instance_id="instance_id", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `instance_id` but received ''"): + await async_client.beta.clusters.remediations.with_raw_response.reject( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `remediation_id` but received ''"): + await async_client.beta.clusters.remediations.with_raw_response.reject( + remediation_id="", + cluster_id="cluster_id", + instance_id="instance_id", + ) From 98e83aa7223452fd834e643b7681615220c1978a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 18:09:24 +0000 Subject: [PATCH 08/14] feat(api): manual updates --- .stats.yml | 6 +- api.md | 2 + .../resources/beta/clusters/remediations.py | 115 +++++++++++++++++ src/together/types/beta/clusters/__init__.py | 1 + .../clusters/remediation_retrieve_response.py | 94 ++++++++++++++ .../beta/clusters/test_remediations.py | 121 ++++++++++++++++++ 6 files changed, 336 insertions(+), 3 deletions(-) create mode 100644 src/together/types/beta/clusters/remediation_retrieve_response.py diff --git a/.stats.yml b/.stats.yml index 3b8397a01..fe90c6295 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 80 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-496df09bdf8bfe14dddc5ba6dc70e219336f51295d2f0671285b4056ec756f21.yml +configured_endpoints: 81 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-b17511a0596b10a96c1af8ea8ab68f43b5a1215f9c8a3ba57ee043215a382b14.yml openapi_spec_hash: 50c3ce80ecc7d25283c96e3544567995 -config_hash: 2ab62260e2c5e9527b47bef3f36e545d +config_hash: 6d7d1c4ec367638d41671e1a4f84808c diff --git a/api.md b/api.md index 1493164db..0a1fb2a72 100644 --- a/api.md +++ b/api.md @@ -98,6 +98,7 @@ Types: ```python from together.types.beta.clusters import ( RemediationCreateResponse, + RemediationRetrieveResponse, RemediationListResponse, RemediationApproveResponse, RemediationCancelResponse, @@ -108,6 +109,7 @@ from together.types.beta.clusters import ( Methods: - client.beta.clusters.remediations.create(instance_id, \*, cluster_id, \*\*params) -> RemediationCreateResponse +- client.beta.clusters.remediations.retrieve(remediation_id, \*, cluster_id, instance_id) -> RemediationRetrieveResponse - client.beta.clusters.remediations.list(instance_id, \*, cluster_id, \*\*params) -> RemediationListResponse - client.beta.clusters.remediations.approve(remediation_id, \*, cluster_id, instance_id, \*\*params) -> RemediationApproveResponse - client.beta.clusters.remediations.cancel(remediation_id, \*, cluster_id, instance_id) -> RemediationCancelResponse diff --git a/src/together/resources/beta/clusters/remediations.py b/src/together/resources/beta/clusters/remediations.py index c130fb213..582665994 100644 --- a/src/together/resources/beta/clusters/remediations.py +++ b/src/together/resources/beta/clusters/remediations.py @@ -29,6 +29,7 @@ from ....types.beta.clusters.remediation_create_response import RemediationCreateResponse from ....types.beta.clusters.remediation_reject_response import RemediationRejectResponse from ....types.beta.clusters.remediation_approve_response import RemediationApproveResponse +from ....types.beta.clusters.remediation_retrieve_response import RemediationRetrieveResponse __all__ = ["RemediationsResource", "AsyncRemediationsResource"] @@ -137,6 +138,57 @@ def create( cast_to=RemediationCreateResponse, ) + def retrieve( + self, + remediation_id: str, + *, + cluster_id: str, + instance_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RemediationRetrieveResponse: + """ + Retrieve the status of a specific remdiation on a specific instance in a + specific cluster. + + Args: + cluster_id: The cluster ID. + + instance_id: The instance ID. + + remediation_id: The remediation ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not cluster_id: + raise ValueError(f"Expected a non-empty value for `cluster_id` but received {cluster_id!r}") + if not instance_id: + raise ValueError(f"Expected a non-empty value for `instance_id` but received {instance_id!r}") + if not remediation_id: + raise ValueError(f"Expected a non-empty value for `remediation_id` but received {remediation_id!r}") + return self._get( + path_template( + "/compute/clusters/{cluster_id}/instances/{instance_id}/remediations/{remediation_id}", + cluster_id=cluster_id, + instance_id=instance_id, + remediation_id=remediation_id, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RemediationRetrieveResponse, + ) + def list( self, instance_id: str, @@ -503,6 +555,57 @@ async def create( cast_to=RemediationCreateResponse, ) + async def retrieve( + self, + remediation_id: str, + *, + cluster_id: str, + instance_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RemediationRetrieveResponse: + """ + Retrieve the status of a specific remdiation on a specific instance in a + specific cluster. + + Args: + cluster_id: The cluster ID. + + instance_id: The instance ID. + + remediation_id: The remediation ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not cluster_id: + raise ValueError(f"Expected a non-empty value for `cluster_id` but received {cluster_id!r}") + if not instance_id: + raise ValueError(f"Expected a non-empty value for `instance_id` but received {instance_id!r}") + if not remediation_id: + raise ValueError(f"Expected a non-empty value for `remediation_id` but received {remediation_id!r}") + return await self._get( + path_template( + "/compute/clusters/{cluster_id}/instances/{instance_id}/remediations/{remediation_id}", + cluster_id=cluster_id, + instance_id=instance_id, + remediation_id=remediation_id, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RemediationRetrieveResponse, + ) + async def list( self, instance_id: str, @@ -772,6 +875,9 @@ def __init__(self, remediations: RemediationsResource) -> None: self.create = to_raw_response_wrapper( remediations.create, ) + self.retrieve = to_raw_response_wrapper( + remediations.retrieve, + ) self.list = to_raw_response_wrapper( remediations.list, ) @@ -793,6 +899,9 @@ def __init__(self, remediations: AsyncRemediationsResource) -> None: self.create = async_to_raw_response_wrapper( remediations.create, ) + self.retrieve = async_to_raw_response_wrapper( + remediations.retrieve, + ) self.list = async_to_raw_response_wrapper( remediations.list, ) @@ -814,6 +923,9 @@ def __init__(self, remediations: RemediationsResource) -> None: self.create = to_streamed_response_wrapper( remediations.create, ) + self.retrieve = to_streamed_response_wrapper( + remediations.retrieve, + ) self.list = to_streamed_response_wrapper( remediations.list, ) @@ -835,6 +947,9 @@ def __init__(self, remediations: AsyncRemediationsResource) -> None: self.create = async_to_streamed_response_wrapper( remediations.create, ) + self.retrieve = async_to_streamed_response_wrapper( + remediations.retrieve, + ) self.list = async_to_streamed_response_wrapper( remediations.list, ) diff --git a/src/together/types/beta/clusters/__init__.py b/src/together/types/beta/clusters/__init__.py index 0ac6cbb1b..4bf653714 100644 --- a/src/together/types/beta/clusters/__init__.py +++ b/src/together/types/beta/clusters/__init__.py @@ -17,3 +17,4 @@ from .remediation_create_response import RemediationCreateResponse as RemediationCreateResponse from .remediation_reject_response import RemediationRejectResponse as RemediationRejectResponse from .remediation_approve_response import RemediationApproveResponse as RemediationApproveResponse +from .remediation_retrieve_response import RemediationRetrieveResponse as RemediationRetrieveResponse diff --git a/src/together/types/beta/clusters/remediation_retrieve_response.py b/src/together/types/beta/clusters/remediation_retrieve_response.py new file mode 100644 index 000000000..ffb6bd3b4 --- /dev/null +++ b/src/together/types/beta/clusters/remediation_retrieve_response.py @@ -0,0 +1,94 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime +from typing_extensions import Literal + +from ...._models import BaseModel + +__all__ = ["RemediationRetrieveResponse"] + + +class RemediationRetrieveResponse(BaseModel): + """ + Remediation represents a node remediation request for an instance. + An instance can have multiple remediations over time (e.g., failed attempts followed by retries). + """ + + id: str + + cluster_id: str + + instance_id: str + + mode: Literal[ + "REMEDIATION_MODE_VM_ONLY", + "REMEDIATION_MODE_HOST_AWARE", + "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT", + "REMEDIATION_MODE_REBOOT_VM", + ] + """Remediation mode specifies how the remediation should be performed. + + - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any + available host. + - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and + provisions a new one on a different host. + """ + + state: Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"] + """RemediationState represents the lifecycle state of a remediation. + + - `PENDING_APPROVAL`: Awaiting approval before processing can begin. + - `PENDING`: Approved and queued for processing. + - `RUNNING`: Actively being processed. + - `SUCCEEDED`: Successfully completed. + - `FAILED`: Failed with an error. + - `CANCELLED`: Cancelled by user or system. + - `AUTO_RESOLVED`: The underlying issue was automatically resolved before + processing. + """ + + trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"] + """RemediationTrigger specifies how the remediation was triggered. + + - `REMEDIATION_TRIGGER_MANUAL`: A user-initiated remediation (either via web UI + or API call). + - `REMEDIATION_TRIGGER_AUTOMATED`: A system-initiated remediation that requires + approval. + """ + + active_health_check_run_id: Optional[str] = None + """Active health check run ID (UUID) that triggered this remediation.""" + + create_time: Optional[datetime] = None + """When the remediation was created.""" + + end_time: Optional[datetime] = None + """When the remediation completed.""" + + error_message: Optional[str] = None + """Error message if the remediation failed.""" + + passive_health_check_event_id: Optional[str] = None + """Passive health check event ID that triggered this remediation.""" + + reason: Optional[str] = None + """User-provided reason for the remediation.""" + + requested_by: Optional[str] = None + """Who requested the remediation.""" + + review_comment: Optional[str] = None + """Review comment.""" + + review_time: Optional[datetime] = None + """When the remediation was reviewed.""" + + reviewed_by: Optional[str] = None + """Who reviewed the remediation.""" + + start_time: Optional[datetime] = None + """When processing started.""" + + update_time: Optional[datetime] = None + """When the remediation was last updated.""" diff --git a/tests/api_resources/beta/clusters/test_remediations.py b/tests/api_resources/beta/clusters/test_remediations.py index 3a359fd9a..277266d4d 100644 --- a/tests/api_resources/beta/clusters/test_remediations.py +++ b/tests/api_resources/beta/clusters/test_remediations.py @@ -15,6 +15,7 @@ RemediationCreateResponse, RemediationRejectResponse, RemediationApproveResponse, + RemediationRetrieveResponse, ) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -87,6 +88,66 @@ def test_path_params_create(self, client: Together) -> None: mode="REMEDIATION_MODE_VM_ONLY", ) + @parametrize + def test_method_retrieve(self, client: Together) -> None: + remediation = client.beta.clusters.remediations.retrieve( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + ) + assert_matches_type(RemediationRetrieveResponse, remediation, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Together) -> None: + response = client.beta.clusters.remediations.with_raw_response.retrieve( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + remediation = response.parse() + assert_matches_type(RemediationRetrieveResponse, remediation, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Together) -> None: + with client.beta.clusters.remediations.with_streaming_response.retrieve( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + remediation = response.parse() + assert_matches_type(RemediationRetrieveResponse, remediation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Together) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `cluster_id` but received ''"): + client.beta.clusters.remediations.with_raw_response.retrieve( + remediation_id="remediation_id", + cluster_id="", + instance_id="instance_id", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `instance_id` but received ''"): + client.beta.clusters.remediations.with_raw_response.retrieve( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `remediation_id` but received ''"): + client.beta.clusters.remediations.with_raw_response.retrieve( + remediation_id="", + cluster_id="cluster_id", + instance_id="instance_id", + ) + @parametrize def test_method_list(self, client: Together) -> None: remediation = client.beta.clusters.remediations.list( @@ -419,6 +480,66 @@ async def test_path_params_create(self, async_client: AsyncTogether) -> None: mode="REMEDIATION_MODE_VM_ONLY", ) + @parametrize + async def test_method_retrieve(self, async_client: AsyncTogether) -> None: + remediation = await async_client.beta.clusters.remediations.retrieve( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + ) + assert_matches_type(RemediationRetrieveResponse, remediation, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncTogether) -> None: + response = await async_client.beta.clusters.remediations.with_raw_response.retrieve( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + remediation = await response.parse() + assert_matches_type(RemediationRetrieveResponse, remediation, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncTogether) -> None: + async with async_client.beta.clusters.remediations.with_streaming_response.retrieve( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="instance_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + remediation = await response.parse() + assert_matches_type(RemediationRetrieveResponse, remediation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncTogether) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `cluster_id` but received ''"): + await async_client.beta.clusters.remediations.with_raw_response.retrieve( + remediation_id="remediation_id", + cluster_id="", + instance_id="instance_id", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `instance_id` but received ''"): + await async_client.beta.clusters.remediations.with_raw_response.retrieve( + remediation_id="remediation_id", + cluster_id="cluster_id", + instance_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `remediation_id` but received ''"): + await async_client.beta.clusters.remediations.with_raw_response.retrieve( + remediation_id="", + cluster_id="cluster_id", + instance_id="instance_id", + ) + @parametrize async def test_method_list(self, async_client: AsyncTogether) -> None: remediation = await async_client.beta.clusters.remediations.list( From e6a7c375285216754eb7f531b40f7bbe843c1638 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 18:18:45 +0000 Subject: [PATCH 09/14] fix(api): remove trigger parameter from remediations list method --- .stats.yml | 4 +- .../resources/beta/clusters/remediations.py | 144 ++++++------------ src/together/resources/models/models.py | 6 +- .../clusters/remediation_approve_params.py | 2 - .../clusters/remediation_create_params.py | 3 +- .../beta/clusters/remediation_list_params.py | 32 ++-- .../clusters/remediation_reject_params.py | 2 - .../beta/clusters/test_remediations.py | 2 - 8 files changed, 72 insertions(+), 123 deletions(-) diff --git a/.stats.yml b/.stats.yml index fe90c6295..d57a71e2c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 81 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-b17511a0596b10a96c1af8ea8ab68f43b5a1215f9c8a3ba57ee043215a382b14.yml -openapi_spec_hash: 50c3ce80ecc7d25283c96e3544567995 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-c8200db997f998a51b2b469b69fcc3a24032e50b45aaaf340c2c176473393adc.yml +openapi_spec_hash: 2544f6840cf7ad23eae3fe8bffed4c4b config_hash: 6d7d1c4ec367638d41671e1a4f84808c diff --git a/src/together/resources/beta/clusters/remediations.py b/src/together/resources/beta/clusters/remediations.py index 582665994..0f4a17b59 100644 --- a/src/together/resources/beta/clusters/remediations.py +++ b/src/together/resources/beta/clusters/remediations.py @@ -77,19 +77,14 @@ def create( """ Creates a new remediation for an instance. - If mode is unspecified, it defaults to VM_ONLY. If trigger is unspecified, it - defaults to MANUAL. + Remediations created via the API goes directly to PENDING state. - For MANUAL triggers: The remediation goes directly to PENDING state. - - For AUTOMATED triggers: The remediation is created with PENDING_APPROVAL state. - The caller must then use ApproveRemediation to start the remediation process. + Our system may trigger automated remediations that require approval. These + remediations are created with PENDING_APPROVAL state. The user must call + /approve to start the actual remediation process. These operations can also be + rejected by calling /reject. Args: - cluster_id: The cluster ID. - - instance_id: The instance ID. - mode: Remediation mode specifies how the remediation should be performed. - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any @@ -97,7 +92,7 @@ def create( - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and provisions a new one on a different host. - remediation_id: Optional. Client-specified ID for idempotency. + remediation_id: Client-specified ID for idempotency. reason: User-provided reason for the remediation. @@ -156,12 +151,6 @@ def retrieve( specific cluster. Args: - cluster_id: The cluster ID. - - instance_id: The instance ID. - - remediation_id: The remediation ID. - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -208,7 +197,6 @@ def list( Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"] ] | Omit = omit, - trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -216,30 +204,33 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> RemediationListResponse: - """Lists remediations for an instance or cluster. - - Use instances/- as wildcard to - list all remediations in a cluster. + """ + Lists remediations for an instance or cluster. Args: - cluster_id: The cluster ID. + instance_id: To list remediations on a specific node, pass the node's instance ID. To list + remediations for all nodes in a cluster, pass `-` as a wildcard for the instance + ID. - instance_id: The instance ID. - - mode: Optional. Filter by remediation mode. Returns only remediations matching the - specified mode. + mode: Filter by remediation mode. Returns only remediations matching the specified + mode. - order_by: Optional. Order by expression. + order_by: Order by expression. - page_size: Optional. Maximum results to return. + page_size: Maximum results to return. - page_token: Optional. Pagination token from previous request. + page_token: Pagination token from previous request. - state: Optional. Filter by state(s). Returns remediations matching any of the specified - states. + state: Filter by state(s). Returns remediations matching any of the specified states. - trigger: Optional. Filter by trigger type. Returns only remediations matching the - specified trigger. + - `PENDING_APPROVAL`: Awaiting approval before processing can begin. + - `PENDING`: Approved and queued for processing. + - `RUNNING`: Actively being processed. + - `SUCCEEDED`: Successfully completed. + - `FAILED`: Failed with an error. + - `CANCELLED`: Cancelled by user or system. + - `AUTO_RESOLVED`: The underlying issue was automatically resolved before + processing. extra_headers: Send extra headers @@ -271,7 +262,6 @@ def list( "page_size": page_size, "page_token": page_token, "state": state, - "trigger": trigger, }, remediation_list_params.RemediationListParams, ), @@ -303,12 +293,6 @@ def approve( remediation after approval. Args: - cluster_id: The cluster ID. - - instance_id: The instance ID. - - remediation_id: The remediation ID. - comment: Comment explaining the action. extra_headers: Send extra headers @@ -414,12 +398,6 @@ def reject( review_comment fields are populated on the remediation after rejection. Args: - cluster_id: The cluster ID. - - instance_id: The instance ID. - - remediation_id: The remediation ID. - comment: Comment explaining the action. extra_headers: Send extra headers @@ -494,19 +472,14 @@ async def create( """ Creates a new remediation for an instance. - If mode is unspecified, it defaults to VM_ONLY. If trigger is unspecified, it - defaults to MANUAL. + Remediations created via the API goes directly to PENDING state. - For MANUAL triggers: The remediation goes directly to PENDING state. - - For AUTOMATED triggers: The remediation is created with PENDING_APPROVAL state. - The caller must then use ApproveRemediation to start the remediation process. + Our system may trigger automated remediations that require approval. These + remediations are created with PENDING_APPROVAL state. The user must call + /approve to start the actual remediation process. These operations can also be + rejected by calling /reject. Args: - cluster_id: The cluster ID. - - instance_id: The instance ID. - mode: Remediation mode specifies how the remediation should be performed. - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any @@ -514,7 +487,7 @@ async def create( - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and provisions a new one on a different host. - remediation_id: Optional. Client-specified ID for idempotency. + remediation_id: Client-specified ID for idempotency. reason: User-provided reason for the remediation. @@ -573,12 +546,6 @@ async def retrieve( specific cluster. Args: - cluster_id: The cluster ID. - - instance_id: The instance ID. - - remediation_id: The remediation ID. - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -625,7 +592,6 @@ async def list( Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"] ] | Omit = omit, - trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -633,30 +599,33 @@ async def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> RemediationListResponse: - """Lists remediations for an instance or cluster. - - Use instances/- as wildcard to - list all remediations in a cluster. + """ + Lists remediations for an instance or cluster. Args: - cluster_id: The cluster ID. + instance_id: To list remediations on a specific node, pass the node's instance ID. To list + remediations for all nodes in a cluster, pass `-` as a wildcard for the instance + ID. - instance_id: The instance ID. - - mode: Optional. Filter by remediation mode. Returns only remediations matching the - specified mode. + mode: Filter by remediation mode. Returns only remediations matching the specified + mode. - order_by: Optional. Order by expression. + order_by: Order by expression. - page_size: Optional. Maximum results to return. + page_size: Maximum results to return. - page_token: Optional. Pagination token from previous request. + page_token: Pagination token from previous request. - state: Optional. Filter by state(s). Returns remediations matching any of the specified - states. + state: Filter by state(s). Returns remediations matching any of the specified states. - trigger: Optional. Filter by trigger type. Returns only remediations matching the - specified trigger. + - `PENDING_APPROVAL`: Awaiting approval before processing can begin. + - `PENDING`: Approved and queued for processing. + - `RUNNING`: Actively being processed. + - `SUCCEEDED`: Successfully completed. + - `FAILED`: Failed with an error. + - `CANCELLED`: Cancelled by user or system. + - `AUTO_RESOLVED`: The underlying issue was automatically resolved before + processing. extra_headers: Send extra headers @@ -688,7 +657,6 @@ async def list( "page_size": page_size, "page_token": page_token, "state": state, - "trigger": trigger, }, remediation_list_params.RemediationListParams, ), @@ -720,12 +688,6 @@ async def approve( remediation after approval. Args: - cluster_id: The cluster ID. - - instance_id: The instance ID. - - remediation_id: The remediation ID. - comment: Comment explaining the action. extra_headers: Send extra headers @@ -831,12 +793,6 @@ async def reject( review_comment fields are populated on the remediation after rejection. Args: - cluster_id: The cluster ID. - - instance_id: The instance ID. - - remediation_id: The remediation ID. - comment: Comment explaining the action. extra_headers: Send extra headers diff --git a/src/together/resources/models/models.py b/src/together/resources/models/models.py index b14a0a630..a3486129c 100644 --- a/src/together/resources/models/models.py +++ b/src/together/resources/models/models.py @@ -68,7 +68,8 @@ def list( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ModelListResponse: """ - Lists all of Together's open-source models + Lists all of Together's open-source models and metadata including pricing, chat + template, and context. Args: dedicated: Filter models to only return dedicated models @@ -195,7 +196,8 @@ async def list( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ModelListResponse: """ - Lists all of Together's open-source models + Lists all of Together's open-source models and metadata including pricing, chat + template, and context. Args: dedicated: Filter models to only return dedicated models diff --git a/src/together/types/beta/clusters/remediation_approve_params.py b/src/together/types/beta/clusters/remediation_approve_params.py index cb86457e7..721f96e08 100644 --- a/src/together/types/beta/clusters/remediation_approve_params.py +++ b/src/together/types/beta/clusters/remediation_approve_params.py @@ -9,10 +9,8 @@ class RemediationApproveParams(TypedDict, total=False): cluster_id: Required[str] - """The cluster ID.""" instance_id: Required[str] - """The instance ID.""" comment: str """Comment explaining the action.""" diff --git a/src/together/types/beta/clusters/remediation_create_params.py b/src/together/types/beta/clusters/remediation_create_params.py index 84374cec9..c32ade02f 100644 --- a/src/together/types/beta/clusters/remediation_create_params.py +++ b/src/together/types/beta/clusters/remediation_create_params.py @@ -9,7 +9,6 @@ class RemediationCreateParams(TypedDict, total=False): cluster_id: Required[str] - """The cluster ID.""" mode: Required[ Literal[ @@ -28,7 +27,7 @@ class RemediationCreateParams(TypedDict, total=False): """ remediation_id: str - """Optional. Client-specified ID for idempotency.""" + """Client-specified ID for idempotency.""" reason: str """User-provided reason for the remediation.""" diff --git a/src/together/types/beta/clusters/remediation_list_params.py b/src/together/types/beta/clusters/remediation_list_params.py index 18e6b884e..215800be9 100644 --- a/src/together/types/beta/clusters/remediation_list_params.py +++ b/src/together/types/beta/clusters/remediation_list_params.py @@ -10,7 +10,6 @@ class RemediationListParams(TypedDict, total=False): cluster_id: Required[str] - """The cluster ID.""" mode: Literal[ "REMEDIATION_MODE_VM_ONLY", @@ -18,30 +17,29 @@ class RemediationListParams(TypedDict, total=False): "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT", "REMEDIATION_MODE_REBOOT_VM", ] - """Optional. + """Filter by remediation mode. - Filter by remediation mode. Returns only remediations matching the specified - mode. + Returns only remediations matching the specified mode. """ order_by: str - """Optional. Order by expression.""" + """Order by expression.""" page_size: int - """Optional. Maximum results to return.""" + """Maximum results to return.""" page_token: str - """Optional. Pagination token from previous request.""" + """Pagination token from previous request.""" state: List[Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"]] - """Optional. - - Filter by state(s). Returns remediations matching any of the specified states. - """ - - trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"] - """Optional. - - Filter by trigger type. Returns only remediations matching the specified - trigger. + """Filter by state(s). Returns remediations matching any of the specified states. + + - `PENDING_APPROVAL`: Awaiting approval before processing can begin. + - `PENDING`: Approved and queued for processing. + - `RUNNING`: Actively being processed. + - `SUCCEEDED`: Successfully completed. + - `FAILED`: Failed with an error. + - `CANCELLED`: Cancelled by user or system. + - `AUTO_RESOLVED`: The underlying issue was automatically resolved before + processing. """ diff --git a/src/together/types/beta/clusters/remediation_reject_params.py b/src/together/types/beta/clusters/remediation_reject_params.py index de91e59f9..cfe532aa3 100644 --- a/src/together/types/beta/clusters/remediation_reject_params.py +++ b/src/together/types/beta/clusters/remediation_reject_params.py @@ -9,10 +9,8 @@ class RemediationRejectParams(TypedDict, total=False): cluster_id: Required[str] - """The cluster ID.""" instance_id: Required[str] - """The instance ID.""" comment: str """Comment explaining the action.""" diff --git a/tests/api_resources/beta/clusters/test_remediations.py b/tests/api_resources/beta/clusters/test_remediations.py index 277266d4d..7fb96a882 100644 --- a/tests/api_resources/beta/clusters/test_remediations.py +++ b/tests/api_resources/beta/clusters/test_remediations.py @@ -166,7 +166,6 @@ def test_method_list_with_all_params(self, client: Together) -> None: page_size=0, page_token="page_token", state=["PENDING_APPROVAL"], - trigger="REMEDIATION_TRIGGER_MANUAL", ) assert_matches_type(RemediationListResponse, remediation, path=["response"]) @@ -558,7 +557,6 @@ async def test_method_list_with_all_params(self, async_client: AsyncTogether) -> page_size=0, page_token="page_token", state=["PENDING_APPROVAL"], - trigger="REMEDIATION_TRIGGER_MANUAL", ) assert_matches_type(RemediationListResponse, remediation, path=["response"]) From 598dbd011990fc2b3fc0738dd178a5fd3997e676 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 18:27:15 +0000 Subject: [PATCH 10/14] feat(api): manual updates --- .stats.yml | 2 +- api.md | 19 ++-- .../resources/beta/clusters/remediations.py | 46 +++++---- src/together/types/beta/cluster.py | 89 +----------------- src/together/types/beta/clusters/__init__.py | 6 +- ...tion_cancel_response.py => remediation.py} | 4 +- .../clusters/remediation_approve_response.py | 94 ------------------- .../clusters/remediation_create_response.py | 94 ------------------- .../clusters/remediation_list_response.py | 92 +----------------- .../clusters/remediation_reject_response.py | 94 ------------------- .../clusters/remediation_retrieve_response.py | 94 ------------------- .../beta/clusters/test_remediations.py | 78 ++++++++------- 12 files changed, 73 insertions(+), 639 deletions(-) rename src/together/types/beta/clusters/{remediation_cancel_response.py => remediation.py} (97%) delete mode 100644 src/together/types/beta/clusters/remediation_approve_response.py delete mode 100644 src/together/types/beta/clusters/remediation_create_response.py delete mode 100644 src/together/types/beta/clusters/remediation_reject_response.py delete mode 100644 src/together/types/beta/clusters/remediation_retrieve_response.py diff --git a/.stats.yml b/.stats.yml index d57a71e2c..37b2bba11 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 81 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-c8200db997f998a51b2b469b69fcc3a24032e50b45aaaf340c2c176473393adc.yml openapi_spec_hash: 2544f6840cf7ad23eae3fe8bffed4c4b -config_hash: 6d7d1c4ec367638d41671e1a4f84808c +config_hash: b35d5968fb07cce1c1be735f874898b1 diff --git a/api.md b/api.md index 0a1fb2a72..156264d90 100644 --- a/api.md +++ b/api.md @@ -96,24 +96,17 @@ Methods: Types: ```python -from together.types.beta.clusters import ( - RemediationCreateResponse, - RemediationRetrieveResponse, - RemediationListResponse, - RemediationApproveResponse, - RemediationCancelResponse, - RemediationRejectResponse, -) +from together.types.beta.clusters import Remediation, RemediationListResponse ``` Methods: -- client.beta.clusters.remediations.create(instance_id, \*, cluster_id, \*\*params) -> RemediationCreateResponse -- client.beta.clusters.remediations.retrieve(remediation_id, \*, cluster_id, instance_id) -> RemediationRetrieveResponse +- client.beta.clusters.remediations.create(instance_id, \*, cluster_id, \*\*params) -> Remediation +- client.beta.clusters.remediations.retrieve(remediation_id, \*, cluster_id, instance_id) -> Remediation - client.beta.clusters.remediations.list(instance_id, \*, cluster_id, \*\*params) -> RemediationListResponse -- client.beta.clusters.remediations.approve(remediation_id, \*, cluster_id, instance_id, \*\*params) -> RemediationApproveResponse -- client.beta.clusters.remediations.cancel(remediation_id, \*, cluster_id, instance_id) -> RemediationCancelResponse -- client.beta.clusters.remediations.reject(remediation_id, \*, cluster_id, instance_id, \*\*params) -> RemediationRejectResponse +- client.beta.clusters.remediations.approve(remediation_id, \*, cluster_id, instance_id, \*\*params) -> Remediation +- client.beta.clusters.remediations.cancel(remediation_id, \*, cluster_id, instance_id) -> Remediation +- client.beta.clusters.remediations.reject(remediation_id, \*, cluster_id, instance_id, \*\*params) -> Remediation ### Storage diff --git a/src/together/resources/beta/clusters/remediations.py b/src/together/resources/beta/clusters/remediations.py index 0f4a17b59..33dff106e 100644 --- a/src/together/resources/beta/clusters/remediations.py +++ b/src/together/resources/beta/clusters/remediations.py @@ -24,12 +24,8 @@ remediation_reject_params, remediation_approve_params, ) +from ....types.beta.clusters.remediation import Remediation from ....types.beta.clusters.remediation_list_response import RemediationListResponse -from ....types.beta.clusters.remediation_cancel_response import RemediationCancelResponse -from ....types.beta.clusters.remediation_create_response import RemediationCreateResponse -from ....types.beta.clusters.remediation_reject_response import RemediationRejectResponse -from ....types.beta.clusters.remediation_approve_response import RemediationApproveResponse -from ....types.beta.clusters.remediation_retrieve_response import RemediationRetrieveResponse __all__ = ["RemediationsResource", "AsyncRemediationsResource"] @@ -73,7 +69,7 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> RemediationCreateResponse: + ) -> Remediation: """ Creates a new remediation for an instance. @@ -130,7 +126,7 @@ def create( {"remediation_id": remediation_id}, remediation_create_params.RemediationCreateParams ), ), - cast_to=RemediationCreateResponse, + cast_to=Remediation, ) def retrieve( @@ -145,7 +141,7 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> RemediationRetrieveResponse: + ) -> Remediation: """ Retrieve the status of a specific remdiation on a specific instance in a specific cluster. @@ -175,7 +171,7 @@ def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=RemediationRetrieveResponse, + cast_to=Remediation, ) def list( @@ -282,7 +278,7 @@ def approve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> RemediationApproveResponse: + ) -> Remediation: """ Approves a pending remediation. @@ -320,7 +316,7 @@ def approve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=RemediationApproveResponse, + cast_to=Remediation, ) def cancel( @@ -335,7 +331,7 @@ def cancel( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> RemediationCancelResponse: + ) -> Remediation: """ Cancels a pending remediation. @@ -372,7 +368,7 @@ def cancel( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=RemediationCancelResponse, + cast_to=Remediation, ) def reject( @@ -388,7 +384,7 @@ def reject( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> RemediationRejectResponse: + ) -> Remediation: """ Rejects a pending remediation. @@ -425,7 +421,7 @@ def reject( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=RemediationRejectResponse, + cast_to=Remediation, ) @@ -468,7 +464,7 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> RemediationCreateResponse: + ) -> Remediation: """ Creates a new remediation for an instance. @@ -525,7 +521,7 @@ async def create( {"remediation_id": remediation_id}, remediation_create_params.RemediationCreateParams ), ), - cast_to=RemediationCreateResponse, + cast_to=Remediation, ) async def retrieve( @@ -540,7 +536,7 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> RemediationRetrieveResponse: + ) -> Remediation: """ Retrieve the status of a specific remdiation on a specific instance in a specific cluster. @@ -570,7 +566,7 @@ async def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=RemediationRetrieveResponse, + cast_to=Remediation, ) async def list( @@ -677,7 +673,7 @@ async def approve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> RemediationApproveResponse: + ) -> Remediation: """ Approves a pending remediation. @@ -715,7 +711,7 @@ async def approve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=RemediationApproveResponse, + cast_to=Remediation, ) async def cancel( @@ -730,7 +726,7 @@ async def cancel( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> RemediationCancelResponse: + ) -> Remediation: """ Cancels a pending remediation. @@ -767,7 +763,7 @@ async def cancel( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=RemediationCancelResponse, + cast_to=Remediation, ) async def reject( @@ -783,7 +779,7 @@ async def reject( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> RemediationRejectResponse: + ) -> Remediation: """ Rejects a pending remediation. @@ -820,7 +816,7 @@ async def reject( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=RemediationRejectResponse, + cast_to=Remediation, ) diff --git a/src/together/types/beta/cluster.py b/src/together/types/beta/cluster.py index e78c9a07a..2f46d55a0 100644 --- a/src/together/types/beta/cluster.py +++ b/src/together/types/beta/cluster.py @@ -5,6 +5,7 @@ from typing_extensions import Literal from ..._models import BaseModel +from .clusters.remediation import Remediation __all__ = [ "Cluster", @@ -19,7 +20,6 @@ "ControlPlaneNodePhaseTransition", "GPUWorkerNode", "GPUWorkerNodePhaseTransition", - "GPUWorkerNodeLatestRemediation", "PhaseTransition", "Volume", "ClusterConfig", @@ -123,91 +123,6 @@ class GPUWorkerNodePhaseTransition(BaseModel): """Timestamp when the phase transition occurred.""" -class GPUWorkerNodeLatestRemediation(BaseModel): - """ - Remediation represents a node remediation request for an instance. - An instance can have multiple remediations over time (e.g., failed attempts followed by retries). - """ - - id: str - - cluster_id: str - - instance_id: str - - mode: Literal[ - "REMEDIATION_MODE_VM_ONLY", - "REMEDIATION_MODE_HOST_AWARE", - "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT", - "REMEDIATION_MODE_REBOOT_VM", - ] - """Remediation mode specifies how the remediation should be performed. - - - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any - available host. - - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and - provisions a new one on a different host. - """ - - state: Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"] - """RemediationState represents the lifecycle state of a remediation. - - - `PENDING_APPROVAL`: Awaiting approval before processing can begin. - - `PENDING`: Approved and queued for processing. - - `RUNNING`: Actively being processed. - - `SUCCEEDED`: Successfully completed. - - `FAILED`: Failed with an error. - - `CANCELLED`: Cancelled by user or system. - - `AUTO_RESOLVED`: The underlying issue was automatically resolved before - processing. - """ - - trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"] - """RemediationTrigger specifies how the remediation was triggered. - - - `REMEDIATION_TRIGGER_MANUAL`: A user-initiated remediation (either via web UI - or API call). - - `REMEDIATION_TRIGGER_AUTOMATED`: A system-initiated remediation that requires - approval. - """ - - active_health_check_run_id: Optional[str] = None - """Active health check run ID (UUID) that triggered this remediation.""" - - create_time: Optional[datetime] = None - """When the remediation was created.""" - - end_time: Optional[datetime] = None - """When the remediation completed.""" - - error_message: Optional[str] = None - """Error message if the remediation failed.""" - - passive_health_check_event_id: Optional[str] = None - """Passive health check event ID that triggered this remediation.""" - - reason: Optional[str] = None - """User-provided reason for the remediation.""" - - requested_by: Optional[str] = None - """Who requested the remediation.""" - - review_comment: Optional[str] = None - """Review comment.""" - - review_time: Optional[datetime] = None - """When the remediation was reviewed.""" - - reviewed_by: Optional[str] = None - """Who reviewed the remediation.""" - - start_time: Optional[datetime] = None - """When processing started.""" - - update_time: Optional[datetime] = None - """When the remediation was last updated.""" - - class GPUWorkerNode(BaseModel): host_name: str @@ -230,7 +145,7 @@ class GPUWorkerNode(BaseModel): instance_id: Optional[str] = None - latest_remediation: Optional[GPUWorkerNodeLatestRemediation] = None + latest_remediation: Optional[Remediation] = None """ Remediation represents a node remediation request for an instance. An instance can have multiple remediations over time (e.g., failed attempts followed by diff --git a/src/together/types/beta/clusters/__init__.py b/src/together/types/beta/clusters/__init__.py index 4bf653714..89ca43ab3 100644 --- a/src/together/types/beta/clusters/__init__.py +++ b/src/together/types/beta/clusters/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations +from .remediation import Remediation as Remediation from .cluster_storage import ClusterStorage as ClusterStorage from .storage_list_params import StorageListParams as StorageListParams from .storage_create_params import StorageCreateParams as StorageCreateParams @@ -13,8 +14,3 @@ from .remediation_list_response import RemediationListResponse as RemediationListResponse from .remediation_reject_params import RemediationRejectParams as RemediationRejectParams from .remediation_approve_params import RemediationApproveParams as RemediationApproveParams -from .remediation_cancel_response import RemediationCancelResponse as RemediationCancelResponse -from .remediation_create_response import RemediationCreateResponse as RemediationCreateResponse -from .remediation_reject_response import RemediationRejectResponse as RemediationRejectResponse -from .remediation_approve_response import RemediationApproveResponse as RemediationApproveResponse -from .remediation_retrieve_response import RemediationRetrieveResponse as RemediationRetrieveResponse diff --git a/src/together/types/beta/clusters/remediation_cancel_response.py b/src/together/types/beta/clusters/remediation.py similarity index 97% rename from src/together/types/beta/clusters/remediation_cancel_response.py rename to src/together/types/beta/clusters/remediation.py index ec8975c9b..fc606a0f7 100644 --- a/src/together/types/beta/clusters/remediation_cancel_response.py +++ b/src/together/types/beta/clusters/remediation.py @@ -6,10 +6,10 @@ from ...._models import BaseModel -__all__ = ["RemediationCancelResponse"] +__all__ = ["Remediation"] -class RemediationCancelResponse(BaseModel): +class Remediation(BaseModel): """ Remediation represents a node remediation request for an instance. An instance can have multiple remediations over time (e.g., failed attempts followed by retries). diff --git a/src/together/types/beta/clusters/remediation_approve_response.py b/src/together/types/beta/clusters/remediation_approve_response.py deleted file mode 100644 index 040e6dab2..000000000 --- a/src/together/types/beta/clusters/remediation_approve_response.py +++ /dev/null @@ -1,94 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional -from datetime import datetime -from typing_extensions import Literal - -from ...._models import BaseModel - -__all__ = ["RemediationApproveResponse"] - - -class RemediationApproveResponse(BaseModel): - """ - Remediation represents a node remediation request for an instance. - An instance can have multiple remediations over time (e.g., failed attempts followed by retries). - """ - - id: str - - cluster_id: str - - instance_id: str - - mode: Literal[ - "REMEDIATION_MODE_VM_ONLY", - "REMEDIATION_MODE_HOST_AWARE", - "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT", - "REMEDIATION_MODE_REBOOT_VM", - ] - """Remediation mode specifies how the remediation should be performed. - - - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any - available host. - - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and - provisions a new one on a different host. - """ - - state: Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"] - """RemediationState represents the lifecycle state of a remediation. - - - `PENDING_APPROVAL`: Awaiting approval before processing can begin. - - `PENDING`: Approved and queued for processing. - - `RUNNING`: Actively being processed. - - `SUCCEEDED`: Successfully completed. - - `FAILED`: Failed with an error. - - `CANCELLED`: Cancelled by user or system. - - `AUTO_RESOLVED`: The underlying issue was automatically resolved before - processing. - """ - - trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"] - """RemediationTrigger specifies how the remediation was triggered. - - - `REMEDIATION_TRIGGER_MANUAL`: A user-initiated remediation (either via web UI - or API call). - - `REMEDIATION_TRIGGER_AUTOMATED`: A system-initiated remediation that requires - approval. - """ - - active_health_check_run_id: Optional[str] = None - """Active health check run ID (UUID) that triggered this remediation.""" - - create_time: Optional[datetime] = None - """When the remediation was created.""" - - end_time: Optional[datetime] = None - """When the remediation completed.""" - - error_message: Optional[str] = None - """Error message if the remediation failed.""" - - passive_health_check_event_id: Optional[str] = None - """Passive health check event ID that triggered this remediation.""" - - reason: Optional[str] = None - """User-provided reason for the remediation.""" - - requested_by: Optional[str] = None - """Who requested the remediation.""" - - review_comment: Optional[str] = None - """Review comment.""" - - review_time: Optional[datetime] = None - """When the remediation was reviewed.""" - - reviewed_by: Optional[str] = None - """Who reviewed the remediation.""" - - start_time: Optional[datetime] = None - """When processing started.""" - - update_time: Optional[datetime] = None - """When the remediation was last updated.""" diff --git a/src/together/types/beta/clusters/remediation_create_response.py b/src/together/types/beta/clusters/remediation_create_response.py deleted file mode 100644 index 2b148f932..000000000 --- a/src/together/types/beta/clusters/remediation_create_response.py +++ /dev/null @@ -1,94 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional -from datetime import datetime -from typing_extensions import Literal - -from ...._models import BaseModel - -__all__ = ["RemediationCreateResponse"] - - -class RemediationCreateResponse(BaseModel): - """ - Remediation represents a node remediation request for an instance. - An instance can have multiple remediations over time (e.g., failed attempts followed by retries). - """ - - id: str - - cluster_id: str - - instance_id: str - - mode: Literal[ - "REMEDIATION_MODE_VM_ONLY", - "REMEDIATION_MODE_HOST_AWARE", - "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT", - "REMEDIATION_MODE_REBOOT_VM", - ] - """Remediation mode specifies how the remediation should be performed. - - - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any - available host. - - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and - provisions a new one on a different host. - """ - - state: Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"] - """RemediationState represents the lifecycle state of a remediation. - - - `PENDING_APPROVAL`: Awaiting approval before processing can begin. - - `PENDING`: Approved and queued for processing. - - `RUNNING`: Actively being processed. - - `SUCCEEDED`: Successfully completed. - - `FAILED`: Failed with an error. - - `CANCELLED`: Cancelled by user or system. - - `AUTO_RESOLVED`: The underlying issue was automatically resolved before - processing. - """ - - trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"] - """RemediationTrigger specifies how the remediation was triggered. - - - `REMEDIATION_TRIGGER_MANUAL`: A user-initiated remediation (either via web UI - or API call). - - `REMEDIATION_TRIGGER_AUTOMATED`: A system-initiated remediation that requires - approval. - """ - - active_health_check_run_id: Optional[str] = None - """Active health check run ID (UUID) that triggered this remediation.""" - - create_time: Optional[datetime] = None - """When the remediation was created.""" - - end_time: Optional[datetime] = None - """When the remediation completed.""" - - error_message: Optional[str] = None - """Error message if the remediation failed.""" - - passive_health_check_event_id: Optional[str] = None - """Passive health check event ID that triggered this remediation.""" - - reason: Optional[str] = None - """User-provided reason for the remediation.""" - - requested_by: Optional[str] = None - """Who requested the remediation.""" - - review_comment: Optional[str] = None - """Review comment.""" - - review_time: Optional[datetime] = None - """When the remediation was reviewed.""" - - reviewed_by: Optional[str] = None - """Who reviewed the remediation.""" - - start_time: Optional[datetime] = None - """When processing started.""" - - update_time: Optional[datetime] = None - """When the remediation was last updated.""" diff --git a/src/together/types/beta/clusters/remediation_list_response.py b/src/together/types/beta/clusters/remediation_list_response.py index aaf15a9fb..174674960 100644 --- a/src/together/types/beta/clusters/remediation_list_response.py +++ b/src/together/types/beta/clusters/remediation_list_response.py @@ -1,97 +1,11 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Optional -from datetime import datetime -from typing_extensions import Literal +from typing import List from ...._models import BaseModel +from .remediation import Remediation -__all__ = ["RemediationListResponse", "Remediation"] - - -class Remediation(BaseModel): - """ - Remediation represents a node remediation request for an instance. - An instance can have multiple remediations over time (e.g., failed attempts followed by retries). - """ - - id: str - - cluster_id: str - - instance_id: str - - mode: Literal[ - "REMEDIATION_MODE_VM_ONLY", - "REMEDIATION_MODE_HOST_AWARE", - "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT", - "REMEDIATION_MODE_REBOOT_VM", - ] - """Remediation mode specifies how the remediation should be performed. - - - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any - available host. - - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and - provisions a new one on a different host. - """ - - state: Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"] - """RemediationState represents the lifecycle state of a remediation. - - - `PENDING_APPROVAL`: Awaiting approval before processing can begin. - - `PENDING`: Approved and queued for processing. - - `RUNNING`: Actively being processed. - - `SUCCEEDED`: Successfully completed. - - `FAILED`: Failed with an error. - - `CANCELLED`: Cancelled by user or system. - - `AUTO_RESOLVED`: The underlying issue was automatically resolved before - processing. - """ - - trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"] - """RemediationTrigger specifies how the remediation was triggered. - - - `REMEDIATION_TRIGGER_MANUAL`: A user-initiated remediation (either via web UI - or API call). - - `REMEDIATION_TRIGGER_AUTOMATED`: A system-initiated remediation that requires - approval. - """ - - active_health_check_run_id: Optional[str] = None - """Active health check run ID (UUID) that triggered this remediation.""" - - create_time: Optional[datetime] = None - """When the remediation was created.""" - - end_time: Optional[datetime] = None - """When the remediation completed.""" - - error_message: Optional[str] = None - """Error message if the remediation failed.""" - - passive_health_check_event_id: Optional[str] = None - """Passive health check event ID that triggered this remediation.""" - - reason: Optional[str] = None - """User-provided reason for the remediation.""" - - requested_by: Optional[str] = None - """Who requested the remediation.""" - - review_comment: Optional[str] = None - """Review comment.""" - - review_time: Optional[datetime] = None - """When the remediation was reviewed.""" - - reviewed_by: Optional[str] = None - """Who reviewed the remediation.""" - - start_time: Optional[datetime] = None - """When processing started.""" - - update_time: Optional[datetime] = None - """When the remediation was last updated.""" +__all__ = ["RemediationListResponse"] class RemediationListResponse(BaseModel): diff --git a/src/together/types/beta/clusters/remediation_reject_response.py b/src/together/types/beta/clusters/remediation_reject_response.py deleted file mode 100644 index e90306bb6..000000000 --- a/src/together/types/beta/clusters/remediation_reject_response.py +++ /dev/null @@ -1,94 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional -from datetime import datetime -from typing_extensions import Literal - -from ...._models import BaseModel - -__all__ = ["RemediationRejectResponse"] - - -class RemediationRejectResponse(BaseModel): - """ - Remediation represents a node remediation request for an instance. - An instance can have multiple remediations over time (e.g., failed attempts followed by retries). - """ - - id: str - - cluster_id: str - - instance_id: str - - mode: Literal[ - "REMEDIATION_MODE_VM_ONLY", - "REMEDIATION_MODE_HOST_AWARE", - "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT", - "REMEDIATION_MODE_REBOOT_VM", - ] - """Remediation mode specifies how the remediation should be performed. - - - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any - available host. - - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and - provisions a new one on a different host. - """ - - state: Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"] - """RemediationState represents the lifecycle state of a remediation. - - - `PENDING_APPROVAL`: Awaiting approval before processing can begin. - - `PENDING`: Approved and queued for processing. - - `RUNNING`: Actively being processed. - - `SUCCEEDED`: Successfully completed. - - `FAILED`: Failed with an error. - - `CANCELLED`: Cancelled by user or system. - - `AUTO_RESOLVED`: The underlying issue was automatically resolved before - processing. - """ - - trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"] - """RemediationTrigger specifies how the remediation was triggered. - - - `REMEDIATION_TRIGGER_MANUAL`: A user-initiated remediation (either via web UI - or API call). - - `REMEDIATION_TRIGGER_AUTOMATED`: A system-initiated remediation that requires - approval. - """ - - active_health_check_run_id: Optional[str] = None - """Active health check run ID (UUID) that triggered this remediation.""" - - create_time: Optional[datetime] = None - """When the remediation was created.""" - - end_time: Optional[datetime] = None - """When the remediation completed.""" - - error_message: Optional[str] = None - """Error message if the remediation failed.""" - - passive_health_check_event_id: Optional[str] = None - """Passive health check event ID that triggered this remediation.""" - - reason: Optional[str] = None - """User-provided reason for the remediation.""" - - requested_by: Optional[str] = None - """Who requested the remediation.""" - - review_comment: Optional[str] = None - """Review comment.""" - - review_time: Optional[datetime] = None - """When the remediation was reviewed.""" - - reviewed_by: Optional[str] = None - """Who reviewed the remediation.""" - - start_time: Optional[datetime] = None - """When processing started.""" - - update_time: Optional[datetime] = None - """When the remediation was last updated.""" diff --git a/src/together/types/beta/clusters/remediation_retrieve_response.py b/src/together/types/beta/clusters/remediation_retrieve_response.py deleted file mode 100644 index ffb6bd3b4..000000000 --- a/src/together/types/beta/clusters/remediation_retrieve_response.py +++ /dev/null @@ -1,94 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional -from datetime import datetime -from typing_extensions import Literal - -from ...._models import BaseModel - -__all__ = ["RemediationRetrieveResponse"] - - -class RemediationRetrieveResponse(BaseModel): - """ - Remediation represents a node remediation request for an instance. - An instance can have multiple remediations over time (e.g., failed attempts followed by retries). - """ - - id: str - - cluster_id: str - - instance_id: str - - mode: Literal[ - "REMEDIATION_MODE_VM_ONLY", - "REMEDIATION_MODE_HOST_AWARE", - "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT", - "REMEDIATION_MODE_REBOOT_VM", - ] - """Remediation mode specifies how the remediation should be performed. - - - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any - available host. - - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and - provisions a new one on a different host. - """ - - state: Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"] - """RemediationState represents the lifecycle state of a remediation. - - - `PENDING_APPROVAL`: Awaiting approval before processing can begin. - - `PENDING`: Approved and queued for processing. - - `RUNNING`: Actively being processed. - - `SUCCEEDED`: Successfully completed. - - `FAILED`: Failed with an error. - - `CANCELLED`: Cancelled by user or system. - - `AUTO_RESOLVED`: The underlying issue was automatically resolved before - processing. - """ - - trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"] - """RemediationTrigger specifies how the remediation was triggered. - - - `REMEDIATION_TRIGGER_MANUAL`: A user-initiated remediation (either via web UI - or API call). - - `REMEDIATION_TRIGGER_AUTOMATED`: A system-initiated remediation that requires - approval. - """ - - active_health_check_run_id: Optional[str] = None - """Active health check run ID (UUID) that triggered this remediation.""" - - create_time: Optional[datetime] = None - """When the remediation was created.""" - - end_time: Optional[datetime] = None - """When the remediation completed.""" - - error_message: Optional[str] = None - """Error message if the remediation failed.""" - - passive_health_check_event_id: Optional[str] = None - """Passive health check event ID that triggered this remediation.""" - - reason: Optional[str] = None - """User-provided reason for the remediation.""" - - requested_by: Optional[str] = None - """Who requested the remediation.""" - - review_comment: Optional[str] = None - """Review comment.""" - - review_time: Optional[datetime] = None - """When the remediation was reviewed.""" - - reviewed_by: Optional[str] = None - """Who reviewed the remediation.""" - - start_time: Optional[datetime] = None - """When processing started.""" - - update_time: Optional[datetime] = None - """When the remediation was last updated.""" diff --git a/tests/api_resources/beta/clusters/test_remediations.py b/tests/api_resources/beta/clusters/test_remediations.py index 7fb96a882..7c50553d5 100644 --- a/tests/api_resources/beta/clusters/test_remediations.py +++ b/tests/api_resources/beta/clusters/test_remediations.py @@ -10,12 +10,8 @@ from together import Together, AsyncTogether from tests.utils import assert_matches_type from together.types.beta.clusters import ( + Remediation, RemediationListResponse, - RemediationCancelResponse, - RemediationCreateResponse, - RemediationRejectResponse, - RemediationApproveResponse, - RemediationRetrieveResponse, ) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -31,7 +27,7 @@ def test_method_create(self, client: Together) -> None: cluster_id="cluster_id", mode="REMEDIATION_MODE_VM_ONLY", ) - assert_matches_type(RemediationCreateResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) @parametrize def test_method_create_with_all_params(self, client: Together) -> None: @@ -42,7 +38,7 @@ def test_method_create_with_all_params(self, client: Together) -> None: remediation_id="remediation_id", reason="reason", ) - assert_matches_type(RemediationCreateResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) @parametrize def test_raw_response_create(self, client: Together) -> None: @@ -55,7 +51,7 @@ def test_raw_response_create(self, client: Together) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" remediation = response.parse() - assert_matches_type(RemediationCreateResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) @parametrize def test_streaming_response_create(self, client: Together) -> None: @@ -68,7 +64,7 @@ def test_streaming_response_create(self, client: Together) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" remediation = response.parse() - assert_matches_type(RemediationCreateResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) assert cast(Any, response.is_closed) is True @@ -95,7 +91,7 @@ def test_method_retrieve(self, client: Together) -> None: cluster_id="cluster_id", instance_id="instance_id", ) - assert_matches_type(RemediationRetrieveResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Together) -> None: @@ -108,7 +104,7 @@ def test_raw_response_retrieve(self, client: Together) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" remediation = response.parse() - assert_matches_type(RemediationRetrieveResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) @parametrize def test_streaming_response_retrieve(self, client: Together) -> None: @@ -121,7 +117,7 @@ def test_streaming_response_retrieve(self, client: Together) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" remediation = response.parse() - assert_matches_type(RemediationRetrieveResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) assert cast(Any, response.is_closed) is True @@ -216,7 +212,7 @@ def test_method_approve(self, client: Together) -> None: cluster_id="cluster_id", instance_id="instance_id", ) - assert_matches_type(RemediationApproveResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) @parametrize def test_method_approve_with_all_params(self, client: Together) -> None: @@ -226,7 +222,7 @@ def test_method_approve_with_all_params(self, client: Together) -> None: instance_id="instance_id", comment="comment", ) - assert_matches_type(RemediationApproveResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) @parametrize def test_raw_response_approve(self, client: Together) -> None: @@ -239,7 +235,7 @@ def test_raw_response_approve(self, client: Together) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" remediation = response.parse() - assert_matches_type(RemediationApproveResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) @parametrize def test_streaming_response_approve(self, client: Together) -> None: @@ -252,7 +248,7 @@ def test_streaming_response_approve(self, client: Together) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" remediation = response.parse() - assert_matches_type(RemediationApproveResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) assert cast(Any, response.is_closed) is True @@ -286,7 +282,7 @@ def test_method_cancel(self, client: Together) -> None: cluster_id="cluster_id", instance_id="instance_id", ) - assert_matches_type(RemediationCancelResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) @parametrize def test_raw_response_cancel(self, client: Together) -> None: @@ -299,7 +295,7 @@ def test_raw_response_cancel(self, client: Together) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" remediation = response.parse() - assert_matches_type(RemediationCancelResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) @parametrize def test_streaming_response_cancel(self, client: Together) -> None: @@ -312,7 +308,7 @@ def test_streaming_response_cancel(self, client: Together) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" remediation = response.parse() - assert_matches_type(RemediationCancelResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) assert cast(Any, response.is_closed) is True @@ -346,7 +342,7 @@ def test_method_reject(self, client: Together) -> None: cluster_id="cluster_id", instance_id="instance_id", ) - assert_matches_type(RemediationRejectResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) @parametrize def test_method_reject_with_all_params(self, client: Together) -> None: @@ -356,7 +352,7 @@ def test_method_reject_with_all_params(self, client: Together) -> None: instance_id="instance_id", comment="comment", ) - assert_matches_type(RemediationRejectResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) @parametrize def test_raw_response_reject(self, client: Together) -> None: @@ -369,7 +365,7 @@ def test_raw_response_reject(self, client: Together) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" remediation = response.parse() - assert_matches_type(RemediationRejectResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) @parametrize def test_streaming_response_reject(self, client: Together) -> None: @@ -382,7 +378,7 @@ def test_streaming_response_reject(self, client: Together) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" remediation = response.parse() - assert_matches_type(RemediationRejectResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) assert cast(Any, response.is_closed) is True @@ -422,7 +418,7 @@ async def test_method_create(self, async_client: AsyncTogether) -> None: cluster_id="cluster_id", mode="REMEDIATION_MODE_VM_ONLY", ) - assert_matches_type(RemediationCreateResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) @parametrize async def test_method_create_with_all_params(self, async_client: AsyncTogether) -> None: @@ -433,7 +429,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncTogether) remediation_id="remediation_id", reason="reason", ) - assert_matches_type(RemediationCreateResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) @parametrize async def test_raw_response_create(self, async_client: AsyncTogether) -> None: @@ -446,7 +442,7 @@ async def test_raw_response_create(self, async_client: AsyncTogether) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" remediation = await response.parse() - assert_matches_type(RemediationCreateResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) @parametrize async def test_streaming_response_create(self, async_client: AsyncTogether) -> None: @@ -459,7 +455,7 @@ async def test_streaming_response_create(self, async_client: AsyncTogether) -> N assert response.http_request.headers.get("X-Stainless-Lang") == "python" remediation = await response.parse() - assert_matches_type(RemediationCreateResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) assert cast(Any, response.is_closed) is True @@ -486,7 +482,7 @@ async def test_method_retrieve(self, async_client: AsyncTogether) -> None: cluster_id="cluster_id", instance_id="instance_id", ) - assert_matches_type(RemediationRetrieveResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncTogether) -> None: @@ -499,7 +495,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncTogether) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" remediation = await response.parse() - assert_matches_type(RemediationRetrieveResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncTogether) -> None: @@ -512,7 +508,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncTogether) -> assert response.http_request.headers.get("X-Stainless-Lang") == "python" remediation = await response.parse() - assert_matches_type(RemediationRetrieveResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) assert cast(Any, response.is_closed) is True @@ -607,7 +603,7 @@ async def test_method_approve(self, async_client: AsyncTogether) -> None: cluster_id="cluster_id", instance_id="instance_id", ) - assert_matches_type(RemediationApproveResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) @parametrize async def test_method_approve_with_all_params(self, async_client: AsyncTogether) -> None: @@ -617,7 +613,7 @@ async def test_method_approve_with_all_params(self, async_client: AsyncTogether) instance_id="instance_id", comment="comment", ) - assert_matches_type(RemediationApproveResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) @parametrize async def test_raw_response_approve(self, async_client: AsyncTogether) -> None: @@ -630,7 +626,7 @@ async def test_raw_response_approve(self, async_client: AsyncTogether) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" remediation = await response.parse() - assert_matches_type(RemediationApproveResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) @parametrize async def test_streaming_response_approve(self, async_client: AsyncTogether) -> None: @@ -643,7 +639,7 @@ async def test_streaming_response_approve(self, async_client: AsyncTogether) -> assert response.http_request.headers.get("X-Stainless-Lang") == "python" remediation = await response.parse() - assert_matches_type(RemediationApproveResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) assert cast(Any, response.is_closed) is True @@ -677,7 +673,7 @@ async def test_method_cancel(self, async_client: AsyncTogether) -> None: cluster_id="cluster_id", instance_id="instance_id", ) - assert_matches_type(RemediationCancelResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) @parametrize async def test_raw_response_cancel(self, async_client: AsyncTogether) -> None: @@ -690,7 +686,7 @@ async def test_raw_response_cancel(self, async_client: AsyncTogether) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" remediation = await response.parse() - assert_matches_type(RemediationCancelResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) @parametrize async def test_streaming_response_cancel(self, async_client: AsyncTogether) -> None: @@ -703,7 +699,7 @@ async def test_streaming_response_cancel(self, async_client: AsyncTogether) -> N assert response.http_request.headers.get("X-Stainless-Lang") == "python" remediation = await response.parse() - assert_matches_type(RemediationCancelResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) assert cast(Any, response.is_closed) is True @@ -737,7 +733,7 @@ async def test_method_reject(self, async_client: AsyncTogether) -> None: cluster_id="cluster_id", instance_id="instance_id", ) - assert_matches_type(RemediationRejectResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) @parametrize async def test_method_reject_with_all_params(self, async_client: AsyncTogether) -> None: @@ -747,7 +743,7 @@ async def test_method_reject_with_all_params(self, async_client: AsyncTogether) instance_id="instance_id", comment="comment", ) - assert_matches_type(RemediationRejectResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) @parametrize async def test_raw_response_reject(self, async_client: AsyncTogether) -> None: @@ -760,7 +756,7 @@ async def test_raw_response_reject(self, async_client: AsyncTogether) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" remediation = await response.parse() - assert_matches_type(RemediationRejectResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) @parametrize async def test_streaming_response_reject(self, async_client: AsyncTogether) -> None: @@ -773,7 +769,7 @@ async def test_streaming_response_reject(self, async_client: AsyncTogether) -> N assert response.http_request.headers.get("X-Stainless-Lang") == "python" remediation = await response.parse() - assert_matches_type(RemediationRejectResponse, remediation, path=["response"]) + assert_matches_type(Remediation, remediation, path=["response"]) assert cast(Any, response.is_closed) is True From c5f5a6d90f0d87d9005fada2ff12d246a0ab5aa7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 18:29:34 +0000 Subject: [PATCH 11/14] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 37b2bba11..cfa550f54 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 81 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-c8200db997f998a51b2b469b69fcc3a24032e50b45aaaf340c2c176473393adc.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-3ae47a93fc03893f34b2e442ab8194c1f4224d9d7fe2d02b6342dbe03862f1e7.yml openapi_spec_hash: 2544f6840cf7ad23eae3fe8bffed4c4b config_hash: b35d5968fb07cce1c1be735f874898b1 From 5047a5c530460b0e8493b985f1bbff18cb9c2025 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 15 May 2026 20:08:39 +0000 Subject: [PATCH 12/14] Add beta cluster remediation CLI commands Co-authored-by: Blaine Kasten --- src/together/lib/cli/__init__.py | 33 ++++ .../api/beta/clusters/remediations/_util.py | 91 +++++++++++ .../api/beta/clusters/remediations/approve.py | 36 +++++ .../api/beta/clusters/remediations/cancel.py | 30 ++++ .../api/beta/clusters/remediations/create.py | 48 ++++++ .../api/beta/clusters/remediations/list.py | 65 ++++++++ .../api/beta/clusters/remediations/reject.py | 36 +++++ src/together/lib/cli/utils/_help_examples.py | 20 +++ tests/cli/test_beta_clusters.py | 146 ++++++++++++++++++ 9 files changed, 505 insertions(+) create mode 100644 src/together/lib/cli/api/beta/clusters/remediations/_util.py create mode 100644 src/together/lib/cli/api/beta/clusters/remediations/approve.py create mode 100644 src/together/lib/cli/api/beta/clusters/remediations/cancel.py create mode 100644 src/together/lib/cli/api/beta/clusters/remediations/create.py create mode 100644 src/together/lib/cli/api/beta/clusters/remediations/list.py create mode 100644 src/together/lib/cli/api/beta/clusters/remediations/reject.py diff --git a/src/together/lib/cli/__init__.py b/src/together/lib/cli/__init__.py index 445871f45..9a61f0cf8 100644 --- a/src/together/lib/cli/__init__.py +++ b/src/together/lib/cli/__init__.py @@ -61,6 +61,7 @@ FINE_TUNING_DOWNLOAD_HELP_EXAMPLES, BETA_CLUSTERS_STORAGE_HELP_EXAMPLES, FILES_RETRIEVE_CONTENT_HELP_EXAMPLES, + BETA_CLUSTERS_REMEDIATIONS_HELP_EXAMPLES, BETA_CLUSTERS_STORAGE_CREATE_HELP_EXAMPLES, BETA_CLUSTERS_STORAGE_UPDATE_HELP_EXAMPLES, BETA_CLUSTERS_GET_CREDENTIALS_HELP_EXAMPLES, @@ -486,6 +487,38 @@ async def run_command() -> None: ) storage_app.command((f"{_CLI}.beta.clusters.storage.delete:delete"), help="Delete a storage volume", alias="-d") +### Clusters > Remediations API commands +remediations_app = clusters_app.command( + App( + name="remediations", + help="Manage node remediations", + group="Subcommands", + help_epilogue=BETA_CLUSTERS_REMEDIATIONS_HELP_EXAMPLES, + ) +) +remediations_app.command( + (f"{_CLI}.beta.clusters.remediations.create:create"), + alias="-c", + help="Create a node remediation", +) +remediations_app.command( + (f"{_CLI}.beta.clusters.remediations.list:list"), + alias="ls", + help="List node remediations", +) +remediations_app.command( + (f"{_CLI}.beta.clusters.remediations.approve:approve"), + help="Approve a pending remediation", +) +remediations_app.command( + (f"{_CLI}.beta.clusters.remediations.cancel:cancel"), + help="Cancel a pending remediation", +) +remediations_app.command( + (f"{_CLI}.beta.clusters.remediations.reject:reject"), + help="Reject a pending remediation", +) + ### Jig commands jig_app = beta_app.command( App(name="jig", help="Build, deploy, and manage custom containers", help_epilogue=JIG_HELP_EXAMPLES) diff --git a/src/together/lib/cli/api/beta/clusters/remediations/_util.py b/src/together/lib/cli/api/beta/clusters/remediations/_util.py new file mode 100644 index 000000000..946cdccda --- /dev/null +++ b/src/together/lib/cli/api/beta/clusters/remediations/_util.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +import sys +from typing import List, Literal, TypeVar, Optional, cast, get_args +from datetime import datetime + +from together import omit +from together._types import Omit +from together.lib.cli.utils.config import CLIConfigParameter +from together.lib.cli.utils._console import console +from together.lib.cli.components.list import ListTable +from together.types.beta.clusters.remediation import Remediation + +EMPTY_MESSAGE = "No remediations found for this cluster." + +T = TypeVar("T") +RemediationState = Literal[ + "PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED" +] +_REMEDIATION_STATES = set(get_args(RemediationState)) + + +def omit_if_none(value: Optional[T]) -> T | Omit: + return omit if value is None else value + + +def parse_states(state: Optional[str]) -> List[RemediationState] | Omit: + if state is None: + return omit + states = [part.strip() for part in state.split(",") if part.strip()] + if not states: + return omit + + invalid_states = sorted(set(states) - _REMEDIATION_STATES) + if invalid_states: + console.print(f"[red]Error:[/red] Invalid remediation state: {', '.join(invalid_states)}") + sys.exit(1) + + return cast(List[RemediationState], states) + + +def format_time(value: Optional[datetime]) -> str: + if value is None: + return "-" + return value.isoformat() + + +def print_remediations(remediations: List[Remediation]) -> None: + table = ListTable(title="Cluster Remediations", empty_message=EMPTY_MESSAGE) + table.add_primary_column("ID") + table.add_column("Instance") + table.add_column("State") + table.add_column("Mode") + table.add_column("Trigger") + table.add_column("Created") + + for remediation in remediations: + table.add_row( + remediation.id, + remediation.instance_id, + remediation.state, + remediation.mode.replace("REMEDIATION_MODE_", ""), + remediation.trigger.replace("REMEDIATION_TRIGGER_", ""), + format_time(remediation.create_time), + ) + + console.print(table) + + +async def resolve_remediation(config: CLIConfigParameter, remediation_id: str) -> Remediation: + clusters = await config.client.beta.clusters.list() + + for cluster in clusters.clusters: + page_token: str | Omit = omit + while True: + response = await config.client.beta.clusters.remediations.list( + "-", + cluster_id=cluster.cluster_id, + page_size=100, + page_token=page_token, + ) + for remediation in response.remediations: + if remediation.id == remediation_id: + return remediation + + if not response.has_next or not response.next_page_token: + break + page_token = response.next_page_token + + console.print(f"[red]Error:[/red] Remediation not found: {remediation_id}") + sys.exit(1) diff --git a/src/together/lib/cli/api/beta/clusters/remediations/approve.py b/src/together/lib/cli/api/beta/clusters/remediations/approve.py new file mode 100644 index 000000000..46428c161 --- /dev/null +++ b/src/together/lib/cli/api/beta/clusters/remediations/approve.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from typing import Optional, Annotated + +from cyclopts import Parameter + +from together._utils._json import openapi_dumps +from together.lib.cli.utils.config import CLIConfigParameter +from together.lib.cli.utils._console import console +from together.lib.cli.components.loader import show_loading_status +from together.lib.cli.api.beta.clusters.remediations._util import omit_if_none, resolve_remediation + + +async def approve( + remediation_id: str, + comment: Annotated[Optional[str], Parameter(help="Comment explaining the approval")] = None, + *, + config: CLIConfigParameter, +) -> None: + """Approve a pending remediation.""" + remediation = await show_loading_status("Finding remediation...", resolve_remediation(config, remediation_id)) + response = await show_loading_status( + "Approving remediation...", + config.client.beta.clusters.remediations.approve( + remediation_id, + cluster_id=remediation.cluster_id, + instance_id=remediation.instance_id, + comment=omit_if_none(comment), + ), + ) + + if config.json: + console.print_json(openapi_dumps(response).decode("utf-8")) + return + + console.print(f"[blue]Remediation approved.[/blue] ({response.id})") diff --git a/src/together/lib/cli/api/beta/clusters/remediations/cancel.py b/src/together/lib/cli/api/beta/clusters/remediations/cancel.py new file mode 100644 index 000000000..fcd34f542 --- /dev/null +++ b/src/together/lib/cli/api/beta/clusters/remediations/cancel.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from together._utils._json import openapi_dumps +from together.lib.cli.utils.config import CLIConfigParameter +from together.lib.cli.utils._console import console +from together.lib.cli.components.loader import show_loading_status +from together.lib.cli.api.beta.clusters.remediations._util import resolve_remediation + + +async def cancel( + remediation_id: str, + *, + config: CLIConfigParameter, +) -> None: + """Cancel a pending remediation.""" + remediation = await show_loading_status("Finding remediation...", resolve_remediation(config, remediation_id)) + response = await show_loading_status( + "Cancelling remediation...", + config.client.beta.clusters.remediations.cancel( + remediation_id, + cluster_id=remediation.cluster_id, + instance_id=remediation.instance_id, + ), + ) + + if config.json: + console.print_json(openapi_dumps(response).decode("utf-8")) + return + + console.print(f"[blue]Remediation cancelled.[/blue] ({response.id})") diff --git a/src/together/lib/cli/api/beta/clusters/remediations/create.py b/src/together/lib/cli/api/beta/clusters/remediations/create.py new file mode 100644 index 000000000..3ef21e7c4 --- /dev/null +++ b/src/together/lib/cli/api/beta/clusters/remediations/create.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from typing import Literal, Optional, Annotated + +from cyclopts import Parameter + +from together._utils._json import openapi_dumps +from together.lib.cli.utils.config import CLIConfigParameter +from together.lib.cli.utils._console import console +from together.lib.cli.components.loader import show_loading_status +from together.lib.cli.api.beta.clusters.remediations._util import omit_if_none + +RemediationModeParameter = Annotated[ + Literal[ + "REMEDIATION_MODE_VM_ONLY", + "REMEDIATION_MODE_HOST_AWARE", + "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT", + "REMEDIATION_MODE_REBOOT_VM", + ], + Parameter(help="How the remediation should be performed"), +] + + +async def create( + cluster_id: str, + instance_id: str, + *, + mode: RemediationModeParameter, + remediation_id: Annotated[Optional[str], Parameter(help="Client-specified ID for idempotency")] = None, + reason: Annotated[Optional[str], Parameter(help="Reason for the remediation")] = None, + config: CLIConfigParameter, +) -> None: + """Create a node remediation for an instance.""" + request = config.client.beta.clusters.remediations.create( + instance_id, + cluster_id=cluster_id, + mode=mode, + remediation_id=omit_if_none(remediation_id), + reason=omit_if_none(reason), + ) + + response = await show_loading_status("Creating remediation...", request) + if config.json: + console.print_json(openapi_dumps(response).decode("utf-8")) + return + + console.print("[blue]Remediation created successfully[/blue]") + console.print(f"[primary]Remediation ID:[/primary] {response.id}") diff --git a/src/together/lib/cli/api/beta/clusters/remediations/list.py b/src/together/lib/cli/api/beta/clusters/remediations/list.py new file mode 100644 index 000000000..7c0124daf --- /dev/null +++ b/src/together/lib/cli/api/beta/clusters/remediations/list.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from typing import Literal, Optional, Annotated + +from cyclopts import Parameter + +from together._utils._json import openapi_dumps +from together.lib.cli.utils.config import CLIConfigParameter +from together.lib.cli.utils._console import console +from together.lib.cli.components.loader import show_loading_status +from together.lib.cli.api.beta.clusters.remediations._util import ( + omit_if_none, + parse_states, + print_remediations, +) + +OptionalRemediationModeParameter = Annotated[ + Optional[ + Literal[ + "REMEDIATION_MODE_VM_ONLY", + "REMEDIATION_MODE_HOST_AWARE", + "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT", + "REMEDIATION_MODE_REBOOT_VM", + ] + ], + Parameter(help="Filter by remediation mode"), +] + + +async def list( + cluster_id: str, + instance_id: Annotated[Optional[str], Parameter(help="Instance ID to list remediations for")] = None, + mode: OptionalRemediationModeParameter = None, + state: Annotated[Optional[str], Parameter(help="Comma-separated remediation states to include")] = None, + order_by: Annotated[Optional[str], Parameter(help="Order by expression")] = None, + page_size: Annotated[Optional[int], Parameter(help="Maximum results to return")] = None, + page_token: Annotated[Optional[str], Parameter(help="Pagination token from a previous request")] = None, + *, + config: CLIConfigParameter, +) -> None: + """List node remediations for a cluster or instance.""" + response = await show_loading_status( + "Loading remediations...", + config.client.beta.clusters.remediations.list( + instance_id or "-", + cluster_id=cluster_id, + mode=omit_if_none(mode), + state=parse_states(state), + order_by=omit_if_none(order_by), + page_size=omit_if_none(page_size), + page_token=omit_if_none(page_token), + ), + ) + + if config.json: + console.print_json(openapi_dumps(response).decode("utf-8")) + return + + print_remediations(response.remediations) + if response.has_next and response.next_page_token: + command = f"tg beta clusters remediations list {cluster_id}" + if instance_id: + command += f" {instance_id}" + console.print("\n[blue dim]To display the next page, run:[/blue dim]") + console.print(f" [dim]-[/dim] [white]{command} --page-token {response.next_page_token}[/white]") diff --git a/src/together/lib/cli/api/beta/clusters/remediations/reject.py b/src/together/lib/cli/api/beta/clusters/remediations/reject.py new file mode 100644 index 000000000..a5ecb7803 --- /dev/null +++ b/src/together/lib/cli/api/beta/clusters/remediations/reject.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from typing import Optional, Annotated + +from cyclopts import Parameter + +from together._utils._json import openapi_dumps +from together.lib.cli.utils.config import CLIConfigParameter +from together.lib.cli.utils._console import console +from together.lib.cli.components.loader import show_loading_status +from together.lib.cli.api.beta.clusters.remediations._util import omit_if_none, resolve_remediation + + +async def reject( + remediation_id: str, + comment: Annotated[Optional[str], Parameter(help="Comment explaining the rejection")] = None, + *, + config: CLIConfigParameter, +) -> None: + """Reject a pending remediation.""" + remediation = await show_loading_status("Finding remediation...", resolve_remediation(config, remediation_id)) + response = await show_loading_status( + "Rejecting remediation...", + config.client.beta.clusters.remediations.reject( + remediation_id, + cluster_id=remediation.cluster_id, + instance_id=remediation.instance_id, + comment=omit_if_none(comment), + ), + ) + + if config.json: + console.print_json(openapi_dumps(response).decode("utf-8")) + return + + console.print(f"[blue]Remediation rejected.[/blue] ({response.id})") diff --git a/src/together/lib/cli/utils/_help_examples.py b/src/together/lib/cli/utils/_help_examples.py index 224eee8a4..c16e05e69 100644 --- a/src/together/lib/cli/utils/_help_examples.py +++ b/src/together/lib/cli/utils/_help_examples.py @@ -251,6 +251,10 @@ [dim]-[/dim] Update or delete a cluster: [primary]tg beta clusters update --num-gpus 16 --cluster-type KUBERNETES[/primary] [primary]tg beta clusters delete [/primary] + +[dim]-[/dim] Manage node remediations: + [primary]tg beta clusters remediations list [/primary] + [primary]tg beta clusters remediations create --mode REMEDIATION_MODE_VM_ONLY[/primary] """ BETA_CLUSTERS_CREATE_HELP_EXAMPLES = """[dim]Examples:[/dim] @@ -329,6 +333,22 @@ [primary]tg beta clusters storage update --size-tib 4[/primary] """ +BETA_CLUSTERS_REMEDIATIONS_HELP_EXAMPLES = """[dim]Examples:[/dim] +[dim]-[/dim] List all remediations for a cluster: + [primary]tg beta clusters remediations list [/primary] + +[dim]-[/dim] List remediations for one instance: + [primary]tg beta clusters remediations list [/primary] + +[dim]-[/dim] Create a remediation: + [primary]tg beta clusters remediations create --mode REMEDIATION_MODE_VM_ONLY --reason "node unhealthy"[/primary] + +[dim]-[/dim] Review automated remediations: + [primary]tg beta clusters remediations approve [/primary] + [primary]tg beta clusters remediations reject --comment "already handled"[/primary] + [primary]tg beta clusters remediations cancel [/primary] +""" + ## Beta > Jig commands JIG_HELP_EXAMPLES = """[dim]Examples:[/dim] diff --git a/tests/cli/test_beta_clusters.py b/tests/cli/test_beta_clusters.py index 5f2bf86ac..ac42ea4c9 100644 --- a/tests/cli/test_beta_clusters.py +++ b/tests/cli/test_beta_clusters.py @@ -53,6 +53,28 @@ def _cluster_body(cluster_id: str = "cluster-1", name: str = "my-cluster", **ove } +def _remediation_body(remediation_id: str = "rem-1", **overrides: Any) -> dict[str, Any]: + body: dict[str, Any] = { + "id": remediation_id, + "cluster_id": "c1", + "instance_id": "i1", + "mode": "REMEDIATION_MODE_VM_ONLY", + "state": "PENDING_APPROVAL", + "trigger": "REMEDIATION_TRIGGER_AUTOMATED", + "reason": "health check failed", + } + body.update(overrides) + return body + + +def _remediation_list_body(*remediations: dict[str, Any]) -> dict[str, Any]: + return { + "has_next": False, + "next_page_token": "", + "remediations": list(remediations), + } + + class TestBetaClustersList: @pytest.mark.respx(base_url=base_url) def test_list_table(self, respx_mock: MockRouter, cli_runner: CliRunner) -> None: @@ -238,3 +260,127 @@ def test_storage_delete_json(self, respx_mock: MockRouter, cli_runner: CliRunner result = cli_runner.invoke(["beta", "clusters", "storage", "delete", "vol-1", "--json"]) assert json.loads(result.output) == {"success": True} assert result.exit_code == 0 + + +class TestBetaClustersRemediations: + @pytest.mark.respx(base_url=base_url) + def test_remediations_create_json(self, respx_mock: MockRouter, cli_runner: CliRunner) -> None: + route = respx_mock.post("/compute/clusters/c1/instances/i1/remediations").mock( + return_value=httpx.Response(200, json=_remediation_body("rem-created", state="PENDING")) + ) + result = cli_runner.invoke( + [ + "beta", + "clusters", + "remediations", + "create", + "c1", + "i1", + "--mode", + "REMEDIATION_MODE_VM_ONLY", + "--reason", + "node unhealthy", + "--remediation-id", + "rem-created", + "--json", + ], + ) + + assert json.loads(result.output)["id"] == "rem-created" + request = cast(Call, route.calls[0]).request + assert request.url.params["remediation_id"] == "rem-created" + assert json.loads(request.content.decode()) == { + "mode": "REMEDIATION_MODE_VM_ONLY", + "reason": "node unhealthy", + } + assert result.exit_code == 0 + + @pytest.mark.respx(base_url=base_url) + def test_remediations_list_uses_wildcard_when_instance_id_omitted( + self, respx_mock: MockRouter, cli_runner: CliRunner + ) -> None: + payload = _remediation_list_body(_remediation_body()) + route = respx_mock.get("/compute/clusters/c1/instances/-/remediations").mock( + return_value=httpx.Response(200, json=payload) + ) + result = cli_runner.invoke(["beta", "clusters", "remediations", "list", "c1", "--json"]) + + assert json.loads(result.output) == payload + assert cast(Call, route.calls[0]).request.url.path == "/compute/clusters/c1/instances/-/remediations" + assert result.exit_code == 0 + + @pytest.mark.respx(base_url=base_url) + def test_remediations_list_accepts_instance_id(self, respx_mock: MockRouter, cli_runner: CliRunner) -> None: + payload = _remediation_list_body(_remediation_body()) + route = respx_mock.get("/compute/clusters/c1/instances/i1/remediations").mock( + return_value=httpx.Response(200, json=payload) + ) + result = cli_runner.invoke(["beta", "clusters", "remediations", "list", "c1", "i1", "--json"]) + + assert json.loads(result.output) == payload + assert cast(Call, route.calls[0]).request.url.path == "/compute/clusters/c1/instances/i1/remediations" + assert result.exit_code == 0 + + @pytest.mark.respx(base_url=base_url) + def test_remediations_approve_resolves_cluster_and_instance( + self, respx_mock: MockRouter, cli_runner: CliRunner + ) -> None: + respx_mock.get("/compute/clusters").mock( + return_value=httpx.Response(200, json={"clusters": [_cluster_body("c1")]}) + ) + respx_mock.get("/compute/clusters/c1/instances/-/remediations").mock( + return_value=httpx.Response(200, json=_remediation_list_body(_remediation_body("rem-approve"))) + ) + route = respx_mock.post("/compute/clusters/c1/instances/i1/remediations/rem-approve/approve").mock( + return_value=httpx.Response(200, json=_remediation_body("rem-approve", state="PENDING")) + ) + + result = cli_runner.invoke( + ["beta", "clusters", "remediations", "approve", "rem-approve", "--comment", "go", "--json"] + ) + + assert json.loads(result.output)["state"] == "PENDING" + assert json.loads(cast(Call, route.calls[0]).request.content.decode()) == {"comment": "go"} + assert result.exit_code == 0 + + @pytest.mark.respx(base_url=base_url) + def test_remediations_cancel_resolves_cluster_and_instance( + self, respx_mock: MockRouter, cli_runner: CliRunner + ) -> None: + respx_mock.get("/compute/clusters").mock( + return_value=httpx.Response(200, json={"clusters": [_cluster_body("c1")]}) + ) + respx_mock.get("/compute/clusters/c1/instances/-/remediations").mock( + return_value=httpx.Response(200, json=_remediation_list_body(_remediation_body("rem-cancel"))) + ) + route = respx_mock.post("/compute/clusters/c1/instances/i1/remediations/rem-cancel/cancel").mock( + return_value=httpx.Response(200, json=_remediation_body("rem-cancel", state="CANCELLED")) + ) + + result = cli_runner.invoke(["beta", "clusters", "remediations", "cancel", "rem-cancel", "--json"]) + + assert json.loads(result.output)["state"] == "CANCELLED" + assert route.calls + assert result.exit_code == 0 + + @pytest.mark.respx(base_url=base_url) + def test_remediations_reject_resolves_cluster_and_instance( + self, respx_mock: MockRouter, cli_runner: CliRunner + ) -> None: + respx_mock.get("/compute/clusters").mock( + return_value=httpx.Response(200, json={"clusters": [_cluster_body("c1")]}) + ) + respx_mock.get("/compute/clusters/c1/instances/-/remediations").mock( + return_value=httpx.Response(200, json=_remediation_list_body(_remediation_body("rem-reject"))) + ) + route = respx_mock.post("/compute/clusters/c1/instances/i1/remediations/rem-reject/reject").mock( + return_value=httpx.Response(200, json=_remediation_body("rem-reject", state="CANCELLED")) + ) + + result = cli_runner.invoke( + ["beta", "clusters", "remediations", "reject", "rem-reject", "--comment", "skip", "--json"] + ) + + assert json.loads(result.output)["state"] == "CANCELLED" + assert json.loads(cast(Call, route.calls[0]).request.content.decode()) == {"comment": "skip"} + assert result.exit_code == 0 From 362807ab3551e214f7255aa31c2816a300bff47a Mon Sep 17 00:00:00 2001 From: Blaine Kasten Date: Mon, 18 May 2026 13:07:33 -0500 Subject: [PATCH 13/14] add remediation retrieve command and some fixes --- src/together/lib/cli/__init__.py | 7 ++ .../remediations/_resolve_remediation.py | 33 +++++++ .../api/beta/clusters/remediations/_util.py | 91 ------------------- .../api/beta/clusters/remediations/approve.py | 5 +- .../api/beta/clusters/remediations/cancel.py | 2 +- .../api/beta/clusters/remediations/create.py | 50 ++++++---- .../api/beta/clusters/remediations/list.py | 70 +++++++------- .../api/beta/clusters/remediations/reject.py | 5 +- .../beta/clusters/remediations/retrieve.py | 31 +++++++ src/together/lib/cli/components/model_dump.py | 19 +++- src/together/lib/cli/utils/_help_examples.py | 25 ++++- .../lib/cli/utils/_preparse_tokens.py | 1 + tests/cli/test_beta_clusters.py | 23 ++++- uv.lock | 2 +- 14 files changed, 208 insertions(+), 156 deletions(-) create mode 100644 src/together/lib/cli/api/beta/clusters/remediations/_resolve_remediation.py delete mode 100644 src/together/lib/cli/api/beta/clusters/remediations/_util.py create mode 100644 src/together/lib/cli/api/beta/clusters/remediations/retrieve.py diff --git a/src/together/lib/cli/__init__.py b/src/together/lib/cli/__init__.py index 9a61f0cf8..ffac00a2b 100644 --- a/src/together/lib/cli/__init__.py +++ b/src/together/lib/cli/__init__.py @@ -65,6 +65,7 @@ BETA_CLUSTERS_STORAGE_CREATE_HELP_EXAMPLES, BETA_CLUSTERS_STORAGE_UPDATE_HELP_EXAMPLES, BETA_CLUSTERS_GET_CREDENTIALS_HELP_EXAMPLES, + BETA_CLUSTERS_REMEDIATIONS_CREATE_HELP_EXAMPLES, ) from together.lib.cli.utils._help_formatter import help_formatter from together.lib.cli.utils._preparse_tokens import preparse_tokens @@ -500,12 +501,18 @@ async def run_command() -> None: (f"{_CLI}.beta.clusters.remediations.create:create"), alias="-c", help="Create a node remediation", + help_epilogue=BETA_CLUSTERS_REMEDIATIONS_CREATE_HELP_EXAMPLES, ) remediations_app.command( (f"{_CLI}.beta.clusters.remediations.list:list"), alias="ls", help="List node remediations", ) +remediations_app.command( + (f"{_CLI}.beta.clusters.remediations.retrieve:retrieve"), + alias="get", + help="Get remediation details", +) remediations_app.command( (f"{_CLI}.beta.clusters.remediations.approve:approve"), help="Approve a pending remediation", diff --git a/src/together/lib/cli/api/beta/clusters/remediations/_resolve_remediation.py b/src/together/lib/cli/api/beta/clusters/remediations/_resolve_remediation.py new file mode 100644 index 000000000..55a49e7ba --- /dev/null +++ b/src/together/lib/cli/api/beta/clusters/remediations/_resolve_remediation.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import sys + +from together import omit +from together._types import Omit +from together.lib.cli.utils.config import CLIConfigParameter +from together.lib.cli.utils._console import console +from together.types.beta.clusters.remediation import Remediation + + +async def resolve_remediation(config: CLIConfigParameter, remediation_id: str) -> Remediation: + clusters = await config.client.beta.clusters.list() + + for cluster in clusters.clusters: + page_token: str | Omit = omit + while True: + response = await config.client.beta.clusters.remediations.list( + "-", + cluster_id=cluster.cluster_id, + page_size=100, + page_token=page_token, + ) + for remediation in response.remediations: + if remediation.id == remediation_id: + return remediation + + if not response.has_next or not response.next_page_token: + break + page_token = response.next_page_token + + console.print(f"[red]Error:[/red] Remediation not found: {remediation_id}") + sys.exit(1) diff --git a/src/together/lib/cli/api/beta/clusters/remediations/_util.py b/src/together/lib/cli/api/beta/clusters/remediations/_util.py deleted file mode 100644 index 946cdccda..000000000 --- a/src/together/lib/cli/api/beta/clusters/remediations/_util.py +++ /dev/null @@ -1,91 +0,0 @@ -from __future__ import annotations - -import sys -from typing import List, Literal, TypeVar, Optional, cast, get_args -from datetime import datetime - -from together import omit -from together._types import Omit -from together.lib.cli.utils.config import CLIConfigParameter -from together.lib.cli.utils._console import console -from together.lib.cli.components.list import ListTable -from together.types.beta.clusters.remediation import Remediation - -EMPTY_MESSAGE = "No remediations found for this cluster." - -T = TypeVar("T") -RemediationState = Literal[ - "PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED" -] -_REMEDIATION_STATES = set(get_args(RemediationState)) - - -def omit_if_none(value: Optional[T]) -> T | Omit: - return omit if value is None else value - - -def parse_states(state: Optional[str]) -> List[RemediationState] | Omit: - if state is None: - return omit - states = [part.strip() for part in state.split(",") if part.strip()] - if not states: - return omit - - invalid_states = sorted(set(states) - _REMEDIATION_STATES) - if invalid_states: - console.print(f"[red]Error:[/red] Invalid remediation state: {', '.join(invalid_states)}") - sys.exit(1) - - return cast(List[RemediationState], states) - - -def format_time(value: Optional[datetime]) -> str: - if value is None: - return "-" - return value.isoformat() - - -def print_remediations(remediations: List[Remediation]) -> None: - table = ListTable(title="Cluster Remediations", empty_message=EMPTY_MESSAGE) - table.add_primary_column("ID") - table.add_column("Instance") - table.add_column("State") - table.add_column("Mode") - table.add_column("Trigger") - table.add_column("Created") - - for remediation in remediations: - table.add_row( - remediation.id, - remediation.instance_id, - remediation.state, - remediation.mode.replace("REMEDIATION_MODE_", ""), - remediation.trigger.replace("REMEDIATION_TRIGGER_", ""), - format_time(remediation.create_time), - ) - - console.print(table) - - -async def resolve_remediation(config: CLIConfigParameter, remediation_id: str) -> Remediation: - clusters = await config.client.beta.clusters.list() - - for cluster in clusters.clusters: - page_token: str | Omit = omit - while True: - response = await config.client.beta.clusters.remediations.list( - "-", - cluster_id=cluster.cluster_id, - page_size=100, - page_token=page_token, - ) - for remediation in response.remediations: - if remediation.id == remediation_id: - return remediation - - if not response.has_next or not response.next_page_token: - break - page_token = response.next_page_token - - console.print(f"[red]Error:[/red] Remediation not found: {remediation_id}") - sys.exit(1) diff --git a/src/together/lib/cli/api/beta/clusters/remediations/approve.py b/src/together/lib/cli/api/beta/clusters/remediations/approve.py index 46428c161..eb81a5346 100644 --- a/src/together/lib/cli/api/beta/clusters/remediations/approve.py +++ b/src/together/lib/cli/api/beta/clusters/remediations/approve.py @@ -4,11 +4,12 @@ from cyclopts import Parameter +from together import omit from together._utils._json import openapi_dumps from together.lib.cli.utils.config import CLIConfigParameter from together.lib.cli.utils._console import console from together.lib.cli.components.loader import show_loading_status -from together.lib.cli.api.beta.clusters.remediations._util import omit_if_none, resolve_remediation +from together.lib.cli.api.beta.clusters.remediations._resolve_remediation import resolve_remediation async def approve( @@ -25,7 +26,7 @@ async def approve( remediation_id, cluster_id=remediation.cluster_id, instance_id=remediation.instance_id, - comment=omit_if_none(comment), + comment=comment or omit, ), ) diff --git a/src/together/lib/cli/api/beta/clusters/remediations/cancel.py b/src/together/lib/cli/api/beta/clusters/remediations/cancel.py index fcd34f542..99adc6bff 100644 --- a/src/together/lib/cli/api/beta/clusters/remediations/cancel.py +++ b/src/together/lib/cli/api/beta/clusters/remediations/cancel.py @@ -4,7 +4,7 @@ from together.lib.cli.utils.config import CLIConfigParameter from together.lib.cli.utils._console import console from together.lib.cli.components.loader import show_loading_status -from together.lib.cli.api.beta.clusters.remediations._util import resolve_remediation +from together.lib.cli.api.beta.clusters.remediations._resolve_remediation import resolve_remediation async def cancel( diff --git a/src/together/lib/cli/api/beta/clusters/remediations/create.py b/src/together/lib/cli/api/beta/clusters/remediations/create.py index 3ef21e7c4..8a6f2db49 100644 --- a/src/together/lib/cli/api/beta/clusters/remediations/create.py +++ b/src/together/lib/cli/api/beta/clusters/remediations/create.py @@ -1,29 +1,29 @@ from __future__ import annotations -from typing import Literal, Optional, Annotated +from typing import Literal, Optional, Annotated, cast from cyclopts import Parameter +from together import omit from together._utils._json import openapi_dumps from together.lib.cli.utils.config import CLIConfigParameter from together.lib.cli.utils._console import console from together.lib.cli.components.loader import show_loading_status -from together.lib.cli.api.beta.clusters.remediations._util import omit_if_none RemediationModeParameter = Annotated[ Literal[ - "REMEDIATION_MODE_VM_ONLY", - "REMEDIATION_MODE_HOST_AWARE", - "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT", - "REMEDIATION_MODE_REBOOT_VM", + "VM_ONLY", + "HOST_AWARE", + "EVICT_WITHOUT_REPLACEMENT", + "REBOOT_VM", ], - Parameter(help="How the remediation should be performed"), + Parameter(help="The type of remediation to perform"), ] async def create( - cluster_id: str, - instance_id: str, + cluster_id: Annotated[str, Parameter(help="The ID of the cluster")], + instance_id: Annotated[str, Parameter(help="The ID of the node within the cluster to remediate")], *, mode: RemediationModeParameter, remediation_id: Annotated[Optional[str], Parameter(help="Client-specified ID for idempotency")] = None, @@ -31,18 +31,32 @@ async def create( config: CLIConfigParameter, ) -> None: """Create a node remediation for an instance.""" - request = config.client.beta.clusters.remediations.create( - instance_id, - cluster_id=cluster_id, - mode=mode, - remediation_id=omit_if_none(remediation_id), - reason=omit_if_none(reason), + safe_mode = cast( + Literal[ + "REMEDIATION_MODE_VM_ONLY", + "REMEDIATION_MODE_HOST_AWARE", + "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT", + "REMEDIATION_MODE_REBOOT_VM", + ], + f"REMEDIATION_MODE_{mode}", + ) + + response = await show_loading_status( + "Creating remediation...", + config.client.beta.clusters.remediations.create( + instance_id, + cluster_id=cluster_id, + mode=safe_mode, + remediation_id=remediation_id or omit, + reason=reason or omit, + ), ) - response = await show_loading_status("Creating remediation...", request) if config.json: console.print_json(openapi_dumps(response).decode("utf-8")) return - console.print("[blue]Remediation created successfully[/blue]") - console.print(f"[primary]Remediation ID:[/primary] {response.id}") + console.print(f"[green]√ Remediation created[/green] [dim]({response.id})[/dim]") + console.print(f" Remediations may take some time to complete.\n") + console.print(f" To retrieve the status:") + console.print(f" [dim]-[/dim] [primary]tg beta clusters remediations {response.id}[/primary]") diff --git a/src/together/lib/cli/api/beta/clusters/remediations/list.py b/src/together/lib/cli/api/beta/clusters/remediations/list.py index 7c0124daf..22c70231d 100644 --- a/src/together/lib/cli/api/beta/clusters/remediations/list.py +++ b/src/together/lib/cli/api/beta/clusters/remediations/list.py @@ -1,40 +1,22 @@ from __future__ import annotations -from typing import Literal, Optional, Annotated +from typing import Optional, Annotated from cyclopts import Parameter +from together import omit from together._utils._json import openapi_dumps +from together.lib.utils.tools import format_datetime from together.lib.cli.utils.config import CLIConfigParameter from together.lib.cli.utils._console import console +from together.lib.cli.components.list import ListTable from together.lib.cli.components.loader import show_loading_status -from together.lib.cli.api.beta.clusters.remediations._util import ( - omit_if_none, - parse_states, - print_remediations, -) - -OptionalRemediationModeParameter = Annotated[ - Optional[ - Literal[ - "REMEDIATION_MODE_VM_ONLY", - "REMEDIATION_MODE_HOST_AWARE", - "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT", - "REMEDIATION_MODE_REBOOT_VM", - ] - ], - Parameter(help="Filter by remediation mode"), -] async def list( cluster_id: str, instance_id: Annotated[Optional[str], Parameter(help="Instance ID to list remediations for")] = None, - mode: OptionalRemediationModeParameter = None, - state: Annotated[Optional[str], Parameter(help="Comma-separated remediation states to include")] = None, - order_by: Annotated[Optional[str], Parameter(help="Order by expression")] = None, - page_size: Annotated[Optional[int], Parameter(help="Maximum results to return")] = None, - page_token: Annotated[Optional[str], Parameter(help="Pagination token from a previous request")] = None, + after: Annotated[Optional[str], Parameter(help="Pagination token from a previous request")] = None, *, config: CLIConfigParameter, ) -> None: @@ -44,11 +26,7 @@ async def list( config.client.beta.clusters.remediations.list( instance_id or "-", cluster_id=cluster_id, - mode=omit_if_none(mode), - state=parse_states(state), - order_by=omit_if_none(order_by), - page_size=omit_if_none(page_size), - page_token=omit_if_none(page_token), + page_token=after or omit, ), ) @@ -56,10 +34,40 @@ async def list( console.print_json(openapi_dumps(response).decode("utf-8")) return - print_remediations(response.remediations) + table = ListTable(title="Cluster Remediations", empty_message="No remediations found for this cluster.") + table.add_column("Created") + table.add_primary_column("Instance", ratio=3) + table.add_column("Mode") + table.add_column("State") + table.add_column("Remediation ID", ratio=3) + + for remediation in response.remediations: + table.add_row( + format_datetime(remediation.create_time) if remediation.create_time else "-", + remediation.instance_id, + remediation.mode.replace("REMEDIATION_MODE_", ""), + _colorize(remediation.state), + remediation.id, + ) + + console.print(table) if response.has_next and response.next_page_token: - command = f"tg beta clusters remediations list {cluster_id}" + command = f"tg beta clusters remediations ls {cluster_id}" if instance_id: command += f" {instance_id}" console.print("\n[blue dim]To display the next page, run:[/blue dim]") - console.print(f" [dim]-[/dim] [white]{command} --page-token {response.next_page_token}[/white]") + console.print(f" [dim]-[/dim] [white]{command} --after {response.next_page_token}[/white]") + + +def _colorize(state: str) -> str: + state_colors = { + "PENDING_APPROVAL": "yellow", + "PENDING": "yellow", + "RUNNING": "yellow", + "SUCCEEDED": "green", + "FAILED": "red", + "CANCELLED": "dim", + "AUTO_RESOLVED": "green", + } + color = state_colors[state] if state in state_colors else "white" + return f"[{color}]{state}[/{color}]" diff --git a/src/together/lib/cli/api/beta/clusters/remediations/reject.py b/src/together/lib/cli/api/beta/clusters/remediations/reject.py index a5ecb7803..6fab890c4 100644 --- a/src/together/lib/cli/api/beta/clusters/remediations/reject.py +++ b/src/together/lib/cli/api/beta/clusters/remediations/reject.py @@ -4,11 +4,12 @@ from cyclopts import Parameter +from together import omit from together._utils._json import openapi_dumps from together.lib.cli.utils.config import CLIConfigParameter from together.lib.cli.utils._console import console from together.lib.cli.components.loader import show_loading_status -from together.lib.cli.api.beta.clusters.remediations._util import omit_if_none, resolve_remediation +from together.lib.cli.api.beta.clusters.remediations._resolve_remediation import resolve_remediation async def reject( @@ -25,7 +26,7 @@ async def reject( remediation_id, cluster_id=remediation.cluster_id, instance_id=remediation.instance_id, - comment=omit_if_none(comment), + comment=comment or omit, ), ) diff --git a/src/together/lib/cli/api/beta/clusters/remediations/retrieve.py b/src/together/lib/cli/api/beta/clusters/remediations/retrieve.py new file mode 100644 index 000000000..ffb458b52 --- /dev/null +++ b/src/together/lib/cli/api/beta/clusters/remediations/retrieve.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from together._utils._json import openapi_dumps +from together.lib.cli.utils.config import CLIConfigParameter +from together.lib.cli.utils._console import console +from together.lib.cli.components.loader import show_loading_status +from together.lib.cli.components.model_dump import print_model_dump +from together.lib.cli.api.beta.clusters.remediations._resolve_remediation import resolve_remediation + + +async def retrieve( + remediation_id: str, + *, + config: CLIConfigParameter, +) -> None: + """Retrieve remediation details.""" + remediation = await show_loading_status("Finding remediation...", resolve_remediation(config, remediation_id)) + response = await show_loading_status( + "Retrieving remediation...", + config.client.beta.clusters.remediations.retrieve( + remediation_id, + cluster_id=remediation.cluster_id, + instance_id=remediation.instance_id, + ), + ) + + if config.json: + console.print_json(openapi_dumps(response).decode("utf-8")) + return + + print_model_dump(response, show_nulls=False) diff --git a/src/together/lib/cli/components/model_dump.py b/src/together/lib/cli/components/model_dump.py index fe2e2d196..c16db6b33 100644 --- a/src/together/lib/cli/components/model_dump.py +++ b/src/together/lib/cli/components/model_dump.py @@ -37,6 +37,8 @@ def _pretty_print_results( table.add_row("-", _pretty_print_results(item)) elif isinstance(results, BaseModel): table.add_row("", _pretty_print_results(results.model_dump(), show_nulls=show_nulls)) + elif isinstance(results, datetime): + table.add_row("", _colorize_value(format_datetime(results))) else: table.add_row("", _colorize_value(results)) return table @@ -70,18 +72,25 @@ def _dump_sorted_model(model: BaseModel) -> dict[str, Any]: - Lists last """ + model_dump = model.model_dump() + + # Filter out keys that are not in the model + model_dump = {k: v for k, v in model_dump.items() if k in model.model_fields_set} + def _sort_items(key: str, value: Any) -> int: - # Returns a sort key: 0 for ID fields, 1 for primitives, 2 for dicts/objects, 3 for lists + # Returns a sort key: 0 for ID fields, 1 for dates, 2 for primitives, 3 for dicts/objects, 4 for lists if key.endswith("_id"): return 0 + elif isinstance(value, datetime): + return 1 elif isinstance(value, dict) or isinstance(value, BaseModel): - return 2 - elif isinstance(value, list): return 3 + elif isinstance(value, list): + return 4 else: - return 1 + return 2 - return dict(sorted(model.model_dump().items(), key=lambda kv: _sort_items(kv[0], kv[1]))) + return dict(sorted(model_dump.items(), key=lambda kv: _sort_items(kv[0], kv[1]))) console.print( _pretty_print_results(_dump_sorted_model(model), show_nulls=show_nulls, expand=expand, padding=padding) diff --git a/src/together/lib/cli/utils/_help_examples.py b/src/together/lib/cli/utils/_help_examples.py index c16e05e69..447b584b6 100644 --- a/src/together/lib/cli/utils/_help_examples.py +++ b/src/together/lib/cli/utils/_help_examples.py @@ -253,7 +253,7 @@ [primary]tg beta clusters delete [/primary] [dim]-[/dim] Manage node remediations: - [primary]tg beta clusters remediations list [/primary] + [primary]tg beta clusters remediations ls [/primary] [primary]tg beta clusters remediations create --mode REMEDIATION_MODE_VM_ONLY[/primary] """ @@ -335,13 +335,16 @@ BETA_CLUSTERS_REMEDIATIONS_HELP_EXAMPLES = """[dim]Examples:[/dim] [dim]-[/dim] List all remediations for a cluster: - [primary]tg beta clusters remediations list [/primary] + [primary]tg beta clusters remediations ls [/primary] [dim]-[/dim] List remediations for one instance: - [primary]tg beta clusters remediations list [/primary] + [primary]tg beta clusters remediations ls [/primary] [dim]-[/dim] Create a remediation: - [primary]tg beta clusters remediations create --mode REMEDIATION_MODE_VM_ONLY --reason "node unhealthy"[/primary] + [primary]tg beta clusters remediations create --mode VM_ONLY --reason "node unhealthy"[/primary] + +[dim]-[/dim] Get remediation details: + [primary]tg beta clusters remediations [/primary] [dim]-[/dim] Review automated remediations: [primary]tg beta clusters remediations approve [/primary] @@ -349,6 +352,20 @@ [primary]tg beta clusters remediations cancel [/primary] """ +BETA_CLUSTERS_REMEDIATIONS_CREATE_HELP_EXAMPLES = """[dim]Examples:[/dim] +[dim]-[/dim] Create a VM-only remediation: + [primary]tg beta clusters remediations create --mode VM_ONLY[/primary] + +[dim]-[/dim] Create a host-aware remediation: + [primary]tg beta clusters remediations create --mode HOST_AWARE[/primary] + +[dim]-[/dim] Create a eviction-without-replacement remediation: + [primary]tg beta clusters remediations create --mode EVICT_WITHOUT_REPLACEMENT[/primary] + +[dim]-[/dim] Create a reboot-vm remediation: + [primary]tg beta clusters remediations create --mode REBOOT_VM[/primary] +""" + ## Beta > Jig commands JIG_HELP_EXAMPLES = """[dim]Examples:[/dim] diff --git a/src/together/lib/cli/utils/_preparse_tokens.py b/src/together/lib/cli/utils/_preparse_tokens.py index cfc7ba9a7..2b302303a 100644 --- a/src/together/lib/cli/utils/_preparse_tokens.py +++ b/src/together/lib/cli/utils/_preparse_tokens.py @@ -15,6 +15,7 @@ "endpoints": re.compile(r"^endpoint-"), "beta clusters": _UUID_RE, "beta clusters storage": _UUID_RE, + "beta clusters remediations": _UUID_RE, "beta jig volumes": _UUID_RE, } diff --git a/tests/cli/test_beta_clusters.py b/tests/cli/test_beta_clusters.py index ac42ea4c9..1e204c49f 100644 --- a/tests/cli/test_beta_clusters.py +++ b/tests/cli/test_beta_clusters.py @@ -277,7 +277,7 @@ def test_remediations_create_json(self, respx_mock: MockRouter, cli_runner: CliR "c1", "i1", "--mode", - "REMEDIATION_MODE_VM_ONLY", + "VM_ONLY", "--reason", "node unhealthy", "--remediation-id", @@ -321,6 +321,27 @@ def test_remediations_list_accepts_instance_id(self, respx_mock: MockRouter, cli assert cast(Call, route.calls[0]).request.url.path == "/compute/clusters/c1/instances/i1/remediations" assert result.exit_code == 0 + @pytest.mark.respx(base_url=base_url) + def test_remediations_retrieve_resolves_cluster_and_instance( + self, respx_mock: MockRouter, cli_runner: CliRunner + ) -> None: + body = _remediation_body("rem-get", state="RUNNING") + respx_mock.get("/compute/clusters").mock( + return_value=httpx.Response(200, json={"clusters": [_cluster_body("c1")]}) + ) + respx_mock.get("/compute/clusters/c1/instances/-/remediations").mock( + return_value=httpx.Response(200, json=_remediation_list_body(_remediation_body("rem-get"))) + ) + route = respx_mock.get("/compute/clusters/c1/instances/i1/remediations/rem-get").mock( + return_value=httpx.Response(200, json=body) + ) + + result = cli_runner.invoke(["beta", "clusters", "remediations", "get", "rem-get", "--json"]) + + assert json.loads(result.output) == body + assert cast(Call, route.calls[0]).request.url.path == "/compute/clusters/c1/instances/i1/remediations/rem-get" + assert result.exit_code == 0 + @pytest.mark.respx(base_url=base_url) def test_remediations_approve_resolves_cluster_and_instance( self, respx_mock: MockRouter, cli_runner: CliRunner diff --git a/uv.lock b/uv.lock index a10f2d912..abe93dbc1 100644 --- a/uv.lock +++ b/uv.lock @@ -1559,7 +1559,7 @@ wheels = [ [[package]] name = "together" -version = "2.12.0" +version = "2.14.0" source = { editable = "." } dependencies = [ { name = "anyio" }, From fe8f4d1775c8a73843987793d5c575cfe0bf5328 Mon Sep 17 00:00:00 2001 From: Blaine Kasten Date: Tue, 19 May 2026 14:12:03 -0500 Subject: [PATCH 14/14] address feedback --- .../beta/clusters/remediations/retrieve.py | 2 +- src/together/lib/cli/components/model_dump.py | 24 +++++++++++++++---- src/together/lib/cli/utils/_help_examples.py | 2 +- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/together/lib/cli/api/beta/clusters/remediations/retrieve.py b/src/together/lib/cli/api/beta/clusters/remediations/retrieve.py index ffb458b52..8143dc450 100644 --- a/src/together/lib/cli/api/beta/clusters/remediations/retrieve.py +++ b/src/together/lib/cli/api/beta/clusters/remediations/retrieve.py @@ -28,4 +28,4 @@ async def retrieve( console.print_json(openapi_dumps(response).decode("utf-8")) return - print_model_dump(response, show_nulls=False) + print_model_dump(response, show_nulls=False, only_set_fields=True) diff --git a/src/together/lib/cli/components/model_dump.py b/src/together/lib/cli/components/model_dump.py index c16db6b33..4bef66807 100644 --- a/src/together/lib/cli/components/model_dump.py +++ b/src/together/lib/cli/components/model_dump.py @@ -12,9 +12,25 @@ def print_model_dump( - model: BaseModel, show_nulls: bool = True, expand: bool = True, padding: PaddingDimensions = (0, 1, 0, 0) + model: BaseModel, + show_nulls: bool = True, + expand: bool = True, + padding: PaddingDimensions = (0, 1, 0, 0), + *, + only_set_fields: bool = False, ) -> None: - """Print an entire model with __decent__ formatting.""" + """Print an entire model with __decent__ formatting. + + Args: + model: The response model to render. + show_nulls: When True, include fields whose value is None or empty, displayed as + "n/a". When False, omit those fields entirely. + expand: Passed to the Rich table; when True, the table stretches to the terminal width. + padding: Rich table cell padding as (top, right, bottom, left). + only_set_fields: When True, only include fields present in the API response + (model.model_fields_set). Use this to avoid showing optional fields that were never + sent and still carry a default value. When False, all model fields are shown. + """ def _pretty_print_results( results: Any, show_nulls: bool = True, expand: bool = False, padding: PaddingDimensions = (0, 1, 0, 0) @@ -74,8 +90,8 @@ def _dump_sorted_model(model: BaseModel) -> dict[str, Any]: model_dump = model.model_dump() - # Filter out keys that are not in the model - model_dump = {k: v for k, v in model_dump.items() if k in model.model_fields_set} + if only_set_fields: + model_dump = {k: v for k, v in model_dump.items() if k in model.model_fields_set} def _sort_items(key: str, value: Any) -> int: # Returns a sort key: 0 for ID fields, 1 for dates, 2 for primitives, 3 for dicts/objects, 4 for lists diff --git a/src/together/lib/cli/utils/_help_examples.py b/src/together/lib/cli/utils/_help_examples.py index 447b584b6..75ff7cb2c 100644 --- a/src/together/lib/cli/utils/_help_examples.py +++ b/src/together/lib/cli/utils/_help_examples.py @@ -254,7 +254,7 @@ [dim]-[/dim] Manage node remediations: [primary]tg beta clusters remediations ls [/primary] - [primary]tg beta clusters remediations create --mode REMEDIATION_MODE_VM_ONLY[/primary] + [primary]tg beta clusters remediations create --mode VM_ONLY[/primary] """ BETA_CLUSTERS_CREATE_HELP_EXAMPLES = """[dim]Examples:[/dim]