lightning-sdk 2025.8.18.post0__py3-none-any.whl → 2025.8.21__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 (37) hide show
  1. lightning_sdk/__init__.py +1 -1
  2. lightning_sdk/api/llm_api.py +6 -2
  3. lightning_sdk/api/studio_api.py +113 -36
  4. lightning_sdk/api/utils.py +108 -18
  5. lightning_sdk/cli/legacy/create.py +9 -11
  6. lightning_sdk/cli/legacy/start.py +1 -0
  7. lightning_sdk/cli/legacy/switch.py +1 -0
  8. lightning_sdk/cli/studio/start.py +1 -0
  9. lightning_sdk/cli/studio/switch.py +1 -0
  10. lightning_sdk/lightning_cloud/openapi/__init__.py +2 -0
  11. lightning_sdk/lightning_cloud/openapi/api/billing_service_api.py +85 -0
  12. lightning_sdk/lightning_cloud/openapi/api/k8_s_cluster_service_api.py +113 -0
  13. lightning_sdk/lightning_cloud/openapi/models/__init__.py +2 -0
  14. lightning_sdk/lightning_cloud/openapi/models/assistant_id_conversations_body.py +15 -15
  15. lightning_sdk/lightning_cloud/openapi/models/id_codeconfig_body.py +3 -81
  16. lightning_sdk/lightning_cloud/openapi/models/orgs_id_body.py +27 -1
  17. lightning_sdk/lightning_cloud/openapi/models/project_id_storage_body.py +27 -1
  18. lightning_sdk/lightning_cloud/openapi/models/storage_complete_body.py +27 -1
  19. lightning_sdk/lightning_cloud/openapi/models/uploads_upload_id_body1.py +27 -1
  20. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_metrics.py +79 -1
  21. lightning_sdk/lightning_cloud/openapi/models/v1_list_aggregated_pod_metrics_response.py +123 -0
  22. lightning_sdk/lightning_cloud/openapi/models/v1_node_metrics.py +79 -1
  23. lightning_sdk/lightning_cloud/openapi/models/v1_organization.py +27 -1
  24. lightning_sdk/lightning_cloud/openapi/models/v1_pod_metrics.py +157 -1
  25. lightning_sdk/lightning_cloud/openapi/models/v1_project_cluster_binding.py +27 -1
  26. lightning_sdk/lightning_cloud/openapi/models/v1_quote_annual_upsell_response.py +201 -0
  27. lightning_sdk/lightning_cloud/openapi/models/v1_update_cloud_space_instance_config_request.py +3 -81
  28. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +1 -131
  29. lightning_sdk/llm/llm.py +2 -2
  30. lightning_sdk/studio.py +39 -6
  31. lightning_sdk/utils/progress.py +284 -0
  32. {lightning_sdk-2025.8.18.post0.dist-info → lightning_sdk-2025.8.21.dist-info}/METADATA +1 -1
  33. {lightning_sdk-2025.8.18.post0.dist-info → lightning_sdk-2025.8.21.dist-info}/RECORD +37 -34
  34. {lightning_sdk-2025.8.18.post0.dist-info → lightning_sdk-2025.8.21.dist-info}/LICENSE +0 -0
  35. {lightning_sdk-2025.8.18.post0.dist-info → lightning_sdk-2025.8.21.dist-info}/WHEEL +0 -0
  36. {lightning_sdk-2025.8.18.post0.dist-info → lightning_sdk-2025.8.21.dist-info}/entry_points.txt +0 -0
  37. {lightning_sdk-2025.8.18.post0.dist-info → lightning_sdk-2025.8.21.dist-info}/top_level.txt +0 -0
@@ -41,7 +41,6 @@ class V1UserFeatures(object):
41
41
  and the value is json key in definition.
