together 2.0.0a13__py3-none-any.whl → 2.0.0a15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. together/_client.py +38 -0
  2. together/_version.py +1 -1
  3. together/constants.py +34 -0
  4. together/error.py +16 -0
  5. together/lib/cli/api/beta/beta.py +12 -0
  6. together/lib/cli/api/beta/clusters.py +357 -0
  7. together/lib/cli/api/beta/clusters_storage.py +152 -0
  8. together/lib/cli/api/utils.py +41 -5
  9. together/lib/cli/cli.py +2 -0
  10. together/lib/types/fine_tuning.py +3 -0
  11. together/resources/__init__.py +14 -0
  12. together/resources/beta/__init__.py +33 -0
  13. together/resources/beta/beta.py +102 -0
  14. together/resources/beta/clusters/__init__.py +33 -0
  15. together/resources/beta/clusters/clusters.py +628 -0
  16. together/resources/beta/clusters/storage.py +490 -0
  17. together/types/__init__.py +12 -1
  18. together/types/beta/__init__.py +12 -0
  19. together/types/beta/cluster.py +93 -0
  20. together/types/beta/cluster_create_params.py +51 -0
  21. together/types/beta/cluster_create_response.py +9 -0
  22. together/types/beta/cluster_delete_response.py +9 -0
  23. together/types/beta/cluster_list_regions_response.py +21 -0
  24. together/types/beta/cluster_list_response.py +12 -0
  25. together/types/beta/cluster_update_params.py +13 -0
  26. together/types/beta/cluster_update_response.py +9 -0
  27. together/types/beta/clusters/__init__.py +10 -0
  28. together/types/beta/clusters/cluster_storage.py +13 -0
  29. together/types/beta/clusters/storage_create_params.py +17 -0
  30. together/types/beta/clusters/storage_create_response.py +9 -0
  31. together/types/beta/clusters/storage_delete_response.py +9 -0
  32. together/types/beta/clusters/storage_list_response.py +12 -0
  33. together/types/beta/clusters/storage_update_params.py +13 -0
  34. together/types/chat_completions.py +7 -0
  35. together/types/endpoints.py +4 -0
  36. together/types/files.py +8 -0
  37. together/types/fine_tuning_cancel_response.py +3 -0
  38. together/types/fine_tuning_list_response.py +3 -0
  39. together/types/finetune.py +27 -0
  40. together/types/finetune_response.py +2 -0
  41. together/types/models.py +2 -0
  42. {together-2.0.0a13.dist-info → together-2.0.0a15.dist-info}/METADATA +55 -8
  43. {together-2.0.0a13.dist-info → together-2.0.0a15.dist-info}/RECORD +46 -15
  44. {together-2.0.0a13.dist-info → together-2.0.0a15.dist-info}/WHEEL +0 -0
  45. {together-2.0.0a13.dist-info → together-2.0.0a15.dist-info}/entry_points.txt +0 -0
  46. {together-2.0.0a13.dist-info → together-2.0.0a15.dist-info}/licenses/LICENSE +0 -0
