lightning-sdk 2025.8.14.post0__py3-none-any.whl → 2025.8.18__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 (68) hide show
  1. lightning_sdk/__init__.py +1 -1
  2. lightning_sdk/cli/__init__.py +1 -0
  3. lightning_sdk/cli/config/__init__.py +14 -0
  4. lightning_sdk/cli/config/get.py +41 -0
  5. lightning_sdk/cli/config/set.py +77 -0
  6. lightning_sdk/cli/config/show.py +9 -0
  7. lightning_sdk/cli/entrypoint.py +60 -41
  8. lightning_sdk/cli/groups.py +35 -0
  9. lightning_sdk/cli/job/__init__.py +7 -0
  10. lightning_sdk/cli/{configure.py → legacy/configure.py} +2 -2
  11. lightning_sdk/cli/{connect.py → legacy/connect.py} +2 -2
  12. lightning_sdk/cli/{create.py → legacy/create.py} +1 -1
  13. lightning_sdk/cli/{delete.py → legacy/delete.py} +3 -3
  14. lightning_sdk/cli/legacy/deploy/__init__.py +0 -0
  15. lightning_sdk/cli/{deploy → legacy/deploy}/_auth.py +1 -1
  16. lightning_sdk/cli/{deploy → legacy/deploy}/devbox.py +8 -2
  17. lightning_sdk/cli/{deploy → legacy/deploy}/serve.py +3 -3
  18. lightning_sdk/cli/{download.py → legacy/download.py} +3 -3
  19. lightning_sdk/cli/legacy/entrypoint.py +110 -0
  20. lightning_sdk/cli/{generate.py → legacy/generate.py} +1 -1
  21. lightning_sdk/cli/{inspection.py → legacy/inspection.py} +1 -1
  22. lightning_sdk/cli/{job_and_mmt_action.py → legacy/job_and_mmt_action.py} +3 -3
  23. lightning_sdk/cli/{jobs_menu.py → legacy/jobs_menu.py} +1 -1
  24. lightning_sdk/cli/{list.py → legacy/list.py} +2 -2
  25. lightning_sdk/cli/{mmts_menu.py → legacy/mmts_menu.py} +1 -1
  26. lightning_sdk/cli/{open.py → legacy/open.py} +2 -2
  27. lightning_sdk/cli/{stop.py → legacy/stop.py} +1 -1
  28. lightning_sdk/cli/{teamspace_menu.py → legacy/teamspace_menu.py} +1 -1
  29. lightning_sdk/cli/{upload.py → legacy/upload.py} +3 -3
  30. lightning_sdk/cli/mmt/__init__.py +7 -0
  31. lightning_sdk/cli/studio/__init__.py +22 -0
  32. lightning_sdk/cli/studio/create.py +53 -0
  33. lightning_sdk/cli/studio/delete.py +44 -0
  34. lightning_sdk/cli/studio/list.py +67 -0
  35. lightning_sdk/cli/studio/ssh.py +112 -0
  36. lightning_sdk/cli/studio/start.py +63 -0
  37. lightning_sdk/cli/studio/stop.py +44 -0
  38. lightning_sdk/cli/studio/switch.py +52 -0
  39. lightning_sdk/cli/utils/__init__.py +7 -0
  40. lightning_sdk/cli/utils/cloud_account_map.py +10 -0
  41. lightning_sdk/cli/utils/resolve.py +28 -0
  42. lightning_sdk/cli/utils/richt_print.py +11 -0
  43. lightning_sdk/lightning_cloud/openapi/api/billing_service_api.py +5 -1
  44. lightning_sdk/lightning_cloud/openapi/api/k8_s_cluster_service_api.py +117 -0
  45. lightning_sdk/lightning_cloud/openapi/models/v1_managed_model.py +29 -3
  46. lightning_sdk/lightning_cloud/openapi/models/v1_notification_type.py +1 -0
  47. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +1 -27
  48. lightning_sdk/lightning_cloud/utils/data_connection.py +51 -1
  49. lightning_sdk/studio.py +17 -4
  50. lightning_sdk/teamspace.py +14 -0
  51. lightning_sdk/utils/config.py +155 -0
  52. lightning_sdk/utils/resolve.py +37 -3
  53. {lightning_sdk-2025.8.14.post0.dist-info → lightning_sdk-2025.8.18.dist-info}/METADATA +2 -1
  54. {lightning_sdk-2025.8.14.post0.dist-info → lightning_sdk-2025.8.18.dist-info}/RECORD +68 -46
  55. /lightning_sdk/cli/{deploy → legacy}/__init__.py +0 -0
  56. /lightning_sdk/cli/{ai_hub.py → legacy/ai_hub.py} +0 -0
  57. /lightning_sdk/cli/{clusters_menu.py → legacy/clusters_menu.py} +0 -0
  58. /lightning_sdk/cli/{docker_cli.py → legacy/docker_cli.py} +0 -0
  59. /lightning_sdk/cli/{exceptions.py → legacy/exceptions.py} +0 -0
  60. /lightning_sdk/cli/{run.py → legacy/run.py} +0 -0
  61. /lightning_sdk/cli/{start.py → legacy/start.py} +0 -0
  62. /lightning_sdk/cli/{studios_menu.py → legacy/studios_menu.py} +0 -0
  63. /lightning_sdk/cli/{switch.py → legacy/switch.py} +0 -0
  64. /lightning_sdk/cli/{coloring.py → utils/coloring.py} +0 -0
  65. {lightning_sdk-2025.8.14.post0.dist-info → lightning_sdk-2025.8.18.dist-info}/LICENSE +0 -0
  66. {lightning_sdk-2025.8.14.post0.dist-info → lightning_sdk-2025.8.18.dist-info}/WHEEL +0 -0
  67. {lightning_sdk-2025.8.14.post0.dist-info → lightning_sdk-2025.8.18.dist-info}/entry_points.txt +0 -0
  68. {lightning_sdk-2025.8.14.post0.dist-info → lightning_sdk-2025.8.18.dist-info}/top_level.txt +0 -0