42
42
  """
43
43
  swagger_types = {
44
- 'add_data_v2': 'bool',
45
44
  'affiliate_links': 'bool',
46
45
  'agents_v2': 'bool',
47
46
  'ai_hub_monetization': 'bool',
@@ -75,13 +74,11 @@ class V1UserFeatures(object):
75
74
  'f241': 'bool',
76
75
  'f242': 'bool',
77
76
  'f243': 'bool',
78
- 'f244': 'bool',
79
77
  'f245': 'bool',
80
78
  'fair_share': 'bool',
81
79
  'featured_studios_admin': 'bool',
82
80
  'gcp_overprovisioning': 'bool',
83
81
  'gcs_connections_optimized': 'bool',
84
- 'gcs_folders': 'bool',
85
82
  'instant_capacity_reservation': 'bool',
86
83
  'job_artifacts_v2': 'bool',
87
84
  'kubernetes_cluster_ui': 'bool',
@@ -109,7 +106,6 @@ class V1UserFeatures(object):
109
106
  'product_license': 'bool',
110
107
  'project_selector': 'bool',
111
108
  'publish_pipelines': 'bool',
112
- 'r2_data_connections': 'bool',
113
109
  'reserved_machines_tab': 'bool',
114
110
  'restartable_jobs': 'bool',
115
111
  'runnable_public_studio_page': 'bool',
@@ -123,7 +119,6 @@ class V1UserFeatures(object):
123
119
  'studio_sharing_v2': 'bool',
124
120
  'studio_version_visibility': 'bool',
125
121
  'trainium2': 'bool',
126
- 'use_internal_data_connection_mounts': 'bool',
127
122
  'use_rclone_mounts_only': 'bool',
128
123
  'vultr': 'bool',
129
124
  'weka': 'bool',
@@ -131,7 +126,6 @@ class V1UserFeatures(object):
131
126
  }
132
127
 
133
128
  attribute_map = {
134
- 'add_data_v2': 'addDataV2',
135
129
  'affiliate_links': 'affiliateLinks',
136
130
  'agents_v2': 'agentsV2',
137
131
  'ai_hub_monetization': 'aiHubMonetization',
@@ -165,13 +159,11 @@ class V1UserFeatures(object):
165
159
  'f241': 'f241',
166
160
  'f242': 'f242',
167
161
  'f243': 'f243',
168
- 'f244': 'f244',
169
162
  'f245': 'f245',
170
163
  'fair_share': 'fairShare',
171
164
  'featured_studios_admin': 'featuredStudiosAdmin',
172
165
  'gcp_overprovisioning': 'gcpOverprovisioning',
173
166
  'gcs_connections_optimized': 'gcsConnectionsOptimized',
174
- 'gcs_folders': 'gcsFolders',
175
167
  'instant_capacity_reservation': 'instantCapacityReservation',
176
168
  'job_artifacts_v2': 'jobArtifactsV2',
177
169
  'kubernetes_cluster_ui': 'kubernetesClusterUi',
@@ -199,7 +191,6 @@ class V1UserFeatures(object):
199
191
  'product_license': 'productLicense',
200
192
  'project_selector': 'projectSelector',
201
193
  'publish_pipelines': 'publishPipelines',
202
- 'r2_data_connections': 'r2DataConnections',
203
194
  'reserved_machines_tab': 'reservedMachinesTab',
204
195
  'restartable_jobs': 'restartableJobs',
205
196
  'runnable_public_studio_page': 'runnablePublicStudioPage',
@@ -213,16 +204,14 @@ class V1UserFeatures(object):
213
204
  'studio_sharing_v2': 'studioSharingV2',
214
205
  'studio_version_visibility': 'studioVersionVisibility',
215
206
  'trainium2': 'trainium2',
216
- 'use_internal_data_connection_mounts': 'useInternalDataConnectionMounts',
217
207
  'use_rclone_mounts_only': 'useRcloneMountsOnly',
218
208
  'vultr': 'vultr',
219
209
  'weka': 'weka',
220
210
  'writable_s3_connections': 'writableS3Connections'
221
211
  }
222
212
 
223
- def __init__(self, add_data_v2: 'bool' =None, affiliate_links: 'bool' =None, agents_v2: 'bool' =None, ai_hub_monetization: 'bool' =None, auto_fast_load: 'bool' =None, auto_join_orgs: 'bool' =None, b2c_experience: 'bool' =None, byo_machine_type: 'bool' =None, cap_add: 'list[str]' =None, cap_drop: 'list[str]' =None, capacity_reservation_byoc: 'bool' =None, capacity_reservation_dry_run: 'bool' =None, chat_models: 'bool' =None, cloudspace_schedules: 'bool' =None, code_tab: 'bool' =None, collab_screen_sharing: 'bool' =None, control_center_monitoring: 'bool' =None, cost_attribution_settings: 'bool' =None, custom_app_domain: 'bool' =None, datasets: 'bool' =None, default_one_cluster: 'bool' =None, deployment_persistent_disk: 'bool' =None, drive_v2: 'bool' =None, enterprise_compute_admin: 'bool' =None, f227: 'bool' =None, f234: 'bool' =None, f236: 'bool' =None, f237: 'bool' =None, f238: 'bool' =None, f239: 'bool' =None, f240: 'bool' =None, f241: 'bool' =None, f242: 'bool' =None, f243: 'bool' =None, f244: 'bool' =None, f245: 'bool' =None, fair_share: 'bool' =None, featured_studios_admin: 'bool' =None, gcp_overprovisioning: 'bool' =None, gcs_connections_optimized: 'bool' =None, gcs_folders: 'bool' =None, instant_capacity_reservation: 'bool' =None, job_artifacts_v2: 'bool' =None, kubernetes_cluster_ui: 'bool' =None, kubernetes_clusters: 'bool' =None, landing_studios: 'bool' =None, lit_logger: 'bool' =None, marketplace: 'bool' =None, mmt_fault_tolerance: 'bool' =None, mmt_strategy_selector: 'bool' =None, model_api_dashboard: 'bool' =None, multiple_studio_versions: 'bool' =None, nerf_fs_nonpaying: 'bool' =None, onboarding_v2: 'bool' =None, org_level_member_permissions: 'bool' =None, org_usage_limits: 'bool' =None, persistent_disk: 'bool' =None, plugin_distributed: 'bool' =None, plugin_inference: 'bool' =None, plugin_label_studio: 'bool' =None, plugin_langflow: 'bool' =None, plugin_python_profiler: 'bool' =None, plugin_sweeps: 'bool' =None, pricing_updates: 'bool' =None, product_generator: 'bool' =None, product_license: 'bool' =None, project_selector: 'bool' =None, publish_pipelines: 'bool' =None, r2_data_connections: 'bool' =None, reserved_machines_tab: 'bool' =None, restartable_jobs: 'bool' =None, runnable_public_studio_page: 'bool' =None, security_docs: 'bool' =None, show_dev_admin: 'bool' =None, single_wallet: 'bool' =None, slurm: 'bool' =None, specialised_studios: 'bool' =None, storage_overuse_deletion: 'bool' =None, studio_config: 'bool' =None, studio_sharing_v2: 'bool' =None, studio_version_visibility: 'bool' =None, trainium2: 'bool' =None, use_internal_data_connection_mounts: 'bool' =None, use_rclone_mounts_only: 'bool' =None, vultr: 'bool' =None, weka: 'bool' =None, writable_s3_connections: 'bool' =None): # noqa: E501
213
+ def __init__(self, affiliate_links: 'bool' =None, agents_v2: 'bool' =None, ai_hub_monetization: 'bool' =None, auto_fast_load: 'bool' =None, auto_join_orgs: 'bool' =None, b2c_experience: 'bool' =None, byo_machine_type: 'bool' =None, cap_add: 'list[str]' =None, cap_drop: 'list[str]' =None, capacity_reservation_byoc: 'bool' =None, capacity_reservation_dry_run: 'bool' =None, chat_models: 'bool' =None, cloudspace_schedules: 'bool' =None, code_tab: 'bool' =None, collab_screen_sharing: 'bool' =None, control_center_monitoring: 'bool' =None, cost_attribution_settings: 'bool' =None, custom_app_domain: 'bool' =None, datasets: 'bool' =None, default_one_cluster: 'bool' =None, deployment_persistent_disk: 'bool' =None, drive_v2: 'bool' =None, enterprise_compute_admin: 'bool' =None, f227: 'bool' =None, f234: 'bool' =None, f236: 'bool' =None, f237: 'bool' =None, f238: 'bool' =None, f239: 'bool' =None, f240: 'bool' =None, f241: 'bool' =None, f242: 'bool' =None, f243: 'bool' =None, f245: 'bool' =None, fair_share: 'bool' =None, featured_studios_admin: 'bool' =None, gcp_overprovisioning: 'bool' =None, gcs_connections_optimized: 'bool' =None, instant_capacity_reservation: 'bool' =None, job_artifacts_v2: 'bool' =None, kubernetes_cluster_ui: 'bool' =None, kubernetes_clusters: 'bool' =None, landing_studios: 'bool' =None, lit_logger: 'bool' =None, marketplace: 'bool' =None, mmt_fault_tolerance: 'bool' =None, mmt_strategy_selector: 'bool' =None, model_api_dashboard: 'bool' =None, multiple_studio_versions: 'bool' =None, nerf_fs_nonpaying: 'bool' =None, onboarding_v2: 'bool' =None, org_level_member_permissions: 'bool' =None, org_usage_limits: 'bool' =None, persistent_disk: 'bool' =None, plugin_distributed: 'bool' =None, plugin_inference: 'bool' =None, plugin_label_studio: 'bool' =None, plugin_langflow: 'bool' =None, plugin_python_profiler: 'bool' =None, plugin_sweeps: 'bool' =None, pricing_updates: 'bool' =None, product_generator: 'bool' =None, product_license: 'bool' =None, project_selector: 'bool' =None, publish_pipelines: 'bool' =None, reserved_machines_tab: 'bool' =None, restartable_jobs: 'bool' =None, runnable_public_studio_page: 'bool' =None, security_docs: 'bool' =None, show_dev_admin: 'bool' =None, single_wallet: 'bool' =None, slurm: 'bool' =None, specialised_studios: 'bool' =None, storage_overuse_deletion: 'bool' =None, studio_config: 'bool' =None, studio_sharing_v2: 'bool' =None, studio_version_visibility: 'bool' =None, trainium2: 'bool' =None, use_rclone_mounts_only: 'bool' =None, vultr: 'bool' =None, weka: 'bool' =None, writable_s3_connections: 'bool' =None): # noqa: E501
224
214
  """V1UserFeatures - a model defined in Swagger""" # noqa: E501
225
- self._add_data_v2 = None
226
215
  self._affiliate_links = None
227
216
  self._agents_v2 = None
228
217
  self._ai_hub_monetization = None
@@ -256,13 +245,11 @@ class V1UserFeatures(object):
256
245
  self._f241 = None
257
246
  self._f242 = None
258
247
  self._f243 = None
259
- self._f244 = None
260
248
  self._f245 = None
261
249
  self._fair_share = None
262
250
  self._featured_studios_admin = None
263
251
  self._gcp_overprovisioning = None
264
252
  self._gcs_connections_optimized = None
265
- self._gcs_folders = None
266
253
  self._instant_capacity_reservation = None
267
254
  self._job_artifacts_v2 = None
268
255
  self._kubernetes_cluster_ui = None
@@ -290,7 +277,6 @@ class V1UserFeatures(object):
290
277
  self._product_license = None
291
278
  self._project_selector = None
292
279
  self._publish_pipelines = None
293
- self._r2_data_connections = None
294
280
  self._reserved_machines_tab = None
295
281
  self._restartable_jobs = None
296
282
  self._runnable_public_studio_page = None
@@ -304,14 +290,11 @@ class V1UserFeatures(object):
304
290
  self._studio_sharing_v2 = None
305
291
  self._studio_version_visibility = None
306
292
  self._trainium2 = None
307
- self._use_internal_data_connection_mounts = None
308
293
  self._use_rclone_mounts_only = None
309
294
  self._vultr = None
310
295
  self._weka = None
311
296
  self._writable_s3_connections = None
312
297
  self.discriminator = None
313
- if add_data_v2 is not None:
314
- self.add_data_v2 = add_data_v2
315
298
  if affiliate_links is not None:
316
299
  self.affiliate_links = affiliate_links
317
300
  if agents_v2 is not None:
@@ -378,8 +361,6 @@ class V1UserFeatures(object):
378
361
  self.f242 = f242
379
362
  if f243 is not None:
380
363
  self.f243 = f243
381
- if f244 is not None:
382
- self.f244 = f244
383
364
  if f245 is not None:
384
365
  self.f245 = f245
385
366
  if fair_share is not None:
@@ -390,8 +371,6 @@ class V1UserFeatures(object):
390
371
  self.gcp_overprovisioning = gcp_overprovisioning
391
372
  if gcs_connections_optimized is not None:
392
373
  self.gcs_connections_optimized = gcs_connections_optimized
393
- if gcs_folders is not None:
394
- self.gcs_folders = gcs_folders
395
374
  if instant_capacity_reservation is not None:
396
375
  self.instant_capacity_reservation = instant_capacity_reservation
397
376
  if job_artifacts_v2 is not None:
@@ -446,8 +425,6 @@ class V1UserFeatures(object):
446
425
  self.project_selector = project_selector
447
426
  if publish_pipelines is not None:
448
427
  self.publish_pipelines = publish_pipelines
449
- if r2_data_connections is not None:
450
- self.r2_data_connections = r2_data_connections
451
428
  if reserved_machines_tab is not None:
452
429
  self.reserved_machines_tab = reserved_machines_tab
453
430
  if restartable_jobs is not None:
@@ -474,8 +451,6 @@ class V1UserFeatures(object):
474
451
  self.studio_version_visibility = studio_version_visibility
475
452
  if trainium2 is not None:
476
453
  self.trainium2 = trainium2
477
- if use_internal_data_connection_mounts is not None:
478
- self.use_internal_data_connection_mounts = use_internal_data_connection_mounts
479
454
  if use_rclone_mounts_only is not None:
480
455
  self.use_rclone_mounts_only = use_rclone_mounts_only
481
456
  if vultr is not None:
@@ -485,27 +460,6 @@ class V1UserFeatures(object):
485
460
  if writable_s3_connections is not None:
486
461
  self.writable_s3_connections = writable_s3_connections
487
462
 
488
- @property
489
- def add_data_v2(self) -> 'bool':
490
- """Gets the add_data_v2 of this V1UserFeatures. # noqa: E501
491
-
492
-
493
- :return: The add_data_v2 of this V1UserFeatures. # noqa: E501
494
- :rtype: bool
495
- """
496
- return self._add_data_v2
497
-
498
- @add_data_v2.setter
499
- def add_data_v2(self, add_data_v2: 'bool'):
500
- """Sets the add_data_v2 of this V1UserFeatures.
501
-
502
-
503
- :param add_data_v2: The add_data_v2 of this V1UserFeatures. # noqa: E501
504
- :type: bool
505
- """
506
-
507
- self._add_data_v2 = add_data_v2
508
-
509
463
  @property
510
464
  def affiliate_links(self) -> 'bool':
511
465
  """Gets the affiliate_links of this V1UserFeatures. # noqa: E501
