aipmodel 0.2.40__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.
@@ -0,0 +1,938 @@
1
+ import os
2
+ import shutil
3
+ from base64 import b64encode
4
+ from datetime import datetime
5
+ import logging
6
+ import requests
7
+ import subprocess
8
+ from dotenv import load_dotenv
9
+ from huggingface_hub import snapshot_download
10
+
11
+ load_dotenv()
12
+
13
+ from .CephS3Manager import CephS3Manager
14
+
15
+ # Setup logging
16
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # ProjectsAPI manages ClearML project operations
20
+ class ProjectsAPI:
21
+ def __init__(self, post, verbose):
22
+ self.verbose = verbose
23
+ if self.verbose:
24
+ print("Initializing ProjectsAPI...")
25
+ self._post = post
26
+ print("[OK] ProjectsAPI initialized successfully")
27
+
28
+ def create(self, name, description=""):
29
+ if self.verbose:
30
+ print(f"Preparing to create project: name={name}, description={description}")
31
+ print(f"Starting to create project: name={name}, description={description}")
32
+ response = self._post("/projects.create", {"name": name, "description": description})
33
+ if not response or "id" not in response:
34
+ error_msg = "Failed to create project in ClearML"
35
+ print(f"[FAIL] {error_msg}")
36
+ raise ValueError(error_msg)
37
+ if self.verbose:
38
+ print(f"Project creation completed: id={response['id']}")
39
+ print(f"[OK] Project created successfully: id={response['id']}")
40
+ return response
41
+
42
+ def get_all(self):
43
+ if self.verbose:
44
+ print("Preparing to retrieve all projects...")
45
+ print("Starting to get all projects...")
46
+ response = self._post("/projects.get_all")
47
+ if not response or "projects" not in response:
48
+ error_msg = "Failed to retrieve projects from ClearML"
49
+ print(f"[FAIL] {error_msg}")
50
+ raise ValueError(error_msg)
51
+ if self.verbose:
52
+ print(f"Projects retrieval completed: found {len(response['projects'])} projects")
53
+ print(f"[OK] Retrieved {len(response['projects'])} projects successfully")
54
+ return response["projects"]
55
+
56
+ # ModelsAPI manages ClearML model operations
57
+ class ModelsAPI:
58
+ def __init__(self, post, verbose):
59
+ self.verbose = verbose
60
+ if self.verbose:
61
+ print("Initializing ModelsAPI...")
62
+ self._post = post
63
+ print("[OK] ModelsAPI initialized successfully")
64
+
65
+ def get_all(self, project_id=None):
66
+ if self.verbose:
67
+ print(f"Preparing to retrieve models for project_id={project_id}")
68
+ print(f"Starting to get all models for project_id={project_id}")
69
+ payload = {"project": project_id} if project_id else {}
70
+ response = self._post("/models.get_all", payload)
71
+
72
+ # Check expected key in proper format
73
+ if isinstance(response, dict):
74
+ if "models" in response and isinstance(response["models"], list):
75
+ if self.verbose:
76
+ print(f"Models retrieval completed: found {len(response['models'])} models")
77
+ print(f"[OK] Retrieved {len(response['models'])} models successfully")
78
+ return response["models"]
79
+ if "data" in response and isinstance(response["data"], dict) and "models" in response["data"]:
80
+ if self.verbose:
81
+ print(f"Models retrieval completed: found {len(response['data']['models'])} models")
82
+ print(f"[OK] Retrieved {len(response['data']['models'])} models successfully")
83
+ return response["data"]["models"]
84
+
85
+ error_msg = f"'models' not found in response: {response}"
86
+ print(f"[ERROR] {error_msg}")
87
+ raise ValueError("Failed to retrieve models from ClearML")
88
+ return []
89
+
90
+ def create(self, name, project_id, metadata=None, uri=""):
91
+ if self.verbose:
92
+ print(f"Preparing to create model: name={name}, project_id={project_id}, uri={uri}")
93
+ print(f"Starting to create model: name={name}, project_id={project_id}, uri={uri}")
94
+ payload = {
95
+ "name": name,
96
+ "project": project_id,
97
+ "uri": uri
98
+ }
99
+
100
+ if isinstance(metadata, dict):
101
+ payload["metadata"] = metadata
102
+
103
+ response = self._post("/models.create", payload)
104
+ if not response or "id" not in response:
105
+ error_msg = "Failed to create model in ClearML"
106
+ print(f"[FAIL] {error_msg}")
107
+ raise ValueError(error_msg)
108
+ if self.verbose:
109
+ print(f"Model creation completed: id={response['id']}")
110
+ print(f"[OK] Model created successfully: id={response['id']}")
111
+ return response
112
+
113
+ def update(self, model_id, uri=None, metadata=None):
114
+ if self.verbose:
115
+ print(f"Preparing to update model: model_id={model_id}, uri={uri}")
116
+ print(f"Starting to update model: model_id={model_id}, uri={uri}")
117
+ payload = {"model": model_id}
118
+ if uri:
119
+ payload["uri"] = uri
120
+ if isinstance(metadata, dict) or isinstance(metadata, list):
121
+ payload["metadata"] = metadata
122
+
123
+ response = self._post("/models.add_or_update_metadata", payload)
124
+ if not response:
125
+ error_msg = "Failed to update model metadata in ClearML"
126
+ print(f"[FAIL] {error_msg}")
127
+ raise ValueError(error_msg)
128
+ if self.verbose:
129
+ print(f"Model metadata update completed for id={model_id}")
130
+ print(f"[OK] Model metadata updated successfully for id={model_id}")
131
+ return response
132
+
133
+ def edit_uri(self, model_id, uri):
134
+ if self.verbose:
135
+ print(f"Preparing to edit URI for model_id={model_id}, uri={uri}")
136
+ print(f"Starting to edit URI for model_id={model_id}, uri={uri}")
137
+ payload = {"model": model_id, "uri": uri}
138
+ response = self._post("/models.edit", payload)
139
+ if not response:
140
+ error_msg = "Failed to edit model URI in ClearML"
141
+ print(f"[FAIL] {error_msg}")
142
+ raise ValueError(error_msg)
143
+ if self.verbose:
144
+ print(f"Model URI edit completed for id={model_id}")
145
+ print(f"[OK] Model URI edited successfully for id={model_id}")
146
+ return response
147
+
148
+ def get_by_id(self, model_id):
149
+ if self.verbose:
150
+ print(f"Preparing to retrieve model by id: {model_id}")
151
+ print(f"Starting to get model by id: {model_id}")
152
+ response = self._post("/models.get_by_id", {"model": model_id})
153
+ if not response:
154
+ error_msg = f"Failed to retrieve model with ID {model_id} from ClearML"
155
+ print(f"[FAIL] {error_msg}")
156
+ raise ValueError(error_msg)
157
+ if self.verbose:
158
+ print(f"Model retrieval completed: id={model_id}")
159
+ print(f"[OK] Model retrieved successfully: id={model_id}")
160
+ return response
161
+
162
+ def delete(self, model_id):
163
+ if self.verbose:
164
+ print(f"Preparing to delete model: id={model_id}")
165
+ print(f"Starting to delete model: id={model_id}")
166
+ response = self._post("/models.delete", {"model": model_id})
167
+ if not response:
168
+ error_msg = f"Failed to delete model with ID {model_id} from ClearML"
169
+ print(f"[FAIL] {error_msg}")
170
+ raise ValueError(error_msg)
171
+ if self.verbose:
172
+ print(f"Model deletion completed: id={model_id}")
173
+ print(f"[OK] Model deleted successfully: id={model_id}")
174
+ return response
175
+
176
+ # MLOpsManager integrates ClearML and Ceph S3 operations
177
+ class MLOpsManager:
178
+ def get_user_info(self):
179
+ return requests.get(
180
+ os.path.join(self.USER_MANAGEMENT_API, f"user/{self.CLEARML_USERNAME}/metadata?token={self.USER_TOKEN}"),
181
+ timeout=10
182
+ ).json()["metadata"]
183
+
184
+ def __init__(
185
+ self,
186
+ user_name,
187
+ user_token,
188
+ CLEARML_API_HOST=None,
189
+ CEPH_ENDPOINT_URL=None,
190
+ USER_MANAGEMENT_API=None,
191
+ verbose=False
192
+ ):
193
+ self.verbose = verbose
194
+ if self.verbose:
195
+ print("Initializing MLOpsManager...")
196
+ print("Starting MLOpsManager initialization...")
197
+ # Load defaults from environment
198
+ self.CLEARML_USERNAME = user_name
199
+ self.USER_TOKEN = user_token
200
+ self.CLEARML_API_HOST = CLEARML_API_HOST or os.environ.get("CLEARML_API_HOST")
201
+ self.USER_MANAGEMENT_API = USER_MANAGEMENT_API or os.environ.get("USER_MANAGEMENT_API")
202
+ self.CEPH_ENDPOINT_URL = CEPH_ENDPOINT_URL or os.environ.get("CEPH_ENDPOINT_URL")
203
+
204
+ # Validate required ClearML credentials
205
+ if self.verbose:
206
+ print("Validating ClearML credentials...")
207
+ if not all([self.CLEARML_API_HOST, self.CLEARML_USERNAME, self.USER_MANAGEMENT_API, self.CEPH_ENDPOINT_URL]):
208
+ error_msg = "Missing required ClearML configuration parameters"
209
+ print(f"[FAIL] {error_msg}")
210
+ raise ValueError(error_msg)
211
+ if self.verbose:
212
+ print("ClearML credentials validated successfully")
213
+ user_info = self.get_user_info()
214
+ # Ceph configuration from environment
215
+ self.CEPH_ADMIN_ACCESS_KEY = user_info["s3_access_key"]
216
+ self.CEPH_ADMIN_SECRET_KEY = user_info["s3_secret_key"]
217
+ self.CEPH_USER_BUCKET = user_info["s3_bucket"]
218
+ self.CLEARML_ACCESS_KEY = user_info["clearml_access_key"]
219
+ self.CLEARML_SECRET_KEY = user_info["clearml_secret_key"]
220
+
221
+ # Health checks for ClearML services
222
+ if self.verbose:
223
+ print("Performing ClearML service health checks...")
224
+ if not self.check_clearml_service():
225
+ error_msg = "ClearML Server down."
226
+ print(f"[FAIL] {error_msg}")
227
+ raise ValueError(error_msg)
228
+ if not self.check_clearml_auth():
229
+ error_msg = "ClearML Authentication not correct."
230
+ print(f"[FAIL] {error_msg}")
231
+ raise ValueError(error_msg)
232
+ if self.verbose:
233
+ print("ClearML service health checks completed")
234
+
235
+ # Initialize CephS3Manager with user-specific bucket
236
+ if self.verbose:
237
+ print("Initializing CephS3Manager...")
238
+ self.ceph = CephS3Manager(
239
+ self.CEPH_ENDPOINT_URL,
240
+ self.CEPH_ADMIN_ACCESS_KEY,
241
+ self.CEPH_ADMIN_SECRET_KEY,
242
+ self.CEPH_USER_BUCKET,
243
+ verbose=self.verbose
244
+ )
245
+ if self.verbose:
246
+ print("CephS3Manager initialized successfully")
247
+
248
+ # Login to ClearML and extract token
249
+ if self.verbose:
250
+ print("Preparing to login to ClearML...")
251
+ print("Logging in to ClearML...")
252
+ creds = f"{self.CLEARML_ACCESS_KEY}:{self.CLEARML_SECRET_KEY}"
253
+ auth_header = b64encode(creds.encode("utf-8")).decode("utf-8")
254
+ res = requests.post(
255
+ f"{self.CLEARML_API_HOST}/auth.login",
256
+ headers={"Authorization": f"Basic {auth_header}"}
257
+ )
258
+ if res.status_code != 200:
259
+ error_msg = "Failed to authenticate with ClearML"
260
+ print(f"[FAIL] {error_msg}")
261
+ raise ValueError(error_msg)
262
+ self.token = res.json()["data"]["token"]
263
+ if self.verbose:
264
+ print("ClearML login completed successfully")
265
+ print("[OK] Logged in to ClearML successfully")
266
+
267
+ self.projects = ProjectsAPI(self._post, verbose=self.verbose)
268
+ self.models = ModelsAPI(self._post, verbose=self.verbose)
269
+
270
+ # Get or create user-specific project
271
+ if self.verbose:
272
+ print("Checking for user-specific project...")
273
+ print("Getting or creating user-specific project...")
274
+ projects = self.projects.get_all()
275
+ self.project_name = f"project_{self.CLEARML_USERNAME}"
276
+ exists = [p for p in projects if p["name"] == self.project_name]
277
+ self.project_id = exists[0]["id"] if exists else self.projects.create(self.project_name)["id"]
278
+ if self.verbose:
279
+ print(f"User-specific project processing completed: project_id={self.project_id}")
280
+ print(f"[OK] Project ID: {self.project_id}")
281
+ print("[OK] MLOpsManager initialized successfully")
282
+
283
+ def _post(self, path, params=None):
284
+ if self.verbose:
285
+ print(f"Preparing POST request to {path}...")
286
+ print(f"Starting POST request to {path}...")
287
+ headers = {"Authorization": f"Bearer {self.token}"}
288
+ try:
289
+ res = requests.post(f"{self.CLEARML_API_HOST}{path}", headers=headers, json=params)
290
+ res.raise_for_status()
291
+
292
+ data = res.json()
293
+ if "data" not in data:
294
+ error_msg = f"No 'data' key in response: {data}"
295
+ print(f"[ERROR] {error_msg}")
296
+ raise ValueError(f"Request to {path} failed: No data in response")
297
+ if self.verbose:
298
+ print(f"POST request to {path} completed successfully")
299
+ print(f"[OK] POST request to {path} successful")
300
+ return data["data"]
301
+
302
+ except requests.exceptions.RequestException as e:
303
+ error_msg = f"Request to {path} failed: {e}"
304
+ print(f"[ERROR] {error_msg}")
305
+ print(f"[ERROR] Status Code: {res.status_code}, Response: {res.text}")
306
+ raise ValueError(f"Request to {path} failed: {str(e)}")
307
+
308
+ except ValueError as e:
309
+ error_msg = f"Failed to parse JSON from {path}: {e}"
310
+ print(f"[ERROR] {error_msg}")
311
+ print(f"[ERROR] Raw response: {res.text}")
312
+ raise ValueError(f"Failed to parse JSON from {path}: {str(e)}")
313
+
314
+ def check_clearml_service(self):
315
+ """
316
+ Check if ClearML service is reachable and responding.
317
+ """
318
+ if self.verbose:
319
+ print("Preparing to check ClearML service...")
320
+ print("Checking ClearML service...")
321
+ try:
322
+ r = requests.get(self.CLEARML_API_HOST + "/auth.login", timeout=5)
323
+ if r.status_code in [200, 401]:
324
+ if self.verbose:
325
+ print("ClearML service check completed")
326
+ print("[OK] ClearML Service")
327
+ return True
328
+ error_msg = f"ClearML Service {r.status_code}"
329
+ print(f"[FAIL] {error_msg}")
330
+ raise ValueError("ClearML Service is not reachable")
331
+ except Exception as e:
332
+ error_msg = f"ClearML Service: {str(e)}"
333
+ print(f"[FAIL] {error_msg}")
334
+ raise ValueError("ClearML Service is not reachable")
335
+
336
+ def check_clearml_auth(self):
337
+ """
338
+ Check if ClearML credentials are valid by attempting login.
339
+ """
340
+ if self.verbose:
341
+ print("Preparing to check ClearML authentication...")
342
+ print("Checking ClearML authentication...")
343
+ try:
344
+ creds = f"{self.CLEARML_ACCESS_KEY}:{self.CLEARML_SECRET_KEY}"
345
+ auth_header = b64encode(creds.encode("utf-8")).decode("utf-8")
346
+ r = requests.post(
347
+ self.CLEARML_API_HOST + "/auth.login",
348
+ headers={"Authorization": f"Basic {auth_header}"},
349
+ timeout=5
350
+ )
351
+ if r.status_code == 200:
352
+ if self.verbose:
353
+ print("ClearML authentication check completed")
354
+ print("[OK] ClearML Auth")
355
+ return True
356
+ error_msg = f"ClearML Auth {r.status_code}"
357
+ print(f"[FAIL] {error_msg}")
358
+ raise ValueError("ClearML Authentication failed")
359
+ except Exception as e:
360
+ error_msg = f"ClearML Auth: {str(e)}"
361
+ print(f"[FAIL] {error_msg}")
362
+ raise ValueError("ClearML Authentication failed")
363
+
364
+ def get_model_id_by_name(self, name):
365
+ if self.verbose:
366
+ print(f"Preparing to get model ID for name: {name}")
367
+ print(f"Starting to get model ID by name: {name}")
368
+ models = self.models.get_all(self.project_id)
369
+ if self.verbose:
370
+ print(f"Retrieved {len(models)} models for ID lookup")
371
+ for m in models:
372
+ if m["name"] == name:
373
+ if self.verbose:
374
+ print(f"Model ID lookup completed: id={m['id']}")
375
+ print(f"[OK] Model ID found: {m['id']}")
376
+ return m["id"]
377
+ if self.verbose:
378
+ print("Model ID lookup completed: no model found")
379
+ print("[OK] No model found with given name")
380
+ return None
381
+
382
+ def get_model_name_by_id(self, model_id):
383
+ if self.verbose:
384
+ print(f"Preparing to get model name for ID: {model_id}")
385
+ print(f"Starting to get model name by ID: {model_id}")
386
+ model = self.models.get_by_id(model_id)
387
+ result = model.get("name") if model else None
388
+ if result:
389
+ if self.verbose:
390
+ print(f"Model name lookup completed: name={result}")
391
+ print(f"[OK] Model name found: {result}")
392
+ else:
393
+ if self.verbose:
394
+ print("Model name lookup completed: no model found")
395
+ print("[OK] No model found with given ID")
396
+ return result
397
+
398
+ def generate_random_string(self):
399
+ if self.verbose:
400
+ print("Preparing to generate random string...")
401
+ print("Generating random string...")
402
+ import random
403
+ import string
404
+ result = ''.join(random.choices(string.ascii_lowercase + string.digits, k=10))
405
+ if self.verbose:
406
+ print(f"Random string generation completed: {result}")
407
+ print(f"[OK] Random string generated: {result}")
408
+ return result
409
+
410
+ def transfer_from_s3(self, source_endpoint_url, source_access_key, source_secret_key, source_bucket, source_path, dest_prefix, exclude=[".git", ".DS_Store"], overwrite=True):
411
+ """
412
+ Transfer a model from another S3 bucket to the initialized bucket.
413
+ """
414
+ if self.verbose:
415
+ print(f"Preparing transfer from S3: source_path={source_path}, dest_prefix={dest_prefix}")
416
+ print(f"Starting transfer from S3: source_path={source_path}, dest_prefix={dest_prefix}")
417
+ tmp_dir = None
418
+ try:
419
+ tmp_dir = f"./tmp_{self.generate_random_string()}"
420
+ if self.verbose:
421
+ print(f"Creating temporary directory: {tmp_dir}")
422
+ print(f"Creating temporary directory: {tmp_dir}")
423
+ os.makedirs(tmp_dir, exist_ok=True)
424
+
425
+ if self.verbose:
426
+ print("Initializing source CephS3Manager...")
427
+ print("Initializing source CephS3Manager...")
428
+ src_ceph = CephS3Manager(source_endpoint_url, source_access_key, source_secret_key, source_bucket)
429
+ if self.verbose:
430
+ print("Source CephS3Manager initialized")
431
+ print("Downloading from source...")
432
+ src_ceph.download(source_path, tmp_dir, keep_folder=True, exclude=exclude, overwrite=overwrite)
433
+
434
+ if self.verbose:
435
+ print("Preparing to delete destination folder if exists...")
436
+ print("Deleting destination folder if exists...")
437
+ self.ceph.delete_folder(dest_prefix) # Ensure clean state
438
+ if self.verbose:
439
+ print("Destination folder deletion completed")
440
+ print("Uploading to destination...")
441
+ self.ceph.upload(tmp_dir, dest_prefix)
442
+
443
+ if self.verbose:
444
+ print("S3 transfer completed successfully")
445
+ print("[OK] Transfer from S3 successful")
446
+ return True
447
+ except Exception as e:
448
+ error_msg = f"Failed to transfer model from S3: {e}"
449
+ print(f"[FAIL] {error_msg}")
450
+ try:
451
+ self.ceph.delete_folder(dest_prefix)
452
+ except Exception as cleanup_error:
453
+ print(f"[ERROR] Failed to clean up destination folder {dest_prefix}: {cleanup_error}")
454
+ raise ValueError(f"Failed to transfer model from S3: {str(e)}")
455
+ finally:
456
+ if tmp_dir and os.path.exists(tmp_dir):
457
+ try:
458
+ shutil.rmtree(tmp_dir)
459
+ if self.verbose:
460
+ print(f"Temporary directory cleanup completed: {tmp_dir}")
461
+ print(f"[OK] Cleaned up temporary directory {tmp_dir}")
462
+ except Exception as cleanup_error:
463
+ print(f"[ERROR] Failed to clean up temporary directory {tmp_dir}: {cleanup_error}")
464
+
465
+ def add_model(self, source_type, model_name=None, source_path=None, code_path=None,
466
+ external_ceph_endpoint_url=None, external_ceph_bucket_name=None, external_ceph_access_key=None, external_ceph_secret_key=None):
467
+ """
468
+ Add a model from various sources (local, Hugging Face, or S3) and register it in ClearML.
469
+ """
470
+ if self.verbose:
471
+ print(f"Preparing to add model: source_type={source_type}, model_name={model_name}")
472
+ print(f"Starting to add model: source_type={source_type}, model_name={model_name}")
473
+ # Input validation
474
+ if not model_name or not isinstance(model_name, str):
475
+ error_msg = "Model name is required"
476
+ logger.error(error_msg)
477
+ print(f"[ERROR] model_name must be a non-empty string")
478
+ return None
479
+ if source_type not in ["local", "hf", "s3"]:
480
+ error_msg = f"Unknown source_type: {source_type}"
481
+ logger.error(error_msg)
482
+ print(f"[ERROR] {error_msg}")
483
+ return None
484
+ if source_type == "local":
485
+ if not source_path or not os.path.exists(source_path):
486
+ error_msg = f"Local path {source_path} does not exist"
487
+ logger.error(error_msg)
488
+ print(f"[FAIL] {error_msg}")
489
+ return None
490
+ if not os.access(source_path, os.R_OK):
491
+ error_msg = f"Cannot read source_path: {source_path}"
492
+ logger.error(error_msg)
493
+ print(f"[FAIL] {error_msg}")
494
+ return None
495
+ if source_type == "hf" and (not source_path or not isinstance(source_path, str)):
496
+ error_msg = f"Invalid or missing source_path for Hugging Face: {source_path}"
497
+ logger.error(error_msg)
498
+ print(f"[FAIL] {error_msg}")
499
+ return None
500
+ if source_type == "s3" and (
501
+ not all([source_path, external_ceph_access_key, external_ceph_secret_key, external_ceph_endpoint_url, external_ceph_bucket_name])
502
+ or not all(isinstance(x, str) for x in [source_path, external_ceph_access_key, external_ceph_secret_key, external_ceph_endpoint_url, external_ceph_bucket_name])
503
+ ):
504
+ error_msg = "Missing required S3 parameters"
505
+ logger.error(error_msg)
506
+ print(f"[FAIL] {error_msg}")
507
+ return None
508
+ if code_path and (not os.path.isfile(code_path) or not code_path.endswith(".py")):
509
+ error_msg = f"Invalid code_path: {code_path}. Must be a valid .py file"
510
+ logger.error(error_msg)
511
+ print(f"[FAIL] {error_msg}")
512
+ return None
513
+
514
+ if self.verbose:
515
+ print("Checking for existing model...")
516
+ print("Checking if model already exists...")
517
+ if self.get_model_id_by_name(model_name):
518
+ warning_msg = f"Model with name '{model_name}' already exists."
519
+ logger.warning(warning_msg)
520
+ print(f"[WARN] {warning_msg}")
521
+ print("[INFO] Listing existing models:")
522
+ self.list_models(verbose=True)
523
+ return None
524
+
525
+ # Determine model_folder_name according to source_type
526
+ if source_type == "hf":
527
+ model_folder_name = f"hf_{model_name}"
528
+ elif source_type == "local":
529
+ model_folder_name = os.path.basename(source_path)
530
+ elif source_type == "s3":
531
+ model_folder_name = os.path.basename(source_path)
532
+ else:
533
+ model_folder_name = ""
534
+
535
+ if self.verbose:
536
+ print(f"Model folder name set: {model_folder_name}")
537
+ print(f"Model folder name determined: {model_folder_name}")
538
+ have_model_py = False
539
+ temp_model_id = self.generate_random_string()
540
+ dest_prefix = f"models/{temp_model_id}/"
541
+ local_path = None
542
+ temp_local_path = None
543
+
544
+ try:
545
+ if source_type == "local":
546
+ # Create a temporary copy to protect source_path
547
+ temp_local_path = f"./tmp_{self.generate_random_string()}"
548
+ if self.verbose:
549
+ print(f"Preparing to copy local source to temporary path: {temp_local_path}")
550
+ print(f"Copying local source to temporary path: {temp_local_path}")
551
+ shutil.copytree(source_path, temp_local_path, dirs_exist_ok=True)
552
+ if self.verbose:
553
+ print("Local source copy completed")
554
+ print("Deleting destination prefix if exists...")
555
+ self.ceph.delete_folder(dest_prefix) # Ensure clean state
556
+ if self.verbose:
557
+ print("Destination prefix deletion completed")
558
+ print("Uploading temporary path...")
559
+ size_mb = self.ceph.upload(temp_local_path, dest_prefix)
560
+ elif source_type == "hf":
561
+ if self.verbose:
562
+ print("Preparing to download from Hugging Face...")
563
+ print("Downloading from Hugging Face...")
564
+ local_path = snapshot_download(repo_id=source_path)
565
+ if self.verbose:
566
+ print("Hugging Face download completed")
567
+ print("Deleting destination prefix if exists...")
568
+ self.ceph.delete_folder(dest_prefix) # Ensure clean state
569
+ if self.verbose:
570
+ print("Destination prefix deletion completed")
571
+ print("Uploading HF model...")
572
+ size_mb = self.ceph.upload(local_path, os.path.join(dest_prefix, model_folder_name))
573
+ elif source_type == "s3":
574
+ if self.verbose:
575
+ print("Preparing to transfer from S3...")
576
+ print("Transferring from S3...")
577
+ success = self.transfer_from_s3(
578
+ source_endpoint_url=external_ceph_endpoint_url,
579
+ source_access_key=external_ceph_access_key,
580
+ source_secret_key=external_ceph_secret_key,
581
+ source_bucket=external_ceph_bucket_name,
582
+ source_path=source_path,
583
+ dest_prefix=dest_prefix,
584
+ exclude=[".git", ".DS_Store"],
585
+ overwrite=True
586
+ )
587
+ if not success:
588
+ error_msg = "Failed to transfer model from S3"
589
+ print(f"[FAIL] {error_msg}")
590
+ raise ValueError(error_msg)
591
+ uri = f"s3://{self.ceph.bucket_name}/{dest_prefix}"
592
+ if self.verbose:
593
+ print("Calculating size of transferred model...")
594
+ print("Getting size of transferred model...")
595
+ size_mb = self.ceph.get_uri_size(uri)
596
+ else:
597
+ error_msg = f"Unknown source_type: {source_type}"
598
+ print(f"[FAIL] {error_msg}")
599
+ raise ValueError(error_msg)
600
+
601
+ if code_path and os.path.isfile(code_path):
602
+ if self.verbose:
603
+ print("Preparing to upload model.py code...")
604
+ print("Uploading model.py code...")
605
+ self.ceph.upload(code_path, dest_prefix + "model.py")
606
+ have_model_py = True
607
+ if self.verbose:
608
+ print("model.py upload completed")
609
+
610
+ # Create model in ClearML after successful upload
611
+ if self.verbose:
612
+ print("Preparing to create model in ClearML...")
613
+ print("Creating model in ClearML...")
614
+ model = self.models.create(
615
+ name=model_name,
616
+ project_id=self.project_id,
617
+ uri="s3://dummy/uri"
618
+ )
619
+
620
+ model_id = model["id"]
621
+ if model_id != temp_model_id:
622
+ new_dest_prefix = f"models/{model_id}/"
623
+ if self.verbose:
624
+ print(f"Preparing to move folder to new prefix: {new_dest_prefix}")
625
+ print(f"Moving folder to new prefix: {new_dest_prefix}")
626
+ if self.ceph.check_if_exists(new_dest_prefix):
627
+ self.ceph.delete_folder(new_dest_prefix)
628
+ self.ceph.move_folder(dest_prefix, new_dest_prefix)
629
+ self.ceph.delete_folder(dest_prefix)
630
+ dest_prefix = new_dest_prefix
631
+ if self.verbose:
632
+ print("Folder move completed")
633
+
634
+ if self.verbose:
635
+ print("Preparing model metadata...")
636
+ print("Preparing metadata...")
637
+ metadata_list = [
638
+ {"key": "modelFolderName", "type": "str", "value": model_folder_name},
639
+ {"key": "haveModelPy", "type": "str", "value": str(have_model_py).lower()},
640
+ {"key": "modelSize", "type": "float", "value": str(size_mb) if size_mb is not None else "0.0"}
641
+ ]
642
+
643
+ uri = f"s3://{self.ceph.bucket_name}/{dest_prefix}"
644
+ if self.verbose:
645
+ print("Preparing to edit model URI...")
646
+ print("Editing model URI...")
647
+ self.models.edit_uri(model_id, uri=uri)
648
+ if self.verbose:
649
+ print("Model URI edit completed")
650
+ print("Updating model metadata...")
651
+ self.models.update(model_id, metadata=metadata_list)
652
+ if self.verbose:
653
+ print("Model metadata update completed")
654
+
655
+ logger.info(f"Model '{model_name}' (ID: {model_id}) added successfully")
656
+ print(f"[SUCCESS] Model '{model_name}' (ID: {model_id}) added successfully")
657
+ return model_id
658
+
659
+ except (Exception, KeyboardInterrupt) as e:
660
+ error_msg = f"Upload or registration failed: {e}"
661
+ logger.error(error_msg)
662
+ print(f"[ERROR] {error_msg}")
663
+ print("[INFO] Cleaning up partially uploaded model...")
664
+ if 'model_id' in locals():
665
+ try:
666
+ self.models.delete(model_id)
667
+ if self.verbose:
668
+ print("ClearML model cleanup completed")
669
+ print("[OK] Cleaned up ClearML model")
670
+ except Exception as cleanup_error:
671
+ error_cleanup = f"Failed to clean up ClearML model {model_id}: {cleanup_error}"
672
+ logger.error(error_cleanup)
673
+ print(f"[ERROR] {error_cleanup}")
674
+ if dest_prefix:
675
+ try:
676
+ self.ceph.delete_folder(dest_prefix)
677
+ if self.verbose:
678
+ print("Ceph folder cleanup completed")
679
+ print("[OK] Cleaned up Ceph folder")
680
+ except Exception as cleanup_error:
681
+ error_cleanup = f"Failed to clean up Ceph folder {dest_prefix}: {cleanup_error}"
682
+ logger.error(error_cleanup)
683
+ print(f"[ERROR] {error_cleanup}")
684
+ return None
685
+ finally:
686
+ # Clean up temporary local paths
687
+ for path in [local_path, temp_local_path]:
688
+ if path and os.path.exists(path):
689
+ try:
690
+ shutil.rmtree(path)
691
+ if self.verbose:
692
+ print(f"Local directory cleanup completed: {path}")
693
+ print(f"[OK] Cleaned up local directory {path}")
694
+ except Exception as cleanup_error:
695
+ error_cleanup = f"Failed to clean up local directory {path}: {cleanup_error}"
696
+ logger.error(error_cleanup)
697
+ print(f"[ERROR] {error_cleanup}")
698
+
699
+ def get_model(self, model_name, local_dest):
700
+ """
701
+ Download a model by name and return its metadata.
702
+ """
703
+ logger.info("Starting get_model for name=%r, dest=%r", model_name, local_dest)
704
+ if self.verbose:
705
+ print(f"Preparing to get model: model_name={model_name}, local_dest={local_dest}")
706
+ print(f"Starting get_model: model_name={model_name}, local_dest={local_dest}")
707
+
708
+ # Resolve model ID
709
+ try:
710
+ if self.verbose:
711
+ print("Preparing to resolve model ID...")
712
+ print("Resolving model ID...")
713
+ model_id = self.get_model_id_by_name(model_name)
714
+ logger.debug("Resolved model_id=%r for name=%r", model_id, model_name)
715
+ if self.verbose:
716
+ print(f"Model ID resolution completed: id={model_id}")
717
+ print(f"[OK] Resolved model_id={model_id}")
718
+ except Exception as exc:
719
+ error_msg = f"Failed to resolve model ID for name: {model_name}"
720
+ logger.exception(error_msg)
721
+ print(f"[FAIL] {error_msg}")
722
+ raise ValueError(error_msg) from exc
723
+
724
+ if not model_id:
725
+ warning_msg = f"Model not found: {model_name}"
726
+ logger.warning(warning_msg)
727
+ print(f"[WARN] {warning_msg}")
728
+ raise ValueError(warning_msg)
729
+
730
+ # Fetch model metadata
731
+ try:
732
+ if self.verbose:
733
+ print("Preparing to fetch model metadata...")
734
+ print("Fetching model metadata...")
735
+ model_data = self.models.get_by_id(model_id)
736
+ logger.debug("Fetched model_data keys=%s", list(model_data.keys()))
737
+ if self.verbose:
738
+ print("Model metadata fetch completed")
739
+ print("[OK] Model metadata fetched")
740
+ except Exception as exc:
741
+ error_msg = f"Failed to fetch model data for id: {model_id}"
742
+ logger.exception(error_msg)
743
+ print(f"[FAIL] {error_msg}")
744
+ raise ValueError(error_msg) from exc
745
+
746
+ # Preserve original extraction logic
747
+ model = model_data.get("model") or model_data
748
+ logger.debug("Normalized model payload type=%s keys=%s", type(model).__name__, list(model.keys()))
749
+ if self.verbose:
750
+ print("Normalizing model payload...")
751
+ print("Model payload normalized")
752
+
753
+ # Extract URI
754
+ try:
755
+ uri = model["uri"]
756
+ logger.debug("Model URI: %r", uri)
757
+ if self.verbose:
758
+ print(f"URI extraction completed: {uri}")
759
+ print(f"[OK] Extracted URI: {uri}")
760
+ except Exception as exc:
761
+ error_msg = f"Model metadata missing 'uri' field for id: {model_id}"
762
+ logger.exception(error_msg)
763
+ print(f"[FAIL] {error_msg}")
764
+ raise ValueError(error_msg) from exc
765
+
766
+ # Derive remote path
767
+ try:
768
+ _, remote_path = uri.replace("s3://", "").split("/", 1)
769
+ logger.debug("Derived remote_path=%r from uri=%r", remote_path, uri)
770
+ if self.verbose:
771
+ print(f"Remote path derivation completed: {remote_path}")
772
+ print(f"[OK] Derived remote_path: {remote_path}")
773
+ except Exception as exc:
774
+ error_msg = f"Invalid model URI format: {uri!r}"
775
+ logger.exception(error_msg)
776
+ print(f"[FAIL] {error_msg}")
777
+ raise ValueError(error_msg) from exc
778
+
779
+ # Download via ceph client
780
+ try:
781
+ logger.info("Downloading from remote_path=%r to local_dest=%r", remote_path, local_dest)
782
+ if self.verbose:
783
+ print(f"Preparing to download model from {remote_path} to {local_dest}...")
784
+ print(f"Downloading model from {remote_path} to {local_dest}...")
785
+ self.ceph.download(
786
+ remote_path,
787
+ local_dest,
788
+ keep_folder=True,
789
+ exclude=[".git", ".DS_Store"],
790
+ overwrite=False,
791
+ )
792
+ logger.info("Download complete for model id=%r, name=%r", model_id, model_name)
793
+ if self.verbose:
794
+ print("Model download completed")
795
+ print(f"[OK] Download complete for model: {model_name}")
796
+ except Exception as exc:
797
+ error_msg = f"Failed to download model from {remote_path!r} to {local_dest!r}"
798
+ logger.exception(error_msg)
799
+ print(f"[FAIL] {error_msg}")
800
+ raise ValueError(error_msg) from exc
801
+
802
+ logger.info("Returning model metadata for name=%r", model_name)
803
+ if self.verbose:
804
+ print("Preparing to return model metadata...")
805
+ print("[OK] Returning model metadata")
806
+ return model
807
+
808
+ def get_model_info(self, identifier):
809
+ if self.verbose:
810
+ print(f"Preparing to get model info for identifier: {identifier}")
811
+ print(f"Starting to get model info for identifier: {identifier}")
812
+ all_models = self.models.get_all(self.project_id)
813
+ if self.verbose:
814
+ print(f"Retrieved {len(all_models)} models for info lookup")
815
+
816
+ def extract_model_info(model):
817
+ print("=" * 40)
818
+ print(f"ID: {model.get('id')}")
819
+ print(f"Name: {model.get('name')}")
820
+ print(f"Created: {model.get('created')}")
821
+ print(f"Framework: {model.get('framework')}")
822
+ print(f"URI: {model.get('uri')}")
823
+
824
+ # Extract and show metadata (including modelSize)
825
+ metadata = model.get("metadata", {})
826
+ print("Metadata:")
827
+ for key, value in metadata.items():
828
+ print(f" - {key}: {value}")
829
+
830
+ # Highlight modelSize if available
831
+ model_size = metadata.get("modelSize", {}).get("value")
832
+ if model_size is not None:
833
+ try:
834
+ print(f"\n[Model Size] {float(model_size):.2f} MB")
835
+ except (ValueError, TypeError):
836
+ print(f"\n[Model Size] Invalid value: {model_size}")
837
+
838
+ print(f"Labels: {model.get('labels')}")
839
+ print("=" * 40)
840
+
841
+ # Try match by ID
842
+ if self.verbose:
843
+ print("Attempting to match model by ID...")
844
+ print("Trying to match by ID...")
845
+ matched_by_id = [m for m in all_models if m.get("id") == identifier]
846
+ if matched_by_id:
847
+ extract_model_info(matched_by_id[0])
848
+ if self.verbose:
849
+ print("Model info retrieval by ID completed")
850
+ print("[OK] Model info retrieved by ID")
851
+ return matched_by_id[0]
852
+
853
+ # Try match by name
854
+ if self.verbose:
855
+ print("Attempting to match model by name...")
856
+ print("Trying to match by name...")
857
+ matched_by_name = [m for m in all_models if m.get("name") == identifier]
858
+ if matched_by_name:
859
+ for model in matched_by_name:
860
+ extract_model_info(model)
861
+ if self.verbose:
862
+ print("Model info retrieval by name completed")
863
+ print("[OK] Model info retrieved by name")
864
+ return matched_by_name
865
+
866
+ info_msg = f"No model found with identifier: '{identifier}'"
867
+ print(f"[INFO] {info_msg}")
868
+ raise ValueError(f"No model found with identifier: '{identifier}'")
869
+
870
+ def list_models(self, verbose=True):
871
+ if self.verbose:
872
+ print("Preparing to list models...")
873
+ print("Starting to list models...")
874
+ try:
875
+ models = self.models.get_all(self.project_id)
876
+ if verbose:
877
+ grouped = {}
878
+ for m in models:
879
+ grouped.setdefault(m["name"], []).append(m["id"])
880
+ for name, ids in grouped.items():
881
+ print(f"[Model] Name: {name}, Count: {len(ids)}")
882
+ else:
883
+ for m in models:
884
+ print(f"[Model] {m['name']} (ID: {m['id']})")
885
+ if self.verbose:
886
+ print(f"Model listing completed: found {len(models)} models")
887
+ print(f"[OK] Listed {len(models)} models successfully")
888
+ return [(m["name"], m["id"]) for m in models]
889
+ except Exception as e:
890
+ error_msg = f"Failed to list models: {e}"
891
+ print(f"[FAIL] {error_msg}")
892
+ raise ValueError(f"Failed to list models: {str(e)}")
893
+
894
+ def delete_model(self, model_id=None, model_name=None):
895
+ if self.verbose:
896
+ print(f"Preparing to delete model: model_id={model_id}, model_name={model_name}")
897
+ print(f"Starting to delete model: model_id={model_id}, model_name={model_name}")
898
+ if model_name and not model_id:
899
+ if self.verbose:
900
+ print("Resolving model ID from name...")
901
+ model_id = self.get_model_id_by_name(model_name)
902
+ if not model_id:
903
+ warning_msg = f"No model found with name '{model_name}'"
904
+ print(f"[WARN] {warning_msg}")
905
+ raise ValueError(f"No model found with name '{model_name}'")
906
+
907
+ if self.verbose:
908
+ print(f"Retrieving model data for id: {model_id}")
909
+ model_data = self.models.get_by_id(model_id)
910
+ if not model_data:
911
+ warning_msg = f"Model with ID '{model_id}' not found."
912
+ print(f"[WARN] {warning_msg}")
913
+ raise ValueError(f"Model with ID '{model_id}' not found")
914
+
915
+ model = model_data.get("model") or model_data
916
+ uri = model.get("uri")
917
+ if not uri:
918
+ warning_msg = f"Model '{model_id}' has no 'uri'."
919
+ print(f"[WARN] {warning_msg}")
920
+ raise ValueError(f"Model '{model_id}' has no URI")
921
+
922
+ try:
923
+ _, remote_path = uri.replace("s3://", "").split("/", 1)
924
+ if self.verbose:
925
+ print(f"Preparing to delete Ceph folder: {remote_path}")
926
+ print(f"Deleting Ceph folder: {remote_path}")
927
+ self.ceph.delete_folder(remote_path)
928
+ if self.verbose:
929
+ print("Ceph folder deletion completed")
930
+ print("Deleting model from ClearML...")
931
+ self.models.delete(model_id)
932
+ if self.verbose:
933
+ print("ClearML model deletion completed")
934
+ print(f"[SUCCESS] Model '{model_id}' deleted successfully from ClearML and Ceph")
935
+ except Exception as e:
936
+ error_msg = f"Failed to delete model '{model_id}': {e}"
937
+ print(f"[FAIL] {error_msg}")
938
+ raise ValueError(f"Failed to delete model '{model_id}': {str(e)}")