@@ -43,6 +43,123 @@ class K8SClusterServiceApi(object):
43
43
  api_client = ApiClient()
44
44
  self.api_client = api_client
45
45
 
46
+ def k8_s_cluster_service_list_aggregated_node_metrics(self, project_id: 'str', cluster_id: 'str', node_name: 'str', **kwargs) -> 'V1ListNodeMetricsResponse': # noqa: E501
47
+ """k8_s_cluster_service_list_aggregated_node_metrics # noqa: E501
48
+
49
+ This method makes a synchronous HTTP request by default. To make an
50
+ asynchronous HTTP request, please pass async_req=True
51
+ >>> thread = api.k8_s_cluster_service_list_aggregated_node_metrics(project_id, cluster_id, node_name, async_req=True)
52
+ >>> result = thread.get()
53
+
54
+ :param async_req bool
55
+ :param str project_id: (required)
56
+ :param str cluster_id: (required)
57
+ :param str node_name: (required)
58
+ :param datetime start: Date range.
59
+ :param datetime end:
60
+ :return: V1ListNodeMetricsResponse
61
+ If the method is called asynchronously,
62
+ returns the request thread.
63
+ """
64
+ kwargs['_return_http_data_only'] = True
65
+ if kwargs.get('async_req'):
66
+ return self.k8_s_cluster_service_list_aggregated_node_metrics_with_http_info(project_id, cluster_id, node_name, **kwargs) # noqa: E501
67
+ else:
68
+ (data) = self.k8_s_cluster_service_list_aggregated_node_metrics_with_http_info(project_id, cluster_id, node_name, **kwargs) # noqa: E501
69
+ return data
70
+
71
+ def k8_s_cluster_service_list_aggregated_node_metrics_with_http_info(self, project_id: 'str', cluster_id: 'str', node_name: 'str', **kwargs) -> 'V1ListNodeMetricsResponse': # noqa: E501
72
+ """k8_s_cluster_service_list_aggregated_node_metrics # noqa: E501
73
+
74
+ This method makes a synchronous HTTP request by default. To make an
75
+ asynchronous HTTP request, please pass async_req=True
76
+ >>> thread = api.k8_s_cluster_service_list_aggregated_node_metrics_with_http_info(project_id, cluster_id, node_name, async_req=True)
77
+ >>> result = thread.get()
78
+
79
+ :param async_req bool
80
+ :param str project_id: (required)
81
+ :param str cluster_id: (required)
82
+ :param str node_name: (required)
83
+ :param datetime start: Date range.
84
+ :param datetime end:
85
+ :return: V1ListNodeMetricsResponse
86
+ If the method is called asynchronously,
87
+ returns the request thread.
88
+ """
89
+
90
+ all_params = ['project_id', 'cluster_id', 'node_name', 'start', 'end'] # noqa: E501
91
+ all_params.append('async_req')
92
+ all_params.append('_return_http_data_only')
93
+ all_params.append('_preload_content')
94
+ all_params.append('_request_timeout')
95
+
96
+ params = locals()
97
+ for key, val in six.iteritems(params['kwargs']):
98
+ if key not in all_params:
99
+ raise TypeError(
100
+ "Got an unexpected keyword argument '%s'"
101
+ " to method k8_s_cluster_service_list_aggregated_node_metrics" % key
102
+ )
103
+ params[key] = val
104
+ del params['kwargs']
105
+ # verify the required parameter 'project_id' is set
106
+ if ('project_id' not in params or
107
+ params['project_id'] is None):
108
+ raise ValueError("Missing the required parameter `project_id` when calling `k8_s_cluster_service_list_aggregated_node_metrics`") # noqa: E501
109
+ # verify the required parameter 'cluster_id' is set
110
+ if ('cluster_id' not in params or
111
+ params['cluster_id'] is None):
112
+ raise ValueError("Missing the required parameter `cluster_id` when calling `k8_s_cluster_service_list_aggregated_node_metrics`") # noqa: E501
113
+ # verify the required parameter 'node_name' is set
114
+ if ('node_name' not in params or
115
+ params['node_name'] is None):
116
+ raise ValueError("Missing the required parameter `node_name` when calling `k8_s_cluster_service_list_aggregated_node_metrics`") # noqa: E501
117
+
118
+ collection_formats = {}
119
+
120
+ path_params = {}
121
+ if 'project_id' in params:
122
+ path_params['projectId'] = params['project_id'] # noqa: E501
123
+ if 'cluster_id' in params:
124
+ path_params['clusterId'] = params['cluster_id'] # noqa: E501
125
+ if 'node_name' in params:
126
+ path_params['nodeName'] = params['node_name'] # noqa: E501
127
+
128
+ query_params = []
129
+ if 'start' in params:
130
+ query_params.append(('start', params['start'])) # noqa: E501
131
+ if 'end' in params:
132
+ query_params.append(('end', params['end'])) # noqa: E501
133
+
134
+ header_params = {}
135
+
136
+ form_params = []
137
+ local_var_files = {}
138
+
139
+ body_params = None
140
+ # HTTP header `Accept`
141
+ header_params['Accept'] = self.api_client.select_header_accept(
142
+ ['application/json']) # noqa: E501
143
+
144
+ # Authentication setting
145
+ auth_settings = [] # noqa: E501
146
+
147
+ return self.api_client.call_api(
148
+ '/v1/projects/{projectId}/clusters/{clusterId}/aggregated-metrics/nodes/{nodeName}', 'GET',
149
+ path_params,
150
+ query_params,
151
+ header_params,
152
+ body=body_params,
153
+ post_params=form_params,
154
+ files=local_var_files,
155
+ response_type='V1ListNodeMetricsResponse', # noqa: E501
156
+ auth_settings=auth_settings,
157
+ async_req=params.get('async_req'),
158
+ _return_http_data_only=params.get('_return_http_data_only'),
159
+ _preload_content=params.get('_preload_content', True),
160
+ _request_timeout=params.get('_request_timeout'),
161
+ collection_formats=collection_formats)
162
+
46
163
  def k8_s_cluster_service_list_cluster_metrics(self, project_id: 'str', cluster_id: 'str', **kwargs) -> 'V1ListClusterMetricsResponse': # noqa: E501