@@ -1199,27 +1153,6 @@ class V1UserFeatures(object):
1199
1153
 
1200
1154
  self._f243 = f243
1201
1155
 
1202
- @property
1203
- def f244(self) -> 'bool':
1204
- """Gets the f244 of this V1UserFeatures. # noqa: E501
1205
-
1206
-
1207
- :return: The f244 of this V1UserFeatures. # noqa: E501
1208
- :rtype: bool
1209
- """
1210
- return self._f244
1211
-
1212
- @f244.setter
1213
- def f244(self, f244: 'bool'):
1214
- """Sets the f244 of this V1UserFeatures.
1215
-
1216
-
1217
- :param f244: The f244 of this V1UserFeatures. # noqa: E501
1218
- :type: bool
1219
- """
1220
-
1221
- self._f244 = f244
1222
-
1223
1156
  @property
1224
1157
  def f245(self) -> 'bool':
1225
1158
  """Gets the f245 of this V1UserFeatures. # noqa: E501
@@ -1325,27 +1258,6 @@ class V1UserFeatures(object):
1325
1258
 
1326
1259
  self._gcs_connections_optimized = gcs_connections_optimized
1327
1260
 
1328
- @property
1329
- def gcs_folders(self) -> 'bool':
1330
- """Gets the gcs_folders of this V1UserFeatures. # noqa: E501
1331
-
1332
-
1333
- :return: The gcs_folders of this V1UserFeatures. # noqa: E501
1334
- :rtype: bool
1335
- """
1336
- return self._gcs_folders
1337
-
1338
- @gcs_folders.setter
1339
- def gcs_folders(self, gcs_folders: 'bool'):
1340
- """Sets the gcs_folders of this V1UserFeatures.
1341
-
1342
-
1343
- :param gcs_folders: The gcs_folders of this V1UserFeatures. # noqa: E501
1344
- :type: bool
1345
- """
1346
-
1347
- self._gcs_folders = gcs_folders
1348
-
1349
1261
  @property
1350
1262
  def instant_capacity_reservation(self) -> 'bool':
1351
1263
  """Gets the instant_capacity_reservation of this V1UserFeatures. # noqa: E501
