clouditia-manager 1.0.0__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.
- clouditia_manager/__init__.py +43 -0
- clouditia_manager/client.py +543 -0
- clouditia_manager/exceptions.py +31 -0
- clouditia_manager-1.0.0.dist-info/METADATA +308 -0
- clouditia_manager-1.0.0.dist-info/RECORD +7 -0
- clouditia_manager-1.0.0.dist-info/WHEEL +5 -0
- clouditia_manager-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Clouditia Manager SDK - Manage GPU sessions via the Computing API
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from clouditia_manager import GPUManager
|
|
6
|
+
|
|
7
|
+
manager = GPUManager(api_key="sk_compute_...")
|
|
8
|
+
|
|
9
|
+
# Create a session
|
|
10
|
+
session = manager.create_session(
|
|
11
|
+
gpu_type="nvidia-rtx-3090",
|
|
12
|
+
vcpu=2,
|
|
13
|
+
ram=4,
|
|
14
|
+
storage=20
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# List sessions
|
|
18
|
+
sessions = manager.list_sessions()
|
|
19
|
+
|
|
20
|
+
# Stop a session
|
|
21
|
+
manager.stop_session("369bde33")
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
__version__ = "1.0.0"
|
|
25
|
+
__author__ = "Clouditia"
|
|
26
|
+
|
|
27
|
+
from .client import GPUManager
|
|
28
|
+
from .exceptions import (
|
|
29
|
+
ClouditiaManagerError,
|
|
30
|
+
AuthenticationError,
|
|
31
|
+
SessionNotFoundError,
|
|
32
|
+
InsufficientResourcesError,
|
|
33
|
+
APIError
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"GPUManager",
|
|
38
|
+
"ClouditiaManagerError",
|
|
39
|
+
"AuthenticationError",
|
|
40
|
+
"SessionNotFoundError",
|
|
41
|
+
"InsufficientResourcesError",
|
|
42
|
+
"APIError"
|
|
43
|
+
]
|
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Clouditia Manager SDK Client
|
|
3
|
+
|
|
4
|
+
Manage GPU sessions via the Computing API (sk_compute_ keys)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
import time
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Optional, Dict, List, Any, Callable
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
from .exceptions import (
|
|
15
|
+
ClouditiaManagerError,
|
|
16
|
+
AuthenticationError,
|
|
17
|
+
SessionNotFoundError,
|
|
18
|
+
InsufficientResourcesError,
|
|
19
|
+
APIError
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class GPUSession:
|
|
25
|
+
"""Represents a GPU session"""
|
|
26
|
+
id: str
|
|
27
|
+
short_id: str
|
|
28
|
+
name: str
|
|
29
|
+
status: str
|
|
30
|
+
gpu_type: str
|
|
31
|
+
gpu_count: int
|
|
32
|
+
vcpu: int
|
|
33
|
+
ram: str
|
|
34
|
+
storage: str
|
|
35
|
+
vscode_port: Optional[int]
|
|
36
|
+
jupyter_port: Optional[int]
|
|
37
|
+
password: Optional[str]
|
|
38
|
+
url: str
|
|
39
|
+
created_at: Optional[datetime]
|
|
40
|
+
started_at: Optional[datetime]
|
|
41
|
+
|
|
42
|
+
def __repr__(self):
|
|
43
|
+
return f"GPUSession(name='{self.name}', id='{self.short_id}', status='{self.status}')"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class GPUInventory:
|
|
48
|
+
"""Represents GPU inventory in marketplace"""
|
|
49
|
+
gpu_type: str
|
|
50
|
+
gpu_name: str
|
|
51
|
+
total: int
|
|
52
|
+
available: int
|
|
53
|
+
in_use: int
|
|
54
|
+
price_per_hour: float
|
|
55
|
+
|
|
56
|
+
def __repr__(self):
|
|
57
|
+
return f"GPUInventory(type='{self.gpu_type}', available={self.available}/{self.total})"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class GPUManager:
|
|
61
|
+
"""
|
|
62
|
+
Clouditia GPU Manager Client
|
|
63
|
+
|
|
64
|
+
Manage GPU sessions using the Computing API.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
api_key: Your sk_compute_ API key
|
|
68
|
+
base_url: API base URL (default: https://clouditia.com)
|
|
69
|
+
timeout: Request timeout in seconds (default: 60)
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
DEFAULT_BASE_URL = "https://clouditia.com"
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
api_key: str,
|
|
77
|
+
base_url: str = None,
|
|
78
|
+
timeout: int = 60
|
|
79
|
+
):
|
|
80
|
+
if not api_key or not api_key.startswith("sk_compute_"):
|
|
81
|
+
raise AuthenticationError("Invalid API key. Must start with 'sk_compute_'")
|
|
82
|
+
|
|
83
|
+
self.api_key = api_key
|
|
84
|
+
self.base_url = (base_url or self.DEFAULT_BASE_URL).rstrip("/")
|
|
85
|
+
self.timeout = timeout
|
|
86
|
+
self._session = requests.Session()
|
|
87
|
+
self._session.headers.update({
|
|
88
|
+
"Authorization": f"Bearer {api_key}",
|
|
89
|
+
"Content-Type": "application/json",
|
|
90
|
+
"User-Agent": "clouditia-manager/1.0.0"
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
# Verify API key on init
|
|
94
|
+
self._verify_api_key()
|
|
95
|
+
|
|
96
|
+
def _verify_api_key(self):
|
|
97
|
+
"""Verify the API key is valid"""
|
|
98
|
+
try:
|
|
99
|
+
response = self._request("GET", "/api/computing/verify/")
|
|
100
|
+
if not response.get("valid"):
|
|
101
|
+
raise AuthenticationError("API key is not valid")
|
|
102
|
+
self.user = response.get("user", {})
|
|
103
|
+
except requests.exceptions.RequestException as e:
|
|
104
|
+
raise APIError(f"Failed to verify API key: {e}")
|
|
105
|
+
|
|
106
|
+
def _request(
|
|
107
|
+
self,
|
|
108
|
+
method: str,
|
|
109
|
+
endpoint: str,
|
|
110
|
+
data: Dict = None,
|
|
111
|
+
params: Dict = None
|
|
112
|
+
) -> Dict:
|
|
113
|
+
"""Make an API request"""
|
|
114
|
+
url = f"{self.base_url}{endpoint}"
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
response = self._session.request(
|
|
118
|
+
method=method,
|
|
119
|
+
url=url,
|
|
120
|
+
json=data,
|
|
121
|
+
params=params,
|
|
122
|
+
timeout=self.timeout
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if response.status_code == 401:
|
|
126
|
+
raise AuthenticationError("Invalid or expired API key")
|
|
127
|
+
elif response.status_code == 403:
|
|
128
|
+
raise AuthenticationError("Access denied")
|
|
129
|
+
elif response.status_code == 404:
|
|
130
|
+
raise SessionNotFoundError("Resource not found")
|
|
131
|
+
|
|
132
|
+
response.raise_for_status()
|
|
133
|
+
return response.json()
|
|
134
|
+
|
|
135
|
+
except requests.exceptions.JSONDecodeError:
|
|
136
|
+
return {"raw": response.text}
|
|
137
|
+
except requests.exceptions.RequestException as e:
|
|
138
|
+
raise APIError(f"Request failed: {e}")
|
|
139
|
+
|
|
140
|
+
def create_session(
|
|
141
|
+
self,
|
|
142
|
+
gpu_type: str = "nvidia-rtx-3090",
|
|
143
|
+
gpu_count: int = 1,
|
|
144
|
+
vcpu: int = 4,
|
|
145
|
+
ram: int = 16,
|
|
146
|
+
storage: int = 20,
|
|
147
|
+
wait_ready: bool = True,
|
|
148
|
+
timeout: int = 180,
|
|
149
|
+
verbose: bool = True
|
|
150
|
+
) -> GPUSession:
|
|
151
|
+
"""
|
|
152
|
+
Create a new GPU session.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
gpu_type: GPU type slug (e.g., 'nvidia-rtx-3090', 'nvidia-rtx-4090')
|
|
156
|
+
gpu_count: Number of GPUs (default: 1)
|
|
157
|
+
vcpu: Number of vCPUs (default: 4)
|
|
158
|
+
ram: RAM in GB (default: 16)
|
|
159
|
+
storage: Storage in GB (default: 20)
|
|
160
|
+
wait_ready: Wait for session to be fully ready (default: True)
|
|
161
|
+
timeout: Max wait time in seconds (default: 180)
|
|
162
|
+
verbose: Print status messages (default: True)
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
GPUSession object with session details
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
InsufficientResourcesError: If requested GPU is not available
|
|
169
|
+
APIError: If session creation fails
|
|
170
|
+
"""
|
|
171
|
+
data = {
|
|
172
|
+
"gpu_type": gpu_type,
|
|
173
|
+
"gpu_count": gpu_count,
|
|
174
|
+
"vcpu": vcpu,
|
|
175
|
+
"ram": ram,
|
|
176
|
+
"storage": storage
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if verbose:
|
|
180
|
+
print(f"Creating GPU session with {gpu_type}...")
|
|
181
|
+
|
|
182
|
+
response = self._request("POST", "/api/computing/sessions/create/", data=data)
|
|
183
|
+
|
|
184
|
+
if not response.get("success"):
|
|
185
|
+
error = response.get("error", "Unknown error")
|
|
186
|
+
if "disponible" in error.lower() or "available" in error.lower():
|
|
187
|
+
raise InsufficientResourcesError(error)
|
|
188
|
+
raise APIError(error)
|
|
189
|
+
|
|
190
|
+
session_data = response.get("session", {})
|
|
191
|
+
session = self._parse_session(session_data)
|
|
192
|
+
|
|
193
|
+
if verbose:
|
|
194
|
+
print(f"Session created: {session.short_id}")
|
|
195
|
+
|
|
196
|
+
# Wait for session to be fully ready
|
|
197
|
+
if wait_ready:
|
|
198
|
+
session = self._wait_for_ready(session.short_id, timeout=timeout, verbose=verbose)
|
|
199
|
+
|
|
200
|
+
return session
|
|
201
|
+
|
|
202
|
+
def _wait_for_ready(
|
|
203
|
+
self,
|
|
204
|
+
session_id: str,
|
|
205
|
+
timeout: int = 180,
|
|
206
|
+
poll_interval: int = 3,
|
|
207
|
+
verbose: bool = True
|
|
208
|
+
) -> GPUSession:
|
|
209
|
+
"""
|
|
210
|
+
Internal method: Wait for session and GPU resources to be ready.
|
|
211
|
+
|
|
212
|
+
This method polls the session status until it's running and ready.
|
|
213
|
+
GPU resource updates are handled automatically by the server.
|
|
214
|
+
"""
|
|
215
|
+
if verbose:
|
|
216
|
+
print(f"Waiting for session {session_id} to be ready...", end="", flush=True)
|
|
217
|
+
|
|
218
|
+
start_time = time.time()
|
|
219
|
+
last_status = None
|
|
220
|
+
|
|
221
|
+
while time.time() - start_time < timeout:
|
|
222
|
+
try:
|
|
223
|
+
session = self.get_session(session_id)
|
|
224
|
+
|
|
225
|
+
if session.status != last_status:
|
|
226
|
+
last_status = session.status
|
|
227
|
+
if verbose and session.status == "pending":
|
|
228
|
+
print(".", end="", flush=True)
|
|
229
|
+
|
|
230
|
+
if session.status == "running":
|
|
231
|
+
# Give a moment for GPU resources to fully initialize
|
|
232
|
+
time.sleep(2)
|
|
233
|
+
|
|
234
|
+
if verbose:
|
|
235
|
+
print(" Ready!")
|
|
236
|
+
print(f"\n{'='*50}")
|
|
237
|
+
print(f" SESSION READY")
|
|
238
|
+
print(f"{'='*50}")
|
|
239
|
+
print(f" Name : {session.name}")
|
|
240
|
+
print(f" Short ID : {session.short_id}")
|
|
241
|
+
print(f" Status : {session.status}")
|
|
242
|
+
print(f" GPU : {session.gpu_type} x{session.gpu_count}")
|
|
243
|
+
print(f" vCPU : {session.vcpu}")
|
|
244
|
+
print(f" RAM : {session.ram}")
|
|
245
|
+
print(f" Storage : {session.storage}")
|
|
246
|
+
print(f" URL : {session.url}")
|
|
247
|
+
print(f" Password : {session.password}")
|
|
248
|
+
print(f"{'='*50}\n")
|
|
249
|
+
|
|
250
|
+
return session
|
|
251
|
+
|
|
252
|
+
elif session.status == "failed":
|
|
253
|
+
if verbose:
|
|
254
|
+
print(" Failed!")
|
|
255
|
+
raise APIError(f"Session {session_id} failed to start")
|
|
256
|
+
|
|
257
|
+
except SessionNotFoundError:
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
if verbose:
|
|
261
|
+
print(".", end="", flush=True)
|
|
262
|
+
|
|
263
|
+
time.sleep(poll_interval)
|
|
264
|
+
|
|
265
|
+
if verbose:
|
|
266
|
+
print(" Timeout!")
|
|
267
|
+
raise APIError(f"Timeout waiting for session {session_id} to be ready")
|
|
268
|
+
|
|
269
|
+
def stop_session(
|
|
270
|
+
self,
|
|
271
|
+
session_id: str,
|
|
272
|
+
wait_stopped: bool = True,
|
|
273
|
+
timeout: int = 120,
|
|
274
|
+
verbose: bool = True
|
|
275
|
+
) -> GPUSession:
|
|
276
|
+
"""
|
|
277
|
+
Stop a running GPU session.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
session_id: Session ID (short or full UUID)
|
|
281
|
+
wait_stopped: Wait for session to be fully stopped (default: True)
|
|
282
|
+
timeout: Max wait time in seconds (default: 120)
|
|
283
|
+
verbose: Print status messages (default: True)
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
GPUSession object with final status
|
|
287
|
+
|
|
288
|
+
Raises:
|
|
289
|
+
SessionNotFoundError: If session is not found
|
|
290
|
+
"""
|
|
291
|
+
# Get session info first
|
|
292
|
+
try:
|
|
293
|
+
session = self.get_session(session_id)
|
|
294
|
+
except SessionNotFoundError:
|
|
295
|
+
raise SessionNotFoundError(f"Session {session_id} not found")
|
|
296
|
+
|
|
297
|
+
if verbose:
|
|
298
|
+
print(f"Stopping session {session.short_id}...")
|
|
299
|
+
|
|
300
|
+
response = self._request(
|
|
301
|
+
"POST",
|
|
302
|
+
"/api/computing/sessions/stop/",
|
|
303
|
+
data={"session_id": session_id}
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if not response.get("success"):
|
|
307
|
+
error = response.get("error", "Unknown error")
|
|
308
|
+
if "not found" in error.lower() or "non trouvée" in error.lower():
|
|
309
|
+
raise SessionNotFoundError(f"Session {session_id} not found")
|
|
310
|
+
raise APIError(error)
|
|
311
|
+
|
|
312
|
+
# Wait for session to be fully stopped
|
|
313
|
+
if wait_stopped:
|
|
314
|
+
session = self._wait_for_stopped(
|
|
315
|
+
session.short_id,
|
|
316
|
+
session_info=session,
|
|
317
|
+
timeout=timeout,
|
|
318
|
+
verbose=verbose
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
return session
|
|
322
|
+
|
|
323
|
+
def _wait_for_stopped(
|
|
324
|
+
self,
|
|
325
|
+
session_id: str,
|
|
326
|
+
session_info: GPUSession = None,
|
|
327
|
+
timeout: int = 120,
|
|
328
|
+
poll_interval: int = 2,
|
|
329
|
+
verbose: bool = True
|
|
330
|
+
) -> GPUSession:
|
|
331
|
+
"""
|
|
332
|
+
Internal method: Wait for session and pods to be fully stopped.
|
|
333
|
+
|
|
334
|
+
GPU resource updates are handled automatically by the server.
|
|
335
|
+
"""
|
|
336
|
+
if verbose:
|
|
337
|
+
print(f"Waiting for pod termination...", end="", flush=True)
|
|
338
|
+
|
|
339
|
+
start_time = time.time()
|
|
340
|
+
|
|
341
|
+
while time.time() - start_time < timeout:
|
|
342
|
+
try:
|
|
343
|
+
session = self.get_session(session_id)
|
|
344
|
+
|
|
345
|
+
if session.status == "stopped":
|
|
346
|
+
if verbose:
|
|
347
|
+
print(" Done!")
|
|
348
|
+
print(f"\n{'='*50}")
|
|
349
|
+
print(f" SESSION STOPPED")
|
|
350
|
+
print(f"{'='*50}")
|
|
351
|
+
print(f" Name : {session.name}")
|
|
352
|
+
print(f" Short ID : {session.short_id}")
|
|
353
|
+
print(f" Status : {session.status}")
|
|
354
|
+
print(f" GPU : {session.gpu_type} (released)")
|
|
355
|
+
print(f"{'='*50}\n")
|
|
356
|
+
|
|
357
|
+
return session
|
|
358
|
+
|
|
359
|
+
except SessionNotFoundError:
|
|
360
|
+
# Session might be deleted, consider it stopped
|
|
361
|
+
session_name = session_info.name if session_info else f"compute-gpu-{session_id}"
|
|
362
|
+
if verbose:
|
|
363
|
+
print(" Done!")
|
|
364
|
+
print(f"\n{'='*50}")
|
|
365
|
+
print(f" SESSION STOPPED")
|
|
366
|
+
print(f"{'='*50}")
|
|
367
|
+
print(f" Name : {session_name}")
|
|
368
|
+
print(f" Short ID : {session_id}")
|
|
369
|
+
print(f" Status : stopped (deleted)")
|
|
370
|
+
print(f"{'='*50}\n")
|
|
371
|
+
|
|
372
|
+
if session_info:
|
|
373
|
+
session_info.status = "stopped"
|
|
374
|
+
return session_info
|
|
375
|
+
return None
|
|
376
|
+
|
|
377
|
+
if verbose:
|
|
378
|
+
print(".", end="", flush=True)
|
|
379
|
+
|
|
380
|
+
time.sleep(poll_interval)
|
|
381
|
+
|
|
382
|
+
if verbose:
|
|
383
|
+
print(" Timeout!")
|
|
384
|
+
|
|
385
|
+
# Return last known state
|
|
386
|
+
try:
|
|
387
|
+
return self.get_session(session_id)
|
|
388
|
+
except:
|
|
389
|
+
if session_info:
|
|
390
|
+
return session_info
|
|
391
|
+
raise APIError(f"Timeout waiting for session {session_id} to stop")
|
|
392
|
+
|
|
393
|
+
def get_session(self, session_id: str) -> GPUSession:
|
|
394
|
+
"""
|
|
395
|
+
Get details of a specific session.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
session_id: Session ID (short or full UUID)
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
GPUSession object
|
|
402
|
+
"""
|
|
403
|
+
response = self._request(
|
|
404
|
+
"GET",
|
|
405
|
+
"/api/computing/sessions/status/",
|
|
406
|
+
params={"session_id": session_id}
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
if not response.get("success"):
|
|
410
|
+
raise SessionNotFoundError(f"Session {session_id} not found")
|
|
411
|
+
|
|
412
|
+
return self._parse_session(response.get("session", {}))
|
|
413
|
+
|
|
414
|
+
def list_sessions(self, status: str = None) -> List[GPUSession]:
|
|
415
|
+
"""
|
|
416
|
+
List all GPU sessions.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
status: Filter by status ('running', 'stopped', 'pending')
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
List of GPUSession objects
|
|
423
|
+
"""
|
|
424
|
+
params = {}
|
|
425
|
+
if status:
|
|
426
|
+
params["status"] = status
|
|
427
|
+
|
|
428
|
+
response = self._request("GET", "/api/computing/sessions/", params=params)
|
|
429
|
+
|
|
430
|
+
sessions = []
|
|
431
|
+
for session_data in response.get("sessions", []):
|
|
432
|
+
sessions.append(self._parse_session(session_data))
|
|
433
|
+
|
|
434
|
+
return sessions
|
|
435
|
+
|
|
436
|
+
def get_inventory(self) -> List[GPUInventory]:
|
|
437
|
+
"""
|
|
438
|
+
Get available GPU inventory.
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
List of GPUInventory objects
|
|
442
|
+
"""
|
|
443
|
+
response = self._request("GET", "/api/computing/inventory/")
|
|
444
|
+
|
|
445
|
+
inventory = []
|
|
446
|
+
for item in response.get("inventory", []):
|
|
447
|
+
inventory.append(GPUInventory(
|
|
448
|
+
gpu_type=item.get("gpu_type", ""),
|
|
449
|
+
gpu_name=item.get("gpu_name", ""),
|
|
450
|
+
total=item.get("total", 0),
|
|
451
|
+
available=item.get("available", 0),
|
|
452
|
+
in_use=item.get("in_use", 0),
|
|
453
|
+
price_per_hour=item.get("price_per_hour", 0.0)
|
|
454
|
+
))
|
|
455
|
+
|
|
456
|
+
return inventory
|
|
457
|
+
|
|
458
|
+
def generate_sdk_key(self, session_id: str, name: str = "SDK Key") -> str:
|
|
459
|
+
"""
|
|
460
|
+
Generate an sk_live_ API key for a session.
|
|
461
|
+
|
|
462
|
+
This key can be used with the 'clouditia' SDK to execute code.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
session_id: Session ID
|
|
466
|
+
name: Key name (default: "SDK Key")
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
The generated sk_live_ API key
|
|
470
|
+
"""
|
|
471
|
+
response = self._request(
|
|
472
|
+
"POST",
|
|
473
|
+
"/api/computing/sessions/generate-key/",
|
|
474
|
+
data={"session_id": session_id, "name": name}
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
if not response.get("success"):
|
|
478
|
+
raise APIError(response.get("error", "Failed to generate key"))
|
|
479
|
+
|
|
480
|
+
return response.get("api_key")
|
|
481
|
+
|
|
482
|
+
def rename_session(self, session_id: str, new_name: str) -> GPUSession:
|
|
483
|
+
"""
|
|
484
|
+
Rename a GPU session.
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
session_id: Session ID (short or full UUID)
|
|
488
|
+
new_name: New name for the session
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
GPUSession object with updated name
|
|
492
|
+
"""
|
|
493
|
+
response = self._request(
|
|
494
|
+
"POST",
|
|
495
|
+
"/api/computing/sessions/rename/",
|
|
496
|
+
data={"session_id": session_id, "name": new_name}
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
if not response.get("success"):
|
|
500
|
+
error = response.get("error", "Failed to rename session")
|
|
501
|
+
if "not found" in error.lower():
|
|
502
|
+
raise SessionNotFoundError(f"Session {session_id} not found")
|
|
503
|
+
raise APIError(error)
|
|
504
|
+
|
|
505
|
+
# Get updated session
|
|
506
|
+
return self.get_session(session_id)
|
|
507
|
+
|
|
508
|
+
def _parse_session(self, data: Dict) -> GPUSession:
|
|
509
|
+
"""Parse session data into GPUSession object"""
|
|
510
|
+
session_id = data.get("id", "")
|
|
511
|
+
short_id = session_id[:8] if session_id else ""
|
|
512
|
+
return GPUSession(
|
|
513
|
+
id=session_id,
|
|
514
|
+
short_id=short_id,
|
|
515
|
+
name=data.get("name", f"compute-gpu-{short_id}"),
|
|
516
|
+
status=data.get("status", "unknown"),
|
|
517
|
+
gpu_type=data.get("gpu_type", ""),
|
|
518
|
+
gpu_count=data.get("gpu_count", 1),
|
|
519
|
+
vcpu=data.get("vcpu", 0),
|
|
520
|
+
ram=data.get("ram", ""),
|
|
521
|
+
storage=data.get("storage", ""),
|
|
522
|
+
vscode_port=data.get("vscode_port"),
|
|
523
|
+
jupyter_port=data.get("jupyter_port"),
|
|
524
|
+
password=data.get("password"),
|
|
525
|
+
url=data.get("url", f"https://clouditia.com/code-editor/{session_id}/"),
|
|
526
|
+
created_at=self._parse_datetime(data.get("created_at")),
|
|
527
|
+
started_at=self._parse_datetime(data.get("started_at"))
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
@staticmethod
|
|
531
|
+
def _parse_datetime(value) -> Optional[datetime]:
|
|
532
|
+
"""Parse datetime string"""
|
|
533
|
+
if not value:
|
|
534
|
+
return None
|
|
535
|
+
if isinstance(value, datetime):
|
|
536
|
+
return value
|
|
537
|
+
try:
|
|
538
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
539
|
+
except (ValueError, AttributeError):
|
|
540
|
+
return None
|
|
541
|
+
|
|
542
|
+
def __repr__(self):
|
|
543
|
+
return f"GPUManager(user='{self.user.get('username', 'unknown')}')"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exceptions for Clouditia Manager SDK
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ClouditiaManagerError(Exception):
|
|
7
|
+
"""Base exception for Clouditia Manager SDK"""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AuthenticationError(ClouditiaManagerError):
|
|
12
|
+
"""Raised when API key is invalid or expired"""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SessionNotFoundError(ClouditiaManagerError):
|
|
17
|
+
"""Raised when session is not found"""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class InsufficientResourcesError(ClouditiaManagerError):
|
|
22
|
+
"""Raised when requested GPU resources are not available"""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class APIError(ClouditiaManagerError):
|
|
27
|
+
"""Raised when API returns an error"""
|
|
28
|
+
def __init__(self, message, status_code=None, response=None):
|
|
29
|
+
super().__init__(message)
|
|
30
|
+
self.status_code = status_code
|
|
31
|
+
self.response = response
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clouditia-manager
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Manage GPU sessions on Clouditia platform
|
|
5
|
+
Home-page: https://clouditia.com
|
|
6
|
+
Author: Clouditia
|
|
7
|
+
Author-email: support@clouditia.com
|
|
8
|
+
Project-URL: Documentation, https://clouditia.com/docs/manager-sdk
|
|
9
|
+
Project-URL: Bug Reports, https://github.com/clouditia/clouditia-manager/issues
|
|
10
|
+
Project-URL: Source, https://github.com/clouditia/clouditia-manager
|
|
11
|
+
Keywords: gpu cloud computing ml machine-learning clouditia
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Requires-Dist: requests>=2.25.0
|
|
25
|
+
Dynamic: author
|
|
26
|
+
Dynamic: author-email
|
|
27
|
+
Dynamic: classifier
|
|
28
|
+
Dynamic: description
|
|
29
|
+
Dynamic: description-content-type
|
|
30
|
+
Dynamic: home-page
|
|
31
|
+
Dynamic: keywords
|
|
32
|
+
Dynamic: project-url
|
|
33
|
+
Dynamic: requires-dist
|
|
34
|
+
Dynamic: requires-python
|
|
35
|
+
Dynamic: summary
|
|
36
|
+
|
|
37
|
+
# Clouditia Manager SDK
|
|
38
|
+
|
|
39
|
+
SDK Python pour gérer les sessions GPU sur la plateforme Clouditia via l'API Computing (`sk_compute_`).
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install clouditia-manager
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from clouditia_manager import GPUManager
|
|
51
|
+
|
|
52
|
+
# Initialiser avec votre clé API sk_compute_
|
|
53
|
+
manager = GPUManager(api_key="sk_compute_xxxxx")
|
|
54
|
+
|
|
55
|
+
# Créer une session GPU
|
|
56
|
+
# Le SDK attend automatiquement que la session soit prête
|
|
57
|
+
session = manager.create_session(
|
|
58
|
+
gpu_type="nvidia-rtx-3090",
|
|
59
|
+
vcpu=2,
|
|
60
|
+
ram=4,
|
|
61
|
+
storage=20
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Output:
|
|
65
|
+
# Creating GPU session with nvidia-rtx-3090...
|
|
66
|
+
# Session created: 0e4c713a
|
|
67
|
+
# Waiting for session 0e4c713a to be ready... Ready!
|
|
68
|
+
#
|
|
69
|
+
# ==================================================
|
|
70
|
+
# SESSION READY
|
|
71
|
+
# ==================================================
|
|
72
|
+
# Name : compute-gpu-0e4c713a
|
|
73
|
+
# Short ID : 0e4c713a
|
|
74
|
+
# Status : running
|
|
75
|
+
# GPU : nvidia-rtx-3090 x1
|
|
76
|
+
# vCPU : 2
|
|
77
|
+
# RAM : 4Gi
|
|
78
|
+
# Storage : 20Gi
|
|
79
|
+
# URL : https://clouditia.com/code-editor/...
|
|
80
|
+
# Password : xxxxxxxxxxxx
|
|
81
|
+
# ==================================================
|
|
82
|
+
|
|
83
|
+
print(f"Session prête: {session.name}")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Configuration
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from clouditia_manager import GPUManager
|
|
90
|
+
|
|
91
|
+
# Configuration par défaut (production)
|
|
92
|
+
manager = GPUManager(api_key="sk_compute_xxxxx")
|
|
93
|
+
|
|
94
|
+
# Configuration personnalisée (développement local)
|
|
95
|
+
manager = GPUManager(
|
|
96
|
+
api_key="sk_compute_xxxxx",
|
|
97
|
+
base_url="http://127.0.0.1:8000/jobs", # URL de base de l'API
|
|
98
|
+
timeout=120 # Timeout en secondes
|
|
99
|
+
)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Fonctionnalités
|
|
103
|
+
|
|
104
|
+
### 1. Vérifier la clé API
|
|
105
|
+
|
|
106
|
+
La vérification est automatique à l'initialisation :
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
manager = GPUManager(api_key="sk_compute_xxxxx")
|
|
110
|
+
print(f"Utilisateur: {manager.user['username']}")
|
|
111
|
+
print(f"Email: {manager.user['email']}")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 2. Créer une session GPU
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
# Création standard (attend automatiquement que la session soit prête)
|
|
118
|
+
session = manager.create_session(
|
|
119
|
+
gpu_type="nvidia-rtx-3090", # Type de GPU
|
|
120
|
+
gpu_count=1, # Nombre de GPUs
|
|
121
|
+
vcpu=4, # Nombre de vCPUs
|
|
122
|
+
ram=16, # RAM en GB
|
|
123
|
+
storage=20 # Stockage en GB
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# La session est prête avec un nom automatique: compute-gpu-{short_id}
|
|
127
|
+
print(f"Nom: {session.name}") # compute-gpu-0e4c713a
|
|
128
|
+
print(f"ID: {session.short_id}") # 0e4c713a
|
|
129
|
+
print(f"Status: {session.status}") # running
|
|
130
|
+
print(f"URL: {session.url}")
|
|
131
|
+
print(f"Password: {session.password}")
|
|
132
|
+
|
|
133
|
+
# Options avancées
|
|
134
|
+
session = manager.create_session(
|
|
135
|
+
gpu_type="nvidia-rtx-3090",
|
|
136
|
+
wait_ready=True, # Attendre que la session soit prête (défaut: True)
|
|
137
|
+
timeout=180, # Timeout en secondes (défaut: 180)
|
|
138
|
+
verbose=True # Afficher les messages de status (défaut: True)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Mode silencieux (sans attente ni messages)
|
|
142
|
+
session = manager.create_session(
|
|
143
|
+
gpu_type="nvidia-rtx-3090",
|
|
144
|
+
wait_ready=False, # Ne pas attendre
|
|
145
|
+
verbose=False # Pas de messages
|
|
146
|
+
)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 3. Lister les sessions
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
# Toutes les sessions
|
|
153
|
+
sessions = manager.list_sessions()
|
|
154
|
+
|
|
155
|
+
# Filtrer par status
|
|
156
|
+
running = manager.list_sessions(status="running")
|
|
157
|
+
stopped = manager.list_sessions(status="stopped")
|
|
158
|
+
|
|
159
|
+
for session in sessions:
|
|
160
|
+
print(f"{session.name} ({session.short_id}): {session.status} - {session.gpu_type}")
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### 4. Obtenir le status d'une session
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
# Par short ID (8 caractères)
|
|
167
|
+
session = manager.get_session("0e4c713a")
|
|
168
|
+
|
|
169
|
+
print(f"Nom: {session.name}") # compute-gpu-0e4c713a
|
|
170
|
+
print(f"Status: {session.status}") # running
|
|
171
|
+
print(f"GPU: {session.gpu_type}") # nvidia-rtx-3090
|
|
172
|
+
print(f"URL: {session.url}")
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### 5. Renommer une session
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
# Chaque session a un nom par défaut: compute-gpu-{short_id}
|
|
179
|
+
session = manager.create_session(gpu_type="nvidia-rtx-3090")
|
|
180
|
+
print(f"Nom par défaut: {session.name}") # compute-gpu-0e4c713a
|
|
181
|
+
|
|
182
|
+
# Renommer la session
|
|
183
|
+
session = manager.rename_session("0e4c713a", "mon-projet-ml-v1")
|
|
184
|
+
print(f"Nouveau nom: {session.name}") # mon-projet-ml-v1
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 6. Arrêter une session
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
# Arrêt standard (attend automatiquement la suppression du pod)
|
|
191
|
+
session = manager.stop_session("0e4c713a")
|
|
192
|
+
|
|
193
|
+
# Output:
|
|
194
|
+
# Stopping session 0e4c713a...
|
|
195
|
+
# Waiting for pod termination... Done!
|
|
196
|
+
#
|
|
197
|
+
# ==================================================
|
|
198
|
+
# SESSION STOPPED
|
|
199
|
+
# ==================================================
|
|
200
|
+
# Name : mon-projet-ml-v1
|
|
201
|
+
# Short ID : 0e4c713a
|
|
202
|
+
# Status : stopped
|
|
203
|
+
# GPU : nvidia-rtx-3090 (released)
|
|
204
|
+
# ==================================================
|
|
205
|
+
|
|
206
|
+
print(f"Session arrêtée: {session.name}")
|
|
207
|
+
print(f"Status: {session.status}")
|
|
208
|
+
|
|
209
|
+
# Options avancées
|
|
210
|
+
session = manager.stop_session(
|
|
211
|
+
"0e4c713a",
|
|
212
|
+
wait_stopped=True, # Attendre la suppression complète (défaut: True)
|
|
213
|
+
timeout=120, # Timeout en secondes (défaut: 120)
|
|
214
|
+
verbose=True # Afficher les messages (défaut: True)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Mode silencieux
|
|
218
|
+
session = manager.stop_session("0e4c713a", wait_stopped=False, verbose=False)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### 7. Consulter l'inventaire GPU
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
inventory = manager.get_inventory()
|
|
225
|
+
|
|
226
|
+
for gpu in inventory:
|
|
227
|
+
print(f"{gpu.gpu_name}: {gpu.available}/{gpu.total} disponibles")
|
|
228
|
+
print(f" Prix: {gpu.price_per_hour}€/h")
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### 8. Générer une clé SDK (sk_live_)
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
# Générer une clé pour utiliser le SDK clouditia
|
|
235
|
+
sdk_key = manager.generate_sdk_key("0e4c713a", name="Ma clé SDK")
|
|
236
|
+
print(f"Clé SDK: {sdk_key}") # sk_live_xxxxx...
|
|
237
|
+
|
|
238
|
+
# Utiliser avec le SDK clouditia
|
|
239
|
+
from clouditia import GPUSession
|
|
240
|
+
gpu = GPUSession(api_key=sdk_key)
|
|
241
|
+
result = gpu.run("print('Hello GPU!')")
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Types de GPU disponibles
|
|
245
|
+
|
|
246
|
+
| GPU | Slug |
|
|
247
|
+
|-----|------|
|
|
248
|
+
| NVIDIA RTX 3060 Ti | `nvidia-rtx-3060ti` |
|
|
249
|
+
| NVIDIA RTX 3080 Ti | `nvidia-rtx-3080ti` |
|
|
250
|
+
| NVIDIA RTX 3090 | `nvidia-rtx-3090` |
|
|
251
|
+
| NVIDIA RTX 4090 | `nvidia-rtx-4090` |
|
|
252
|
+
|
|
253
|
+
## Gestion des erreurs
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
from clouditia_manager import (
|
|
257
|
+
GPUManager,
|
|
258
|
+
AuthenticationError,
|
|
259
|
+
SessionNotFoundError,
|
|
260
|
+
InsufficientResourcesError,
|
|
261
|
+
APIError
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
manager = GPUManager(api_key="sk_compute_xxxxx")
|
|
266
|
+
session = manager.create_session(gpu_type="nvidia-rtx-4090")
|
|
267
|
+
except AuthenticationError:
|
|
268
|
+
print("Clé API invalide")
|
|
269
|
+
except InsufficientResourcesError:
|
|
270
|
+
print("Aucun GPU disponible")
|
|
271
|
+
except SessionNotFoundError:
|
|
272
|
+
print("Session non trouvée")
|
|
273
|
+
except APIError as e:
|
|
274
|
+
print(f"Erreur API: {e}")
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Référence API
|
|
278
|
+
|
|
279
|
+
| Méthode | Description |
|
|
280
|
+
|---------|-------------|
|
|
281
|
+
| `GPUManager(api_key, base_url, timeout)` | Initialise le SDK |
|
|
282
|
+
| `create_session(gpu_type, gpu_count, vcpu, ram, storage, wait_ready, timeout, verbose)` | Crée une session GPU |
|
|
283
|
+
| `stop_session(session_id, wait_stopped, timeout, verbose)` | Arrête une session |
|
|
284
|
+
| `get_session(session_id)` | Récupère les détails d'une session |
|
|
285
|
+
| `list_sessions(status)` | Liste les sessions (filtre optionnel) |
|
|
286
|
+
| `rename_session(session_id, new_name)` | Renomme une session |
|
|
287
|
+
| `get_inventory()` | Récupère l'inventaire GPU |
|
|
288
|
+
| `generate_sdk_key(session_id, name)` | Génère une clé sk_live_ |
|
|
289
|
+
|
|
290
|
+
## Attributs GPUSession
|
|
291
|
+
|
|
292
|
+
| Attribut | Type | Description |
|
|
293
|
+
|----------|------|-------------|
|
|
294
|
+
| `id` | str | UUID complet de la session |
|
|
295
|
+
| `short_id` | str | ID court (8 caractères) |
|
|
296
|
+
| `name` | str | Nom de la session |
|
|
297
|
+
| `status` | str | running, stopped, pending, failed |
|
|
298
|
+
| `gpu_type` | str | Type de GPU |
|
|
299
|
+
| `gpu_count` | int | Nombre de GPUs |
|
|
300
|
+
| `vcpu` | int | Nombre de vCPUs |
|
|
301
|
+
| `ram` | str | RAM allouée |
|
|
302
|
+
| `storage` | str | Stockage alloué |
|
|
303
|
+
| `url` | str | URL d'accès |
|
|
304
|
+
| `password` | str | Mot de passe |
|
|
305
|
+
|
|
306
|
+
## License
|
|
307
|
+
|
|
308
|
+
MIT License
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
clouditia_manager/__init__.py,sha256=1kv3zDC8klv_X3pPL5JmyMmMu6CjlRCvjJ8naGjd9lo,849
|
|
2
|
+
clouditia_manager/client.py,sha256=Jfh9VHzAWK1LuifPfS_A39lv7jvr00Nef7fYNKHpHxw,17301
|
|
3
|
+
clouditia_manager/exceptions.py,sha256=ysn_iuAkyXDO0b3v-AzQmnpqjKuOoo6tVN8VIvCTTYI,753
|
|
4
|
+
clouditia_manager-1.0.0.dist-info/METADATA,sha256=l-KGFW_qYqVgJQW0kBNKZ6EZJdxh2BAjxDGL45jui_0,8937
|
|
5
|
+
clouditia_manager-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
+
clouditia_manager-1.0.0.dist-info/top_level.txt,sha256=rkZJF7zMHNO17z7JYEKk20GrJ30bSNKetDOESvwkA-k,18
|
|
7
|
+
clouditia_manager-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
clouditia_manager
|