47
164
  """k8_s_cluster_service_list_cluster_metrics # noqa: E501
48
165
 
@@ -60,7 +60,8 @@ class V1ManagedModel(object):
60
60
  'throughput': 'float',
61
61
  'time_to_first_token': 'float',
62
62
  'token_threshold': 'str',
63
- 'top_k': 'str'
63
+ 'top_k': 'str',
64
+ 'user_id': 'str'
64
65
  }
65
66
 
66
67
  attribute_map = {
@@ -83,10 +84,11 @@ class V1ManagedModel(object):
83
84
  'throughput': 'throughput',
84
85
  'time_to_first_token': 'timeToFirstToken',
85
86
  'token_threshold': 'tokenThreshold',
86
- 'top_k': 'topK'
87
+ 'top_k': 'topK',
88
+ 'user_id': 'userId'
87
89
  }
88
90
 
89
- def __init__(self, abilities: 'V1ManagedModelAbilities' =None, assistant_id: 'str' =None, completion_token_price: 'float' =None, completion_token_price_above_threshold: 'float' =None, context_length: 'str' =None, deployment_details: 'V1DeploymentDetails' =None, description: 'str' =None, display_name: 'str' =None, endpoint_id: 'str' =None, id: 'str' =None, max_completion_tokens: 'str' =None, name: 'str' =None, prompt_token_price: 'float' =None, prompt_token_price_above_threshold: 'float' =None, status: 'V1AssistantModelStatus' =None, temperature: 'float' =None, throughput: 'float' =None, time_to_first_token: 'float' =None, token_threshold: 'str' =None, top_k: 'str' =None): # noqa: E501
91
+ def __init__(self, abilities: 'V1ManagedModelAbilities' =None, assistant_id: 'str' =None, completion_token_price: 'float' =None, completion_token_price_above_threshold: 'float' =None, context_length: 'str' =None, deployment_details: 'V1DeploymentDetails' =None, description: 'str' =None, display_name: 'str' =None, endpoint_id: 'str' =None, id: 'str' =None, max_completion_tokens: 'str' =None, name: 'str' =None, prompt_token_price: 'float' =None, prompt_token_price_above_threshold: 'float' =None, status: 'V1AssistantModelStatus' =None, temperature: 'float' =None, throughput: 'float' =None, time_to_first_token: 'float' =None, token_threshold: 'str' =None, top_k: 'str' =None, user_id: 'str' =None): # noqa: E501
90
92
  """V1ManagedModel - a model defined in Swagger""" # noqa: E501
91
93
  self._abilities = None
92
94
  self._assistant_id = None
@@ -108,6 +110,7 @@ class V1ManagedModel(object):
108
110
  self._time_to_first_token = None
109
111
  self._token_threshold = None
110
112
  self._top_k = None
113
+ self._user_id = None
111
114
  self.discriminator = None
112
115
  if abilities is not None:
113
116
  self.abilities = abilities
@@ -149,6 +152,8 @@ class V1ManagedModel(object):
149
152
  self.token_threshold = token_threshold
150
153
  if top_k is not None:
151
154
  self.top_k = top_k
155
+ if user_id is not None:
156
+ self.user_id = user_id
152
157
 
153
158
  @property
154
159
  def abilities(self) -> 'V1ManagedModelAbilities':
@@ -570,6 +575,27 @@ class V1ManagedModel(object):
570
575
 
571
576
  self._top_k = top_k
572
577
 
578
+ @property
579
+ def user_id(self) -> 'str':
580
+ """Gets the user_id of this V1ManagedModel. # noqa: E501
581
+
582
+
583
+ :return: The user_id of this V1ManagedModel. # noqa: E501
584
+ :rtype: str
585
+ """
586
+ return self._user_id
587
+
588
+ @user_id.setter
589
+ def user_id(self, user_id: 'str'):
590
+ """Sets the user_id of this V1ManagedModel.
591
+
592
+
593
+ :param user_id: The user_id of this V1ManagedModel. # noqa: E501
594
+ :type: str
595
+ """
596
+
597
+ self._user_id = user_id
598
+
573
599
  def to_dict(self) -> dict:
574
600
  """Returns the model properties as a dict"""
575
601
  result = {}
@@ -41,6 +41,7 @@ class V1NotificationType(object):
41
41
  LOW_FUNDS = "NOTIFICATION_TYPE_LOW_FUNDS"
42
42
  LONG_WORKLOADS = "NOTIFICATION_TYPE_LONG_WORKLOADS"
43
43
  DEPLOYMENT_ERROR = "NOTIFICATION_TYPE_DEPLOYMENT_ERROR"
44
+ REQUESTED_ACCESS = "NOTIFICATION_TYPE_REQUESTED_ACCESS"
44
45
  """