@@ -1913,27 +1825,6 @@ class V1UserFeatures(object):
1913
1825
 
1914
1826
  self._publish_pipelines = publish_pipelines
1915
1827
 
1916
- @property
1917
- def r2_data_connections(self) -> 'bool':
1918
- """Gets the r2_data_connections of this V1UserFeatures. # noqa: E501
1919
-
1920
-
1921
- :return: The r2_data_connections of this V1UserFeatures. # noqa: E501
1922
- :rtype: bool
1923
- """
1924
- return self._r2_data_connections
1925
-
1926
- @r2_data_connections.setter
1927
- def r2_data_connections(self, r2_data_connections: 'bool'):
1928
- """Sets the r2_data_connections of this V1UserFeatures.
1929
-
1930
-
1931
- :param r2_data_connections: The r2_data_connections of this V1UserFeatures. # noqa: E501
1932
- :type: bool
1933
- """
1934
-
1935
- self._r2_data_connections = r2_data_connections
1936
-
1937
1828
  @property
1938
1829
  def reserved_machines_tab(self) -> 'bool':
1939
1830
  """Gets the reserved_machines_tab of this V1UserFeatures. # noqa: E501
@@ -2207,27 +2098,6 @@ class V1UserFeatures(object):
2207
2098
 
2208
2099
  self._trainium2 = trainium2
2209
2100
 
2210
- @property
2211
- def use_internal_data_connection_mounts(self) -> 'bool':
2212
- """Gets the use_internal_data_connection_mounts of this V1UserFeatures. # noqa: E501
2213
-
2214
-
2215
- :return: The use_internal_data_connection_mounts of this V1UserFeatures. # noqa: E501
2216
- :rtype: bool
2217
- """
2218
- return self._use_internal_data_connection_mounts
2219
-
2220
- @use_internal_data_connection_mounts.setter
2221
- def use_internal_data_connection_mounts(self, use_internal_data_connection_mounts: 'bool'):
2222
- """Sets the use_internal_data_connection_mounts of this V1UserFeatures.
2223
-
2224
-
2225
- :param use_internal_data_connection_mounts: The use_internal_data_connection_mounts of this V1UserFeatures. # noqa: E501
2226
- :type: bool
2227
- """
2228
-
2229
- self._use_internal_data_connection_mounts = use_internal_data_connection_mounts
2230
-
2231
2101
  @property
2232
2102
  def use_rclone_mounts_only(self) -> 'bool':
2233
2103
  """Gets the use_rclone_mounts_only of this V1UserFeatures. # noqa: E501
