lightning-sdk 2025.8.14__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.
- lightning_sdk/__init__.py +1 -1
- lightning_sdk/api/studio_api.py +7 -10
- lightning_sdk/cli/__init__.py +1 -0
- lightning_sdk/cli/config/__init__.py +14 -0
- lightning_sdk/cli/config/get.py +41 -0
- lightning_sdk/cli/config/set.py +77 -0
- lightning_sdk/cli/config/show.py +9 -0
- lightning_sdk/cli/entrypoint.py +60 -41
- lightning_sdk/cli/groups.py +35 -0
- lightning_sdk/cli/job/__init__.py +7 -0
- lightning_sdk/cli/{configure.py → legacy/configure.py} +2 -2
- lightning_sdk/cli/{connect.py → legacy/connect.py} +2 -2
- lightning_sdk/cli/{create.py → legacy/create.py} +1 -1
- lightning_sdk/cli/{delete.py → legacy/delete.py} +3 -3
- lightning_sdk/cli/legacy/deploy/__init__.py +0 -0
- lightning_sdk/cli/{deploy → legacy/deploy}/_auth.py +1 -1
- lightning_sdk/cli/{deploy → legacy/deploy}/devbox.py +8 -2
- lightning_sdk/cli/{deploy → legacy/deploy}/serve.py +3 -3
- lightning_sdk/cli/{download.py → legacy/download.py} +3 -3
- lightning_sdk/cli/legacy/entrypoint.py +110 -0
- lightning_sdk/cli/{generate.py → legacy/generate.py} +1 -1
- lightning_sdk/cli/{inspection.py → legacy/inspection.py} +1 -1
- lightning_sdk/cli/{job_and_mmt_action.py → legacy/job_and_mmt_action.py} +3 -3
- lightning_sdk/cli/{jobs_menu.py → legacy/jobs_menu.py} +1 -1
- lightning_sdk/cli/{list.py → legacy/list.py} +2 -2
- lightning_sdk/cli/{mmts_menu.py → legacy/mmts_menu.py} +1 -1
- lightning_sdk/cli/{open.py → legacy/open.py} +2 -2
- lightning_sdk/cli/{stop.py → legacy/stop.py} +1 -1
- lightning_sdk/cli/{teamspace_menu.py → legacy/teamspace_menu.py} +1 -1
- lightning_sdk/cli/{upload.py → legacy/upload.py} +3 -3
- lightning_sdk/cli/mmt/__init__.py +7 -0
- lightning_sdk/cli/studio/__init__.py +22 -0
- lightning_sdk/cli/studio/create.py +53 -0
- lightning_sdk/cli/studio/delete.py +44 -0
- lightning_sdk/cli/studio/list.py +67 -0
- lightning_sdk/cli/studio/ssh.py +112 -0
- lightning_sdk/cli/studio/start.py +63 -0
- lightning_sdk/cli/studio/stop.py +44 -0
- lightning_sdk/cli/studio/switch.py +52 -0
- lightning_sdk/cli/utils/__init__.py +7 -0
- lightning_sdk/cli/utils/cloud_account_map.py +10 -0
- lightning_sdk/cli/utils/resolve.py +28 -0
- lightning_sdk/cli/utils/richt_print.py +11 -0
- lightning_sdk/lightning_cloud/openapi/api/billing_service_api.py +5 -1
- lightning_sdk/lightning_cloud/openapi/api/k8_s_cluster_service_api.py +117 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_managed_model.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_notification_type.py +1 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +1 -27
- lightning_sdk/lightning_cloud/utils/data_connection.py +51 -1
- lightning_sdk/studio.py +19 -8
- lightning_sdk/teamspace.py +14 -0
- lightning_sdk/utils/config.py +155 -0
- lightning_sdk/utils/resolve.py +37 -3
- {lightning_sdk-2025.8.14.dist-info → lightning_sdk-2025.8.18.dist-info}/METADATA +2 -1
- {lightning_sdk-2025.8.14.dist-info → lightning_sdk-2025.8.18.dist-info}/RECORD +69 -47
- /lightning_sdk/cli/{deploy → legacy}/__init__.py +0 -0
- /lightning_sdk/cli/{ai_hub.py → legacy/ai_hub.py} +0 -0
- /lightning_sdk/cli/{clusters_menu.py → legacy/clusters_menu.py} +0 -0
- /lightning_sdk/cli/{docker_cli.py → legacy/docker_cli.py} +0 -0
- /lightning_sdk/cli/{exceptions.py → legacy/exceptions.py} +0 -0
- /lightning_sdk/cli/{run.py → legacy/run.py} +0 -0
- /lightning_sdk/cli/{start.py → legacy/start.py} +0 -0
- /lightning_sdk/cli/{studios_menu.py → legacy/studios_menu.py} +0 -0
- /lightning_sdk/cli/{switch.py → legacy/switch.py} +0 -0
- /lightning_sdk/cli/{coloring.py → utils/coloring.py} +0 -0
- {lightning_sdk-2025.8.14.dist-info → lightning_sdk-2025.8.18.dist-info}/LICENSE +0 -0
- {lightning_sdk-2025.8.14.dist-info → lightning_sdk-2025.8.18.dist-info}/WHEEL +0 -0
- {lightning_sdk-2025.8.14.dist-info → lightning_sdk-2025.8.18.dist-info}/entry_points.txt +0 -0
- {lightning_sdk-2025.8.14.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,
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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:
|
|
@@ -529,7 +542,7 @@ class Studio:
|
|
|
529
542
|
def auto_sleep(self, value: bool) -> None:
|
|
530
543
|
if not value and self.machine == Machine.CPU:
|
|
531
544
|
warnings.warn("Disabling auto-sleep will convert the Studio from free to paid!")
|
|
532
|
-
self._studio_api.update_autoshutdown(self._studio.id, self._teamspace.id, enabled=value
|
|
545
|
+
self._studio_api.update_autoshutdown(self._studio.id, self._teamspace.id, enabled=value)
|
|
533
546
|
self._update_studio_reference()
|
|
534
547
|
|
|
535
548
|
@property
|
|
@@ -540,9 +553,7 @@ class Studio:
|
|
|
540
553
|
@auto_sleep_time.setter
|
|
541
554
|
def auto_sleep_time(self, value: int) -> None:
|
|
542
555
|
warnings.warn("Setting auto-sleep time will convert the Studio from free to paid!")
|
|
543
|
-
self._studio_api.update_autoshutdown(
|
|
544
|
-
self._studio.id, self._teamspace.id, idle_shutdown_seconds=value, studio=self._studio
|
|
545
|
-
)
|
|
556
|
+
self._studio_api.update_autoshutdown(self._studio.id, self._teamspace.id, idle_shutdown_seconds=value)
|
|
546
557
|
self._update_studio_reference()
|
|
547
558
|
|
|
548
559
|
@property
|
lightning_sdk/teamspace.py
CHANGED
|
@@ -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
|
lightning_sdk/utils/resolve.py
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|