45
46
  Attributes:
46
47
  swagger_types (dict): The key is attribute name
@@ -41,7 +41,6 @@ class V1UserFeatures(object):
41
41
  and the value is json key in definition.
42
42
  """
43
43
  swagger_types = {
44
- 'academic_tier': 'bool',
45
44
  'add_data_v2': 'bool',
46
45
  'affiliate_links': 'bool',
47
46
  'agents_v2': 'bool',
@@ -132,7 +131,6 @@ class V1UserFeatures(object):
132
131
  }
133
132
 
134
133
  attribute_map = {
135
- 'academic_tier': 'academicTier',
136
134
  'add_data_v2': 'addDataV2',
137
135
  'affiliate_links': 'affiliateLinks',
138
136
  'agents_v2': 'agentsV2',
@@ -222,9 +220,8 @@ class V1UserFeatures(object):
222
220
  'writable_s3_connections': 'writableS3Connections'
223
221
  }
224
222
 
225
- def __init__(self, academic_tier: 'bool' =None, 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
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
226
224
  """V1UserFeatures - a model defined in Swagger""" # noqa: E501
227
- self._academic_tier = None
228
225
  self._add_data_v2 = None
229
226
  self._affiliate_links = None
230
227
  self._agents_v2 = None
@@ -313,8 +310,6 @@ class V1UserFeatures(object):
313
310
  self._weka = None
314
311
  self._writable_s3_connections = None
315
312
  self.discriminator = None
316
- if academic_tier is not None:
317
- self.academic_tier = academic_tier
318
313
  if add_data_v2 is not None:
319
314
  self.add_data_v2 = add_data_v2
320
315
  if affiliate_links is not None:
@@ -490,27 +485,6 @@ class V1UserFeatures(object):
490
485
  if writable_s3_connections is not None:
491
486
  self.writable_s3_connections = writable_s3_connections
492
487
 
493
- @property
494
- def academic_tier(self) -> 'bool':
495
- """Gets the academic_tier of this V1UserFeatures. # noqa: E501
496
-
497
-
498
- :return: The academic_tier of this V1UserFeatures. # noqa: E501
499
- :rtype: bool
500
- """
501
- return self._academic_tier
502
-
503
- @academic_tier.setter
504
- def academic_tier(self, academic_tier: 'bool'):
505
- """Sets the academic_tier of this V1UserFeatures.
506
-
507
-
508
- :param academic_tier: The academic_tier of this V1UserFeatures. # noqa: E501
509
- :type: bool
510
- """
511
-
512
- self._academic_tier = academic_tier
513
-
514
488
  @property
515
489
  def add_data_v2(self) -> 'bool':
516
490
  """Gets the add_data_v2 of this V1UserFeatures. # noqa: E501
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  from time import sleep, time
3
3
  from lightning_sdk.lightning_cloud import rest_client