lightning_sdk/llm/llm.py CHANGED
@@ -285,7 +285,7 @@ class LLM:
285
285
  self,
286
286
  prompt: str,
287
287
  system_prompt: Optional[str] = None,
288
- max_completion_tokens: Optional[int] = 500,
288
+ max_completion_tokens: Optional[int] = None,
289
289
  images: Optional[Union[List[str], str]] = None,
290
290
  conversation: Optional[str] = None,
291
291
  metadata: Optional[Dict[str, str]] = None,
@@ -319,7 +319,7 @@ class LLM:
319
319
  self,
320
320
  prompt: str,
321
321
  system_prompt: Optional[str] = None,
322
- max_completion_tokens: Optional[int] = 500,
322
+ max_completion_tokens: Optional[int] = None,
323
323
  images: Optional[Union[List[str], str]] = None,
324
324
  conversation: Optional[str] = None,
325
325
  metadata: Optional[Dict[str, str]] = None,
lightning_sdk/studio.py CHANGED
@@ -59,6 +59,9 @@ class Studio:
59
59
  _skip_init = False
60
60
  _skip_setup = False
61
61
 
62
+ # whether to show progress bars during operations
63
+ show_progress = False
64
+
62
65
  def __init__(
63
66
  self,
64
67
  name: Optional[str] = None,
@@ -254,9 +257,26 @@ class Studio:
254
257
 
255
258
  if status != Status.Stopped:
256
259
  raise RuntimeError(f"Cannot start a studio that is not stopped. Studio {self.name} is {status}.")
257
- self._studio_api.start_studio(
258
- self._studio.id, self._teamspace.id, machine, interruptible=interruptible, max_runtime=max_runtime
259
- )
260
+
261
+ # Show progress bar during startup
262
+ if self.show_progress:
263
+ from lightning_sdk.utils.progress import StudioProgressTracker
264
+
265
+ with StudioProgressTracker("start", show_progress=True) as progress:
266
+ # Start the studio without blocking
267
+ self._studio_api.start_studio_async(
268
+ self._studio.id, self._teamspace.id, machine, interruptible=interruptible, max_runtime=max_runtime
269
+ )
270
+
271
+ # Track progress through completion
272
+ progress.track_startup_phases(
273
+ lambda: self._studio_api.get_studio_status(self._studio.id, self._teamspace.id)
274
+ )
275
+ else:
276
+ # Use the blocking version if no progress is needed
277
+ self._studio_api.start_studio(
278
+ self._studio.id, self._teamspace.id, machine, interruptible=interruptible, max_runtime=max_runtime
279
+ )
260
280
 
261
281
  self._setup()
262
282
 
@@ -324,9 +344,22 @@ class Studio:
324
344
  raise RuntimeError(
325
345
  f"Cannot switch machine on a studio that is not running. Studio {self.name} is {status}."
326
346
  )
327
- self._studio_api.switch_studio_machine(
328
- self._studio.id, self._teamspace.id, machine, interruptible=interruptible
329
- )
347
+
348
+ if self.show_progress:
349
+ from lightning_sdk.utils.progress import StudioProgressTracker
350
+
351
+ with StudioProgressTracker("switch", show_progress=True) as progress:
352
+ # Update progress before starting the switch
353
+ progress.update_progress(5, "Initiating machine switch...")
354
+
355
+ # Start the switch operation with progress tracking
356
+ self._studio_api.switch_studio_machine_with_progress(
357
+ self._studio.id, self._teamspace.id, machine, interruptible=interruptible, progress=progress
358
+ )
359
+ else:
360
+ self._studio_api.switch_studio_machine(
361
+ self._studio.id, self._teamspace.id, machine, interruptible=interruptible
362
+ )
330
363
 
331
364
  def run_and_detach(self, *commands: str, timeout: float = 10, check_interval: float = 1) -> str:
332
365
  """Runs given commands on the Studio and returns immediately.
@@ -0,0 +1,284 @@
1
+ """Studio startup/switch progress bar utilities."""
2
+
3
+ import time
4
+ import types
5
+ from enum import Enum
6
+ from typing import Any, Callable, List, Optional, Type, Union
7
+
8
+ from rich.console import Console
9
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TaskID, TextColumn, TimeElapsedColumn
10
+
11
+ from lightning_sdk.lightning_cloud.openapi.models.v1_cluster_accelerator import V1ClusterAccelerator
12
+ from lightning_sdk.lightning_cloud.openapi.models.v1_get_cloud_space_instance_status_response import (
13
+ V1GetCloudSpaceInstanceStatusResponse,
14
+ )
15
+
16
+
17
+ class StartupPhase(Enum):
18
+ """Studio startup phase messages."""
19
+
20
+ ALLOCATING_MACHINE = "Allocating machine from cloud provider..."
21
+ STUDIO_STARTING = "Studio is starting up..."
22
+ SETTING_UP_ENVIRONMENT = "Setting up Studio environment..."
23
+ RESTORING_STATE = "Restoring Studio state..."
24
+ FINALIZING_SETUP = "Finalizing Studio setup..."
25
+ COMPLETED = "Studio started successfully"
26
+
27
+
28
+ def get_switching_progress_message(percentage: int, is_base_studio: bool, is_new_cloud_space: bool) -> str:
29
+ """Get progress message for switching studios."""
30
+ percentage = max(0, min(100, round(percentage)))
31
+
32
+ if percentage > 98:
33
+ message = "Done"
34
+ elif percentage > 80:
35
+ if is_new_cloud_space:
36
+ message = "Setting up Base Studio..." if is_base_studio else "Preparing Studio..."
37
+ else:
38
+ message = "Restoring Studio..."
39
+ elif percentage > 60:
40
+ message = "Setting up machine from the cloud provider"
41
+ else:
42
+ message = "Allocating machine from the cloud provider"
43
+
44
+ return f"({percentage}%) {message}"
45
+
46
+
47
+ def estimated_studio_ready_in_seconds(
48
+ cloud_space: Any, cloud_space_instance_status: Any, accelerators: Optional[List[V1ClusterAccelerator]] = None
49
+ ) -> int:
50
+ """Calculate estimated seconds until studio is ready."""
51
+ # Default estimate
52
+ return 120
53
+
54
+
55
+ def progress_bar_growth(default_timeout: int, counter: float) -> int:
56
+ """Calculate progress bar growth based on timeout and counter."""
57
+ if default_timeout <= 0:
58
+ return 100
59
+
60
+ value = ((default_timeout - counter) / default_timeout) * 100
61
+ if value > 100:
62
+ value = 100
63
+ if value < 0:
64
+ value = 0
65
+ return int(value)
66
+
67
+
68
+ class StudioProgressTracker:
69
+ """Tracks and displays progress for studio startup/switching operations."""
70
+
71
+ def __init__(self, operation_type: str = "start", show_progress: bool = True, check_interval: float = 1.0) -> None:
72
+ """Initialize progress tracker.
73
+
74
+ Args:
75
+ operation_type: Type of operation ('start' or 'switch')
76
+ show_progress: Whether to display progress bar
77
+ check_interval: Seconds between status checks
78
+ """
79
+ self.operation_type = operation_type
80
+ self.show_progress = show_progress
81
+ self.check_interval = check_interval
82
+ self.progress: Optional[Progress] = None
83
+ self.task_id: Optional[TaskID] = None
84
+ self.console = Console()
85
+ self._last_message = ""
86
+
87
+ def __enter__(self) -> "StudioProgressTracker":
88
+ """Enter context manager."""
89
+ if self.show_progress:
90
+ self.progress = Progress(
91
+ SpinnerColumn(),
92
+ TextColumn("[progress.description]{task.description}"),
93
+ BarColumn(),
94
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
95
+ TimeElapsedColumn(),
96
+ console=self.console,
97
+ transient=False,
98
+ )
99
+ self.progress.start()
100
+ self.task_id = self.progress.add_task(f"{self.operation_type.capitalize()}ing Studio...", total=100)
101
+ return self
102
+
103
+ def __exit__(
104
+ self,
105
+ exc_type: Union[Type[BaseException], None],
106
+ exc_val: Union[BaseException, None],
107
+ exc_tb: Union[types.TracebackType, None],
108
+ ) -> None:
109
+ """Exit context manager."""
110
+ if self.progress:
111
+ self.progress.stop()
112
+
113
+ def update_progress(
114
+ self, percentage: int, message: str = "", is_base_studio: bool = False, is_new_cloud_space: bool = False
115
+ ) -> None:
116
+ """Update progress bar with current percentage and message."""
117
+ if not self.progress or self.task_id is None:
118
+ return
119
+
120
+ if self.operation_type == "switch":
121
+ display_message = get_switching_progress_message(percentage, is_base_studio, is_new_cloud_space)
122
+ else:
123
+ display_message = message or f"{self.operation_type.capitalize()}ing Studio..."
124
+
125
+ # Update description if message changed
126
+ if display_message != self._last_message:
127
+ self.progress.update(self.task_id, description=display_message)
128
+ self._last_message = display_message
129
+
130
+ # Never show 100% until truly complete
131
+ completed = min(percentage, 98) if percentage < 100 else 100
132
+ self.progress.update(self.task_id, completed=completed)
133
+
134
+ # Force console refresh to ensure progress is visible
135
+ self.progress.refresh()
136
+
137
+ def complete(self, success_message: str = "") -> None:
138
+ """Mark operation as complete."""
139
+ if self.progress and self.task_id is not None:
140
+ self.progress.update(self.task_id, completed=100, description=success_message or "Done")
141
+
142
+ def track_startup_phases(
143
+ self,
144
+ status_getter: Callable[[], V1GetCloudSpaceInstanceStatusResponse],
145
+ accelerators: Optional[List[V1ClusterAccelerator]] = None,
146
+ timeout: int = 600,
147
+ ) -> None:
148
+ """Track startup phases and update progress accordingly."""
149
+ start_time = time.time()
150
+ last_progress = 5
151
+ phase_start_times = {}
152
+ last_message_time = 0
153
+ message_stability_delay = 3.0 # Seconds to wait before changing message
154
+
155
+ # Show initial progress immediately
156
+ self.update_progress(5, StartupPhase.ALLOCATING_MACHINE.value)
157
+
158
+ while True:
159
+ try:
160
+ status = status_getter()
161
+ elapsed = time.time() - start_time
162
+
163
+ # Default fallback progress based on time
164
+ time_based_progress = min(95, int((elapsed / timeout) * 100))
165
+ current_progress = max(last_progress, time_based_progress)
166
+ current_message = StartupPhase.ALLOCATING_MACHINE.value
167
+
168
+ # Check if we have detailed status information
169
+ if hasattr(status, "in_use") and status.in_use:
170
+ in_use = status.in_use
171
+
172
+ # Check startup status for detailed phases
173
+ if hasattr(in_use, "startup_status") and in_use.startup_status:
174
+ startup_status = in_use.startup_status
175
+
176
+ # Check completion first
177
+ if (
178
+ hasattr(startup_status, "top_up_restore_finished")
179
+ and startup_status.top_up_restore_finished
180
+ ):
181
+ self.complete(StartupPhase.COMPLETED.value)
182
+ break
183
+
184
+ # Check other phases in descending priority
185
+ if (
186
+ hasattr(startup_status, "initial_restore_finished")
187
+ and startup_status.initial_restore_finished
188
+ ):
189
+ current_progress = max(current_progress, 85)
190
+ current_message = StartupPhase.FINALIZING_SETUP.value
191
+ elif hasattr(startup_status, "container_ready") and startup_status.container_ready:
192
+ current_progress = max(current_progress, 70)
193
+ current_message = StartupPhase.RESTORING_STATE.value
194
+ elif hasattr(startup_status, "machine_ready") and startup_status.machine_ready:
195
+ current_progress = max(current_progress, 60)
196
+ current_message = StartupPhase.SETTING_UP_ENVIRONMENT.value
197
+
198
+ # Check general phase information
199
+ if hasattr(in_use, "phase") and in_use.phase:
200
+ phase = in_use.phase
201
+
202
+ if phase == "CLOUD_SPACE_INSTANCE_STATE_RUNNING":
203
+ current_progress = max(current_progress, 80)
204
+ current_message = StartupPhase.STUDIO_STARTING.value
205
+ elif phase == "CLOUD_SPACE_INSTANCE_STATE_PENDING":
206
+ # Track time in pending phase for smoother progress
207
+ if "pending" not in phase_start_times:
208
+ phase_start_times["pending"] = time.time()
209
+
210
+ pending_elapsed = time.time() - phase_start_times["pending"]
211
+ # Progress more smoothly through pending phase (10-60%)
212
+ pending_progress = 10 + min(50, int((pending_elapsed / 60) * 50))
213
+ current_progress = max(current_progress, pending_progress)
214
+ current_message = StartupPhase.ALLOCATING_MACHINE.value
215
+
216
+ # Check for requested machine status (pre-allocation)
217
+ elif hasattr(status, "requested") and status.requested:
218
+ if "allocation" not in phase_start_times:
219
+ phase_start_times["allocation"] = time.time()
220
+
221
+ allocation_elapsed = time.time() - phase_start_times["allocation"]
222
+ # Progress through allocation phase (5-30%)
223
+ allocation_progress = 5 + min(25, int((allocation_elapsed / 30) * 25))
224
+ current_progress = max(current_progress, allocation_progress)
225
+ current_message = StartupPhase.ALLOCATING_MACHINE.value
226
+
227
+ # Ensure progress never decreases and moves smoothly
228
+ if current_progress > last_progress:
229
+ # Smooth progress increases - don't jump too much at once
230
+ max_increment = 3 if current_progress - last_progress > 10 else current_progress - last_progress
231
+ current_progress = last_progress + max_increment
232
+
233
+ current_progress = min(98, current_progress) # Never show 100% until truly complete
234
+
235
+ # Only update message if enough time has passed since last message change
236
+ # or if this is the first message
237
+ current_time = time.time()
238
+ should_update_message = current_message != self._last_message and (
239
+ current_time - last_message_time >= message_stability_delay or last_message_time == 0
240
+ )
241
+
242
+ if should_update_message:
243
+ self.update_progress(current_progress, current_message)
244
+ last_message_time = current_time
245
+ else:
246
+ # Update progress but keep existing message
247
+ if self.progress and self.task_id is not None:
248
+ self.progress.update(self.task_id, completed=current_progress)
249
+ self.progress.refresh()
250
+
251
+ last_progress = current_progress
252
+
253
+ # Break if we timeout
254
+ if elapsed > timeout:
255
+ self.complete("Studio start may still be in progress...")
256
+ break
257
+
258
+ except Exception:
259
+ # Continue on API errors but still update progress
260
+ elapsed = time.time() - start_time
261
+ fallback_progress = min(95, max(last_progress, int((elapsed / timeout) * 100)))
262
+
263
+ # Only update message if enough time has passed
264
+ current_time = time.time()
265
+ should_update_message = StartupPhase.ALLOCATING_MACHINE.value != self._last_message and (
266
+ current_time - last_message_time >= message_stability_delay or last_message_time == 0
267
+ )
268
+
269
+ if should_update_message:
270
+ self.update_progress(fallback_progress, StartupPhase.ALLOCATING_MACHINE.value)
271
+ last_message_time = current_time
272
+ else:
273
+ # Update progress but keep existing message
274
+ if self.progress and self.task_id is not None:
275
+ self.progress.update(self.task_id, completed=fallback_progress)
276
+ self.progress.refresh()
277
+
278
+ last_progress = fallback_progress
279
+
280
+ if elapsed > timeout:
281
+ self.complete("Studio start may still be in progress...")
282
+ break
283
+
284
+ time.sleep(self.check_interval)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lightning_sdk
3
- Version: 2025.8.18.post0
3
+ Version: 2025.8.21
4
4
  Summary: SDK to develop using Lightning AI Studios
5
5
  Author-email: Lightning-AI <justus@lightning.ai>
6
6
  License: MIT License