together/_client.py CHANGED
@@ -32,6 +32,7 @@ from ._base_client import (
32
32
 
33
33
  if TYPE_CHECKING:
34
34
  from .resources import (
35
+ beta,
35
36
  chat,
36
37
  jobs,
37
38
  audio,
@@ -58,6 +59,7 @@ if TYPE_CHECKING:
58
59
  from .resources.videos import VideosResource, AsyncVideosResource
59
60
  from .resources.batches import BatchesResource, AsyncBatchesResource
60
61
  from .resources.hardware import HardwareResource, AsyncHardwareResource
62
+ from .resources.beta.beta import BetaResource, AsyncBetaResource
61
63
  from .resources.chat.chat import ChatResource, AsyncChatResource
62
64
  from .resources.endpoints import EndpointsResource, AsyncEndpointsResource
63
65
  from .resources.embeddings import EmbeddingsResource, AsyncEmbeddingsResource
@@ -136,6 +138,12 @@ class Together(SyncAPIClient):
136
138
 
137
139
  self._default_stream_cls = Stream
138
140
 
141
+ @cached_property
142
+ def beta(self) -> BetaResource:
143
+ from .resources.beta import BetaResource
144
+
145
+ return BetaResource(self)
146
+
139
147
  @cached_property
140
148
  def chat(self) -> ChatResource:
141
149
  from .resources.chat import ChatResource
@@ -405,6 +413,12 @@ class AsyncTogether(AsyncAPIClient):
405
413
 
406
414
  self._default_stream_cls = AsyncStream
407
415
 
416
+ @cached_property
417
+ def beta(self) -> AsyncBetaResource:
418
+ from .resources.beta import AsyncBetaResource
419
+
420
+ return AsyncBetaResource(self)
421
+
408
422
  @cached_property
409
423
  def chat(self) -> AsyncChatResource:
410
424
  from .resources.chat import AsyncChatResource
@@ -622,6 +636,12 @@ class TogetherWithRawResponse:
622
636
  def __init__(self, client: Together) -> None:
623
637
  self._client = client
624
638
 
639
+ @cached_property
640
+ def beta(self) -> beta.BetaResourceWithRawResponse:
641
+ from .resources.beta import BetaResourceWithRawResponse
642
+
643
+ return BetaResourceWithRawResponse(self._client.beta)
644
+
625
645
  @cached_property
626
646
  def chat(self) -> chat.ChatResourceWithRawResponse:
627
647
  from .resources.chat import ChatResourceWithRawResponse
@@ -725,6 +745,12 @@ class AsyncTogetherWithRawResponse:
725
745
  def __init__(self, client: AsyncTogether) -> None:
726
746
  self._client = client
727
747
 
748
+ @cached_property
749
+ def beta(self) -> beta.AsyncBetaResourceWithRawResponse:
750
+ from .resources.beta import AsyncBetaResourceWithRawResponse
751
+
752
+ return AsyncBetaResourceWithRawResponse(self._client.beta)
753
+
728
754
  @cached_property
729
755
  def chat(self) -> chat.AsyncChatResourceWithRawResponse:
730
756
  from .resources.chat import AsyncChatResourceWithRawResponse
@@ -828,6 +854,12 @@ class TogetherWithStreamedResponse:
828
854
  def __init__(self, client: Together) -> None:
829
855
  self._client = client
830
856
 
857
+ @cached_property
858
+ def beta(self) -> beta.BetaResourceWithStreamingResponse:
859
+ from .resources.beta import BetaResourceWithStreamingResponse
860
+
861
+ return BetaResourceWithStreamingResponse(self._client.beta)
862
+
831
863
  @cached_property
832
864
  def chat(self) -> chat.ChatResourceWithStreamingResponse:
833
865
  from .resources.chat import ChatResourceWithStreamingResponse
@@ -931,6 +963,12 @@ class AsyncTogetherWithStreamedResponse:
931
963
  def __init__(self, client: AsyncTogether) -> None:
932
964
  self._client = client
933
965
 
966
+ @cached_property
967
+ def beta(self) -> beta.AsyncBetaResourceWithStreamingResponse:
968
+ from .resources.beta import AsyncBetaResourceWithStreamingResponse
969
+
970
+ return AsyncBetaResourceWithStreamingResponse(self._client.beta)
971
+
934
972
  @cached_property
935
973
  def chat(self) -> chat.AsyncChatResourceWithStreamingResponse:
936
974
  from .resources.chat import AsyncChatResourceWithStreamingResponse
together/_version.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
2
 
3
3
  __title__ = "together"
4
- __version__ = "2.0.0-alpha.13" # x-release-please-version
4
+ __version__ = "2.0.0-alpha.15" # x-release-please-version
together/constants.py ADDED
@@ -0,0 +1,34 @@
1
+ # Manually added to minimize breaking changes from V1
2
+ from ._constants import (
3
+ MAX_RETRY_DELAY as MAX_RETRY_DELAY,
4
+ DEFAULT_MAX_RETRIES,
5
+ INITIAL_RETRY_DELAY as INITIAL_RETRY_DELAY,
6
+ )
7
+ from .lib.constants import (
8
+ MIN_SAMPLES as MIN_SAMPLES,
9
+ DISABLE_TQDM as DISABLE_TQDM,
10
+ MAX_IMAGE_BYTES as MAX_IMAGE_BYTES,
11
+ NUM_BYTES_IN_GB as NUM_BYTES_IN_GB,
12
+ MAX_FILE_SIZE_GB as MAX_FILE_SIZE_GB,
13
+ MIN_PART_SIZE_MB as MIN_PART_SIZE_MB,
14
+ DOWNLOAD_BLOCK_SIZE as DOWNLOAD_BLOCK_SIZE,
15
+ MAX_MULTIPART_PARTS as MAX_MULTIPART_PARTS,
16
+ TARGET_PART_SIZE_MB as TARGET_PART_SIZE_MB,
17
+ MAX_CONCURRENT_PARTS as MAX_CONCURRENT_PARTS,
18
+ MAX_IMAGES_PER_EXAMPLE as MAX_IMAGES_PER_EXAMPLE,
19
+ MULTIPART_THRESHOLD_GB as MULTIPART_THRESHOLD_GB,
20
+ MAX_BASE64_IMAGE_LENGTH as MAX_BASE64_IMAGE_LENGTH,
21
+ MULTIPART_UPLOAD_TIMEOUT as MULTIPART_UPLOAD_TIMEOUT,
22
+ PARQUET_EXPECTED_COLUMNS as PARQUET_EXPECTED_COLUMNS,
23
+ REQUIRED_COLUMNS_MESSAGE as REQUIRED_COLUMNS_MESSAGE,
24
+ JSONL_REQUIRED_COLUMNS_MAP as JSONL_REQUIRED_COLUMNS_MAP,
25
+ POSSIBLE_ROLES_CONVERSATION as POSSIBLE_ROLES_CONVERSATION,
26
+ DatasetFormat as DatasetFormat,
27
+ )
28
+
29
+ TIMEOUT_SECS = 600
30
+ MAX_SESSION_LIFETIME_SECS = 180
31
+ MAX_CONNECTION_RETRIES = 2
32
+ MAX_RETRIES = DEFAULT_MAX_RETRIES
33
+
34
+ BASE_URL = "https://api.together.xyz/v1"
together/error.py ADDED
@@ -0,0 +1,16 @@
1
+
2
+ # Manually added to minimize breaking changes from V1
3
+ from ._exceptions import (
4
+ APIError as APIError,
5
+ RateLimitError as RateLimitError,
6
+ APITimeoutError,
7
+ BadRequestError,
8
+ APIConnectionError as APIConnectionError,
9
+ AuthenticationError as AuthenticationError,
10
+ APIResponseValidationError,
11
+ )
12
+
13
+ Timeout = APITimeoutError
14
+ InvalidRequestError = BadRequestError
15
+ TogetherException = APIError
16
+ ResponseError = APIResponseValidationError
@@ -0,0 +1,12 @@
1
+ import click
2
+
3
+ from together.lib.cli.api.beta.clusters import clusters
4
+
5
+
6
+ @click.group()
7
+ def beta() -> None:
8
+ """Beta API commands"""
9
+ pass
10
+
11
+
12
+ beta.add_command(clusters)
@@ -0,0 +1,357 @@
1
+ from __future__ import annotations
2
+
3
+ import json as json_lib
4
+ import getpass
5
+ from typing import Any, Dict, List, Literal
6
+
7
+ import click
8
+ from rich import print
9
+ from tabulate import tabulate
10
+
11
+ from together import Together, omit
12
+ from together._response import APIResponse as APIResponse
13
+ from together.types.beta import Cluster, ClusterCreateParams
14
+ from together.lib.cli.api.utils import handle_api_errors
15
+ from together.types.beta.cluster_create_params import SharedVolume
16
+ from together.lib.cli.api.beta.clusters_storage import storage
17
+
18
+
19
+ def print_clusters(clusters: List[Cluster]) -> None:
20
+ data: List[Dict[str, Any]] = []
21
+ for cluster in clusters:
22
+ data.append(
23
+ {
24
+ "ID": cluster.cluster_id,
25
+ "Name": cluster.cluster_name,
26
+ "Status": cluster.status,
27
+ "Region": cluster.region,
28
+ }
29
+ )
30
+ click.echo(tabulate(data, headers="keys", tablefmt="grid"))
31
+
32
+
33
+ @click.group()
34
+ @click.pass_context
35
+ def clusters(ctx: click.Context) -> None:
36
+ """Clusters API commands"""
37
+ pass
38
+
39
+
40
+ clusters.add_command(storage)
41
+
42
+
43
+ @clusters.command()
44
+ @click.option(
45
+ "--json",
46
+ is_flag=True,
47
+ help="Output in JSON format",
48
+ )
49
+ @click.pass_context
50
+ def list(ctx: click.Context, json: bool) -> None:
51
+ """List clusters"""
52
+ client: Together = ctx.obj
53
+
54
+ response = client.beta.clusters.list()
55
+
56
+ if json:
57
+ click.echo(json_lib.dumps(response.model_dump(exclude_none=True), indent=4))
58
+ else:
59
+ print_clusters(response.clusters)
60
+
61
+
62
+ @clusters.command()
63
+ @click.option(
64
+ "--name",
65
+ type=str,
66
+ help="Name of the cluster",
67
+ )
68
+ @click.option(
69
+ "--num-gpus",
70
+ type=int,
71
+ help="Number of GPUs to allocate in the cluster",
72
+ )
73
+ @click.option(
74
+ "--region",
75
+ type=str,
76
+ help="Region to create the cluster in",
77
+ )
78
+ @click.option(
79
+ "--billing-type",
80
+ type=str,
81
+ help="Billing type to use for the cluster",
82
+ )
83
+ @click.option(
84
+ "--driver-version",
85
+ type=str,
86
+ help="Driver version to use for the cluster",
87
+ )
88
+ @click.option(
89
+ "--duration-days",
90
+ type=int,
91
+ help="Duration in days to keep the cluster running for reserved clusters",
92
+ )
93
+ @click.option(
94
+ "--gpu-type",
95
+ type=str,
96
+ help="GPU type to use for the cluster. Find available gpu types for each region with the `list-regions` command.",
97
+ )
98
+ @click.option("--cluster-type", type=click.Choice(["KUBERNETES", "SLURM"]), help="Cluster type")
99
+ @click.option(
100
+ "--volume",
101
+ type=str,
102
+ help="Storage volume ID to use for the cluster",
103
+ )
104
+ @click.option(
105
+ "--json",
106
+ is_flag=True,
107
+ help="Output in JSON format",
108
+ )
109
+ @click.pass_context
110
+ @handle_api_errors("Clusters")
111
+ def create(
112
+ ctx: click.Context,
113
+ name: str | None = None,
114
+ num_gpus: int | None = None,
115
+ region: str | None = None,
116
+ billing_type: Literal["RESERVED", "ON_DEMAND"] | None = None,
117
+ driver_version: str | None = None,
118
+ duration_days: int | None = None,
119
+ gpu_type: str | None = None,
120
+ cluster_type: Literal["KUBERNETES", "SLURM"] | None = None,
121
+ volume: str | None = None,
122
+ json: bool = False,
123
+ ) -> None:
124
+ """Create a cluster"""
125
+ client: Together = ctx.obj
126
+
127
+ params = ClusterCreateParams(
128
+ cluster_name=name, # type: ignore
129
+ num_gpus=num_gpus, # type: ignore
130
+ region=region, # type: ignore
131
+ billing_type=billing_type, # type: ignore
132
+ driver_version=driver_version, # type: ignore
133
+ duration_days=duration_days, # type: ignore
134
+ gpu_type=gpu_type, # type: ignore
135
+ cluster_type=cluster_type, # type: ignore
136
+ )
137
+
138
+ # Lazily add this so its not put in the object as None - just looks bad aesthetically
139
+ if volume:
140
+ params["volume_id"] = volume
141
+
142
+ # JSON Mode skips hand holding through the argument setup
143
+ if not json:
144
+ if not name:
145
+ params["cluster_name"] = click.prompt("Clusters: Cluster name:", default=getpass.getuser(), type=str)
146
+
147
+ # TODO
148
+ # GPU should be queried first
149
+ # Validate region has the gpu selected.
150
+
151
+ if not gpu_type:
152
+ # TODO: Pull GPUS from region list and the region selected.
153
+ # TODO: Add instance_types to region list api
154
+ params["gpu_type"] = click.prompt(
155
+ "Clusters: Cluster GPU type:",
156
+ type=click.Choice(["H100_SXM", "H200_SXM", "RTX_6000_PCI", "L40_PCIE", "B200_SXM", "H100_SXM_INF"]),
157
+ )
158
+
159
+ if not region:
160
+ regions = client.beta.clusters.list_regions()
161
+ params["region"] = click.prompt(
162
+ "Clusters: Cluster region:",
163
+ default=regions.regions[0].name,
164
+ type=click.Choice([region.name for region in regions.regions]),
165
+ )
166
+
167
+ if num_gpus is None:
168
+ params["num_gpus"] = click.prompt("Clusters: Cluster GPUs count", type=click.IntRange(min=8, max=64))
169
+
170
+ if not billing_type:
171
+ params["billing_type"] = click.prompt(
172
+ "Clusters: Cluster billing type:", default="ON_DEMAND", type=click.Choice(["RESERVED", "ON_DEMAND"])
173
+ )
174
+
175
+ if not driver_version:
176
+ regions = client.beta.clusters.list_regions()
177
+
178
+ # Get the driver versions for the selected region
179
+ driver_versions: List[str] = []
180
+ for region_obj in regions.regions:
181
+ if region_obj.name == params["region"]:
182
+ driver_versions.extend(region_obj.driver_versions)
183
+
184
+ params["driver_version"] = click.prompt(
185
+ "Clusters: Cluster driver version:", default="CUDA_12_5_555", type=click.Choice(driver_versions)
186
+ )
187
+
188
+ if not duration_days and params["billing_type"] == "RESERVED":
189
+ params["duration_days"] = click.prompt("Clusters: Cluster reserved duration (1-90 days):", default=3)
190
+
191
+ if not cluster_type:
192
+ params["cluster_type"] = click.prompt(
193
+ "Clusters: Cluster type:", default="KUBERNETES", type=click.Choice(["KUBERNETES", "SLURM"])
194
+ )
195
+
196
+ # In our QA environment, we don't accept storage volume creation, so we skip the prompt
197
+ if not volume and "qa" not in client.base_url.host:
198
+ if click.confirm("Clusters: Create a new storage volume?"):
199
+ default_volume_name = f"{params['cluster_name']}-storage"
200
+ params["shared_volume"] = SharedVolume(
201
+ region=f"{params['region']}",
202
+ size_tib=1,
203
+ volume_name=default_volume_name,
204
+ )
205
+ params["shared_volume"]["volume_name"] = click.prompt(
206
+ "Clusters: Storage volume name:", default=default_volume_name, type=str
207
+ )
208
+ params["shared_volume"]["size_tib"] = click.prompt(
209
+ "Clusters: Storage volume size (TiB):", default=1, type=click.IntRange(min=1, max=1024)
210
+ )
211
+ else:
212
+ # TODO: We need bound status and region on the volume list from the API.
213
+ # Only show volumes in the region selected and that are not attached to a cluster.
214
+ volumes = client.beta.clusters.storage.list()
215
+ params["volume_id"] = click.prompt(
216
+ "Clusters: Which storage volume to use?",
217
+ default=volumes.volumes[0].volume_id,
218
+ type=click.Choice([volume.volume_id for volume in volumes.volumes]),
219
+ )
220
+
221
+ click.echo("Clusters: Creating cluster with the following parameters:")
222
+ print(ClusterCreateParams(**params)) # type: ignore
223
+
224
+ response = client.beta.clusters.create(**params)
225
+
226
+ if json:
227
+ click.echo(json_lib.dumps(response.model_dump(exclude_none=True), indent=4))
228
+ else:
229
+ click.echo(f"Clusters: Cluster created successfully")
230
+ click.echo(f"Clusters: {response.cluster_id}")
231
+
232
+
233
+ @clusters.command()
234
+ @click.argument("cluster-id", required=True)
235
+ @click.option(
236
+ "--json",
237
+ is_flag=True,
238
+ help="Output in JSON format",
239
+ )
240
+ @click.pass_context
241
+ @handle_api_errors("Clusters")
242
+ def retrieve(ctx: click.Context, cluster_id: str, json: bool) -> None:
243
+ """Retrieve a cluster by ID"""
244
+ client: Together = ctx.obj
245
+
246
+ if not json:
247
+ click.echo(f"Clusters: Retrieving cluster...")
248
+
249
+ response = client.beta.clusters.retrieve(cluster_id)
250
+
251
+ if json:
252
+ click.echo(json_lib.dumps(response.model_dump(exclude_none=True), indent=4))
253
+ else:
254
+ print(response)
255
+
256
+
257
+ @clusters.command()
258
+ @click.argument("cluster-id", required=True)
259
+ @click.option(
260
+ "--num-gpus",
261
+ type=int,
262
+ help="Number of GPUs to allocate in the cluster",
263
+ )
264
+ @click.option(
265
+ "--cluster-type",
266
+ type=click.Choice(["KUBERNETES", "SLURM"]),
267
+ help="Cluster type",
268
+ )
269
+ @click.option(
270
+ "--json",
271
+ is_flag=True,
272
+ help="Output in JSON format",
273
+ )
274
+ @click.pass_context
275
+ @handle_api_errors("Clusters")
276
+ def update(
277
+ ctx: click.Context,
278
+ cluster_id: str,
279
+ num_gpus: int | None = None,
280
+ cluster_type: Literal["KUBERNETES", "SLURM"] | None = None,
281
+ json: bool = False,
282
+ ) -> None:
283
+ """Update a cluster"""
284
+ client: Together = ctx.obj
285
+
286
+ if not json:
287
+ click.echo("Clusters: Updating cluster...")
288
+
289
+ client.beta.clusters.update(
290
+ cluster_id,
291
+ num_gpus=num_gpus if num_gpus is not None else omit,
292
+ cluster_type=cluster_type if cluster_type is not None else omit,
293
+ )
294
+
295
+ if json:
296
+ cluster = client.beta.clusters.retrieve(cluster_id)
297
+ click.echo(json_lib.dumps(cluster.model_dump(exclude_none=True), indent=4))
298
+ else:
299
+ click.echo("Clusters: Done")
300
+
301
+
302
+ @clusters.command()
303
+ @click.argument("cluster-id", required=True)
304
+ @click.option(
305
+ "--json",
306
+ is_flag=True,
307
+ help="Output in JSON format",
308
+ )
309
+ @click.pass_context
310
+ @handle_api_errors("Clusters")
311
+ def delete(ctx: click.Context, cluster_id: str, json: bool) -> None:
312
+ """Delete a cluster by ID"""
313
+ client: Together = ctx.obj
314
+
315
+ if json:
316
+ response = client.beta.clusters.delete(cluster_id=cluster_id)
317
+ click.echo(json_lib.dumps(response.model_dump(), indent=2))
318
+ return
319
+
320
+ cluster = client.beta.clusters.retrieve(cluster_id=cluster_id)
321
+ print_clusters([cluster])
322
+ if not click.confirm(f"Clusters: Are you sure you want to delete cluster {cluster.cluster_name}?"):
323
+ return
324
+
325
+ click.echo("Clusters: Deleting cluster...")
326
+ response = client.beta.clusters.delete(cluster_id=cluster_id)
327
+
328
+ click.echo(f"Clusters: Deleted cluster {cluster.cluster_name}")
329
+
330
+
331
+ @clusters.command()
332
+ @click.option(
333
+ "--json",
334
+ is_flag=True,
335
+ help="Output in JSON format",
336
+ )
337
+ @click.pass_context
338
+ @handle_api_errors("Clusters")
339
+ def list_regions(ctx: click.Context, json: bool) -> None:
340
+ """List regions"""
341
+ client: Together = ctx.obj
342
+
343
+ response = client.beta.clusters.list_regions()
344
+
345
+ if json:
346
+ click.echo(json_lib.dumps(response.model_dump(exclude_none=True), indent=4))
347
+ else:
348
+ data: List[Dict[str, Any]] = []
349
+ for region in response.regions:
350
+ data.append(
351
+ {
352
+ "Name": region.name,
353
+ "Availability Zones": ", ".join(region.availability_zones) if region.availability_zones else "",
354
+ "Driver Versions": ", ".join(region.driver_versions) if region.driver_versions else "",
355
+ }
356
+ )
357
+ click.echo(tabulate(data, headers="keys", tablefmt="grid"))
@@ -0,0 +1,152 @@
1
+ import json as json_lib
2
+ from typing import Any, Dict, List
3
+
4
+ import click
5
+ from rich import print
6
+ from tabulate import tabulate
7
+
8
+ from together import Together
9
+ from together.lib.cli.api.utils import handle_api_errors
10
+ from together.types.beta.clusters import ClusterStorage
11
+
12
+
13
+ def print_storage(storage: List[ClusterStorage]) -> None:
14
+ data: List[Dict[str, Any]] = []
15
+ for volume in storage:
16
+ data.append(
17
+ {
18
+ "ID": volume.volume_id,
19
+ "Name": volume.volume_name,
20
+ "Size": volume.size_tib,
21
+ }
22
+ )
23
+ click.echo(tabulate(data, headers="keys", tablefmt="grid"))
24
+
25
+
26
+ @click.group()
27
+ @click.pass_context
28
+ def storage(ctx: click.Context) -> None:
29
+ """Clusters Storage API commands"""
30
+ pass
31
+
32
+
33
+ @storage.command()
34
+ @click.option(
35
+ "--region",
36
+ required=True,
37
+ type=str,
38
+ help="Region to create the storage volume in",
39
+ )
40
+ @click.option(
41
+ "--size-tib",
42
+ required=True,
43
+ type=int,
44
+ help="Size of the storage volume in TiB",
45
+ )
46
+ @click.option(
47
+ "--volume-name",
48
+ required=True,
49
+ type=str,
50
+ help="Name of the storage volume",
51
+ )
52
+ @click.option(
53
+ "--json",
54
+ is_flag=True,
55
+ help="Output in JSON format",
56
+ )
57
+ @click.pass_context
58
+ @handle_api_errors("Clusters Storage")
59
+ def create(ctx: click.Context, region: str, size_tib: int, volume_name: str, json: bool) -> None:
60
+ """Create a storage volume"""
61
+ client: Together = ctx.obj
62
+
63
+ response = client.beta.clusters.storage.create(
64
+ region=region,
65
+ size_tib=size_tib,
66
+ volume_name=volume_name,
67
+ )
68
+
69
+ if json:
70
+ click.echo(json_lib.dumps(response.model_dump_json(), indent=2))
71
+ else:
72
+ click.echo(f"Storage volume created successfully")
73
+ click.echo(response.volume_id)
74
+
75
+
76
+ @storage.command()
77
+ @click.argument(
78
+ "volume-id",
79
+ required=True,
80
+ )
81
+ @click.option(
82
+ "--json",
83
+ is_flag=True,
84
+ help="Output in JSON format",
85
+ )
86
+ @click.pass_context
87
+ @handle_api_errors("Clusters Storage")
88
+ def retrieve(ctx: click.Context, volume_id: str, json: bool) -> None:
89
+ """Retrieve a storage volume"""
90
+ client: Together = ctx.obj
91
+
92
+ if not json:
93
+ click.echo(f"Clusters Storage: Retrieving storage volume...")
94
+
95
+ response = client.beta.clusters.storage.retrieve(volume_id)
96
+
97
+ if json:
98
+ click.echo(json_lib.dumps(response.model_dump(), indent=2))
99
+ else:
100
+ print(response)
101
+
102
+
103
+ @storage.command()
104
+ @click.argument(
105
+ "volume-id",
106
+ required=True,
107
+ )
108
+ @click.option(
109
+ "--json",
110
+ is_flag=True,
111
+ help="Output in JSON format",
112
+ )
113
+ @click.pass_context
114
+ @handle_api_errors("Clusters Storage")
115
+ def delete(ctx: click.Context, volume_id: str, json: bool) -> None:
116
+ """Delete a storage volume"""
117
+ client: Together = ctx.obj
118
+
119
+ if json:
120
+ response = client.beta.clusters.storage.delete(volume_id)
121
+ click.echo(json_lib.dumps(response.model_dump(), indent=2))
122
+ return
123
+
124
+ storage = client.beta.clusters.storage.retrieve(volume_id)
125
+ print_storage([storage])
126
+ if not click.confirm(f"Clusters Storage: Are you sure you want to delete storage volume {storage.volume_name}?"):
127
+ return
128
+
129
+ click.echo("Clusters Storage: Deleting storage volume...")
130
+ response = client.beta.clusters.storage.delete(volume_id)
131
+
132
+ click.echo(f"Clusters Storage: Deleted storage volume {storage.volume_name}")
133
+
134
+
135
+ @storage.command()
136
+ @click.option(
137
+ "--json",
138
+ is_flag=True,
139
+ help="Output in JSON format",
140
+ )
141
+ @click.pass_context
142
+ @handle_api_errors("Clusters Storage")
143
+ def list(ctx: click.Context, json: bool) -> None:
144
+ """List storage volumes"""
145
+ client: Together = ctx.obj
146
+
147
+ response = client.beta.clusters.storage.list()
148
+
149
+ if json:
150
+ click.echo(json_lib.dumps(response.model_dump(), indent=2))
151
+ else:
152
+ print_storage(response.volumes)