4
- from lightning_sdk.lightning_cloud.openapi import Create, V1AwsDataConnection, V1S3FolderDataConnection, V1EfsConfig, V1GcpDataConnection, V1FilestoreDataConnection, V1DataConnectionTier
4
+ from lightning_sdk.lightning_cloud.openapi import Create, V1AwsDataConnection, V1S3FolderDataConnection, V1EfsConfig, V1GcpDataConnection, V1FilestoreDataConnection, V1DataConnectionTier, V1R2DataConnection
5
5
  from lightning_sdk.lightning_cloud.openapi.rest import ApiException
6
6
  import urllib3
7
7
 
@@ -327,6 +327,56 @@ def create_filestore_folder(folder_name: str, region: str, capacity_gb: int = 10
327
327
  sleep(1)
328
328
 
329
329
 
330
+ def create_cloud_agnostic_folder(folder_name: str, create_timeout: int = 30) -> None:
331
+ """
332
+ Utility function to create a Cloud-Agnostic (R2) folder.
333
+
334
+ Args:
335
+ folder_name: The name of the folder to create.
336
+ create_timeout (int): The timeout for the folder creation.
337
+ """
338
+ client = rest_client.LightningClient(retry=False)
339
+
340
+ project_id = os.getenv("LIGHTNING_CLOUD_PROJECT_ID")
341
+ cluster_id = os.getenv("LIGHTNING_CLUSTER_ID")
342
+
343
+ # Get existing data connections
344
+ data_connections = client.data_connection_service_list_data_connections(project_id).data_connections
345
+
346
+ for connection in data_connections:
347
+ existing_folder_name = getattr(connection, 'name', None)
348
+ isCloudAgnostic = getattr(connection, 'r2_folder', None) is not None
349
+
350
+ if existing_folder_name == folder_name and isCloudAgnostic:
351
+ return
352
+
353
+ # If we get here, no matching folder was found, proceed with creation
354
+ body = Create(
355
+ name=folder_name,
356
+ create_resources=True,
357
+ r2=V1R2DataConnection(name=folder_name),
358
+ )
359
+ try:
360
+ connection = client.data_connection_service_create_data_connection(body, project_id)
361
+ except ApiException as e:
362
+ # Note: This function can be called in a distributed way.
363
+ # There is a race condition where one machine might create the entry before another machine
364
+ # and this request would fail with duplicated key
365
+ # In this case, it is fine not to raise
366
+ if'duplicate key value violates unique constraint' in str(e.body):
367
+ return
368
+ raise e from None
369
+
370
+ except urllib3.exceptions.HTTPError as e:
371
+ raise e from None
372
+
373
+ # Wait for the filesystem picks up the newly added Cloud-Agnostic folder
374
+ start = time()
375
+
376
+ while not os.path.isdir(f"/teamspace/lightning_storage/{folder_name}") and (time() - start) < create_timeout:
377
+ sleep(1)
378
+
379
+
330
380
  def delete_data_connection(name: str):
331
381
  """Utility to delete a data connection
332
382
 
lightning_sdk/studio.py CHANGED
@@ -98,12 +98,25 @@ class Studio:
98
98
  default_cloud_account=self._teamspace.default_cloud_account,
99
99
  )
100
100
 
101
+ # Resolve studio name if not provided: explicit → env (LIGHTNING_CLOUD_SPACE_ID) → config defaults
101
102
  if name is None:
102
103
  studio_id = os.environ.get("LIGHTNING_CLOUD_SPACE_ID", None)
103
- if studio_id is None:
104
- raise ValueError("Cannot autodetect Studio. Either use the SDK from within a Studio or pass a name!")
105
- self._studio = self._studio_api.get_studio_by_id(studio_id=studio_id, teamspace_id=self._teamspace.id)
106
- else:
104
+ if studio_id is not None:
105
+ # We're inside a studio, get it by ID
106
+ self._studio = self._studio_api.get_studio_by_id(studio_id=studio_id, teamspace_id=self._teamspace.id)
107
+ else:
108
+ # Try config defaults
109
+ from lightning_sdk.utils.config import Config, DefaultConfigKeys
110
+
111
+ config = Config()
112
+ name = config.get_value(DefaultConfigKeys.studio)
113
+ if name is None:
114
+ raise ValueError(
115
+ "Cannot autodetect Studio. Either use the SDK from within a Studio or pass a name!"
116
+ )
117
+
118
+ # If we have a name (explicit or from config), get studio by name
119
+ if name is not None:
107
120
  try:
108
121
  self._studio = self._studio_api.get_studio(name, self._teamspace.id)
109
122
  except ValueError as e:
@@ -70,6 +70,20 @@ class Teamspace:
70
70
  self._user = _resolve_user(user)
71
71
  self._org = _resolve_org(org)
72
72
 
73
+ # If still no user or org resolved, try config defaults
74
+ if self._user is None and self._org is None:
75
+ from lightning_sdk.utils.config import Config, DefaultConfigKeys
76
+
77
+ config = Config()
78
+ owner_type = config.get_value(DefaultConfigKeys.teamspace_owner_type)
79
+ owner_name = config.get_value(DefaultConfigKeys.teamspace_owner)
80
+
81
+ if owner_type and owner_name:
82
+ if owner_type.lower() == "organization":
83
+ self._org = _resolve_org(owner_name)
84
+ elif owner_type.lower() == "user":
85
+ self._user = _resolve_user(owner_name)
86
+
73
87
  self._owner: Owner
74
88
  if self._user is None and self._org is None:
75
89
  raise RuntimeError(
@@ -0,0 +1,155 @@
1
+ import os
2
+ from collections.abc import Mapping
3
+ from dataclasses import dataclass
4
+ from typing import Any, Dict, Optional, Sequence
5
+
6
+ import yaml
7
+
8
+ _DEFAULT_CONFIG_FILE_PATH = "~/.lightning/config.yaml"
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class DefaultConfigKeys:
13
+ """Default configuration keys for the Lightning SDK."""
14
+
15
+ organization: str = "organization.name"
16
+ user: str = "user.name"
17
+
18
+ teamspace_name: str = "teamspace.name"
19
+ teamspace_owner: str = "teamspace.owner"
20
+ teamspace_owner_type: str = "teamspace.owner_type"
21
+
22
+ machine: str = "machine.name"
23
+
24
+ studio: str = "studio.name"
25
+
26
+
27
+ class ConfigProxy:
28
+ def __init__(self, root: "Config", *path: str) -> None:
29
+ self._root = root
30
+ self._path = path # list of keys from root
31
+
32
+ def __getattr__(self, name: str) -> "ConfigProxy":
33
+ """Returns a reference to a nested ConfigProxy object a level deeper in the config hierarchy.
34
+
35
+ Args:
36
+ name: the name of the attribute to access, which corresponds to a key in the config.
37
+
38
+ Returns:
39
+ ConfigProxy: the next ConfigProxy object for the attribute.
40
+ """
41
+ # Build a deeper path and return a new proxy
42
+ return ConfigProxy(self._root, *self._path, name)
43
+
44
+ def __setattr__(self, name: str, value: Any) -> None:
45
+ """Sets the name attribute to value at the current hierarchy level.
46
+
47
+ Args:
48
+ name: the attribute name to set, which corresponds to a key in the config.
49
+ value: the value to set for the given attribute name in the config.
50
+ """
51
+ if name in ("_root", "_path"): # internal attributes
52
+ super().__setattr__(name, value)
53
+ else:
54
+ # Assign a nested value in the root config
55
+ self._root._set_nested([*self._path, name], value)
56
+
57
+
58
+ class Config:
59
+ def __init__(self, config_file: Optional[str] = None) -> None:
60
+ """Config class to manage configuration settings for the lightning SDK and CLI.
61
+
62
+ Args:
63
+ config_file: the file path where the configuration is stored.
64
+ If None, defaults to "~/.lightning/config.yaml".
65
+ """
66
+ if config_file is None:
67
+ config_file = _DEFAULT_CONFIG_FILE_PATH
68
+ self._config_file = os.path.expanduser(config_file)
69
+
70
+ def _load_config(self) -> Dict[str, Any]:
71
+ if not os.path.exists(self._config_file):
72
+ return {} # Return empty dict if config doesn't exist
73
+ with open(self._config_file) as f:
74
+ return yaml.safe_load(f) or {}
75
+
76
+ def _save_config(self, config: Dict[str, Any]) -> None:
77
+ os.makedirs(os.path.dirname(self._config_file), exist_ok=True)
78
+ config = _unflatten_dict(config)
79
+ with open(self._config_file, "w") as f:
80
+ yaml.safe_dump(config, f, default_flow_style=False, sort_keys=True)
81
+
82
+ def _set_nested(self, keys: Sequence[str], value: str) -> None:
83
+ config = self._load_config()
84
+ curr = config
85
+ for k in keys[:-1]:
86
+ if k not in curr or not isinstance(curr[k], Dict):
87
+ curr[k] = {}
88
+ curr = curr[k]
89
+ curr[keys[-1]] = value
90
+ self._save_config(config)
91
+
92
+ def get_value(self, key_path: str) -> Optional[str]:
93
+ """Gets a value from the config using dot notation.
94
+
95
+ Args:
96
+ key_path: the dot-separated path to the config value (e.g. "teamspace.name")
97
+
98
+ Returns:
99
+ The config value if found, None otherwise
100
+ """
101
+ config = self._load_config()
102
+ if not isinstance(config, Mapping):
103
+ return None
104
+
105
+ keys = key_path.split(".")
106
+ curr = config
107
+ for k in keys:
108
+ if not isinstance(curr, dict) or k not in curr:
109
+ return None
110
+ curr = curr[k]
111
+ return curr if isinstance(curr, str) else None
112
+
113
+ def __getattr__(self, name: str) -> ConfigProxy:
114
+ """Returns a proxy to the actual values to allow for nested access.
115
+
116
+ Args:
117
+ name: the name of the value to retrieve.
118
+
119
+ Returns:
120
+ ConfigProxy: a proxy object that allows nested access to the configuration.
121
+ """
122
+ return ConfigProxy(self, name)
123
+
124
+ def __setattr__(self, name: str, value: Any) -> None:
125
+ """Sets the name attribute to value at the root level."""
126
+ if name in ("_config_file",): # internal attributes
127
+ super().__setattr__(name, value)
128
+ else:
129
+ # Assign a value at the root level
130
+ self._set_nested([name], value)
131
+
132
+ def __repr__(self) -> str:
133
+ """Returns a string representation of the config."""
134
+ return str(self)
135
+
136
+ def __str__(self) -> str:
137
+ """Returns a string representation of the config."""
138
+ return yaml.dump(
139
+ {"Config": {"config_file": self._config_file, **self._load_config()}},
140
+ indent=4,
141
+ sort_keys=True,
142
+ )
143
+
144
+
145
+ def _unflatten_dict(flat_dict: Dict[str, Any]) -> Dict[str, Any]:
146
+ unflattened_dict = {}
147
+ for key, value in flat_dict.items():
148
+ keys = key.split(".")
149
+ curr = unflattened_dict
150
+ for k in keys[:-1]:
151
+ if k not in curr:
152
+ curr[k] = {}
153
+ curr = curr[k]
154
+ curr[keys[-1]] = value
155
+ return unflattened_dict
@@ -90,6 +90,11 @@ def _resolve_deprecated_cluster(cloud_account: Optional[str], cluster: Optional[
90
90
  def _resolve_org_name(name: Optional[str]) -> Optional[str]:
91
91
  if name is None:
92
92
  name = os.environ.get("LIGHTNING_ORG", "") or None
93
+ if name is None:
94
+ from lightning_sdk.utils.config import Config, DefaultConfigKeys
95
+
96
+ config = Config()
97
+ name = config.get_value(DefaultConfigKeys.organization)
93
98
  return name
94
99
 
95
100
 
@@ -118,6 +123,11 @@ def _resolve_org(org: Optional[Union[str, "Organization"]]) -> Optional["Organiz
118
123
  def _resolve_user_name(name: Optional[str]) -> Optional[str]:
119
124
  if name is None:
120
125
  name = os.environ.get("LIGHTNING_USERNAME", "") or None
126
+ if name is None:
127
+ from lightning_sdk.utils.config import Config, DefaultConfigKeys
128
+
129
+ config = Config()
130
+ name = config.get_value(DefaultConfigKeys.user)
121
131
  return name
122
132
 
123
133
 
@@ -137,6 +147,11 @@ def _resolve_user(user: Optional[Union[str, "User"]]) -> Optional["User"]:
137
147
  def _resolve_teamspace_name(name: Optional[str]) -> Optional[str]:
138
148
  if name is None:
139
149
  name = os.environ.get("LIGHTNING_TEAMSPACE", "") or None
150
+ if name is None:
151
+ from lightning_sdk.utils.config import Config, DefaultConfigKeys
152
+
153
+ config = Config()
154
+ name = config.get_value(DefaultConfigKeys.teamspace_name)
140
155
  return name
141
156
 
142
157
 
@@ -165,10 +180,29 @@ def _resolve_teamspace(
165
180
  return Teamspace(name=teamspace, org=org)
166
181
 
167
182
  user = _resolve_user(user)
168
- if user is None:
169
- raise RuntimeError("Neither user nor org provided, but one of them needs to be provided")
170
183
 
171
- return Teamspace(name=teamspace, user=user)
184
+ # If still no user or org resolved, try config defaults
185
+ if user is None and org is None:
186
+ from lightning_sdk.utils.config import Config, DefaultConfigKeys
187
+
188
+ config = Config()
189
+ owner_type = config.get_value(DefaultConfigKeys.teamspace_owner_type)
190
+ owner_name = config.get_value(DefaultConfigKeys.teamspace_owner)
191
+
192
+ if owner_type and owner_name:
193
+ if owner_type.lower() == "organization":
194
+ org = _resolve_org(owner_name)
195
+ elif owner_type.lower() == "user":
196
+ user = _resolve_user(owner_name)
197
+
198
+ # Final resolution check
199
+ if org is not None:
200
+ return Teamspace(name=teamspace, org=org)
201
+
202
+ if user is not None:
203
+ return Teamspace(name=teamspace, user=user)
204
+
205
+ raise RuntimeError("Neither user nor org provided, but one of them needs to be provided")
172
206
 
173
207
 
174
208
  def _get_organizations_for_authed_user() -> List["Organization"]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lightning_sdk
3
- Version: 2025.8.14.post0
3
+ Version: 2025.8.18
4
4
  Summary: SDK to develop using Lightning AI Studios
5
5
  Author-email: Lightning-AI <justus@lightning.ai>
6
6
  License: MIT License
@@ -39,6 +39,7 @@ Requires-Dist: docker
39
39
  Requires-Dist: fastapi
40
40
  Requires-Dist: packaging
41
41
  Requires-Dist: pyjwt
42
+ Requires-Dist: pyyaml
42
43
  Requires-Dist: requests
43
44
  Requires-Dist: rich
44
45
  Requires-Dist: simple-term-menu